diff --git a/jdk_21_maven/cs/rest-gui/microcks/.editorconfig b/jdk_21_maven/cs/rest-gui/microcks/.editorconfig new file mode 100644 index 000000000..c2cdfb8ad --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/jdk_21_maven/cs/rest-gui/microcks/.jshintrc b/jdk_21_maven/cs/rest-gui/microcks/.jshintrc new file mode 100644 index 000000000..f75096980 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/.jshintrc @@ -0,0 +1,23 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "globals": { + "angular": false + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/ADOPTERS.md b/jdk_21_maven/cs/rest-gui/microcks/ADOPTERS.md new file mode 100644 index 000000000..967e6aee1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/ADOPTERS.md @@ -0,0 +1,47 @@ +> [!IMPORTANT] +> This file is automatically synchronized across Microcks organization repositories. To make edits, please submit a PR to the source repository: https://github.com/microcks/.github/edit/main/ADOPTERS.md + +# Adopters + +**📢 _If you're using Microcks in your organization, please add your company name to this list. 🙏 It really helps the project to gain momentum and credibility. It's a small contribution back to the project with a significant impact._** + +Respect is a core principle in our open source community; see our [Code of Conduct](https://github.com/microcks/.github/blob/main/CODE_OF_CONDUCT.md). We value your contributions and rely on your input, and we believe we can achieve more together. + +If you need to know why and how to add yourself to the list, please read the blog post "[Join the Microcks Adopters list](https://microcks.io/blog/join-adopters-list/) and Empower the vibrant open source Community 🙌" + +This document also lists the organizations using Microcks based on public information in blog posts, events, and videos. If any organization would like to get added or removed, please edit this file (make a pull request) after following our [contribution guide](https://github.com/microcks/.github/blob/master/CONTRIBUTING.md) and by following these specifics guidelines: +- Kindly include a reference (such as a link to a public blog post, video, slides, etc.) that mentions using Microcks. +- You consent to have your company’s name and logo featured on the [Microcks.io website](https://microcks.io/), included in our adopters' section, and potentially displayed in our rotating logo carousel. +- Adopter type follows the [CNCF definitions outlined](https://github.com/cncf/toc/blob/main/FAQ.md#what-is-the-definition-of-an-adopter): CNCF `End-User` member, `Another project`, `end user`, `Service Provider` or `Consultancy`. + +> You can contact the project [maintainers](https://github.com/microcks/.github/blob/main/MAINTAINERS.md) if you have any questions or need further information. + + +| Organization | Contact | Adopter type | Description of Use / Reference | +|---------------------|---------------------|---------------------|-----------------------------------------------------------------------------------| +| [Lombard Odier Group](https://www.lombardodier.com/) | [Ludovic Pourrat](https://github.com/ludovic-pourrat) | `end user` | Multi-protocol API Mocking and Sandbox as a service #APIOps. APIdays Paris 2022 - Adding a mock as a service capability to your API strategy portfolio 👉 [slide deck](https://speakerdeck.com/apidays/apidays-paris-2022-adding-a-mock-as-a-service-capability-to-your-api-strategy-portfolio-ludovic-pourrat-lombard-odier) ⭐️ | +| [La Poste Groupe](https://www.lapostegroupe.com/) | [Nicolas Matelot](https://www.linkedin.com/in/nicolas-matelot/) [Romain Gil](https://www.linkedin.com/in/romain-gil-8444898a) | `end user` | Cloud-native application development, API Mocking and Sandbox. | +| [J.B. Hunt](https://www.jbhunt.com/) | [Carol Gschwend](https://github.com/carolgschwend) | `end user` | Accelerating cloud-native application development "The developers of the project mentioned above saved at least 7 months using Microcks. They were not only able to work concurrently but also captured the exact business requirements specified by the product owner in the form of example request/response pairs. These persistent mocks can now be utilized in sandbox environments if needed." See J.B. Hunt ⭐️ [blog post](https://microcks.io/blog/jb-hunt-mock-it-till-you-make-it/) ⭐️ | +| [Société Générale](https://www.societegenerale.com/en) | [Patrice Lachance](https://github.com/patlachance) | `end user` | Multi-protocol API Mocking, Testing and Sandbox for cloud-native APIs / applications. Cloud Innovation Platform presentation and Microcks demo 👉 [Red Hat Summit 2019](https://www.redhat.com/files/summit/session-assets/2019/T8B6B4.pdf) ⭐️| +| [Deloitte](https://www.deloitte.com/global/en.html) | [Madiha Rehman](https://www.linkedin.com/in/madihar/) | `End-User` `Consultancy` | Utilised Microcks to create backend mocks for 170+ APIs including REST and SOAP services.| +| [sesam-vitale](https://www.sesam-vitale.fr/) | [Vincent Fasciaux](mailto:vincent.fasciaux@sesam-vitale.fr) | `end user` | We use Microcks to replace the SUT's dependencies with test doubles in order to accelerate our API deliveries.| +| [BNP PARIBAS](https://group.bnpparibas/en/) | [Nadji BERRAF](https://www.linkedin.com/in/nadji-berraf-26707148/) | `end user` | We use Microcks since 2022 to mock our legacy core banking systems and mainframe APIs in order to increase agility, accelerate development, and reduce costs. Microcks is also integrated into our API-first strategy for building and delivering new cloud-native services.| +| [Akwatype](https://akwatype.io) | [Pierre-Michel Bret](https://www.linkedin.com/in/pierre-michel-bret/) | `Service Provider` | Use of Microcks to mock the APIs corresponding to the OpenAPI contracts generated by Akwatype, integration through Git.| +| [codecentric AG](https://www.codecentric.de) | [Daniel Kocot](https://www.linkedin.com/in/danielkocot/) | `Consultancy` | API Operations pipeline with an integration of Microcks and consulting services around API Mocking and Testing.| +| [CNAM](https://www.ameli.fr) | [Sébastien Fraigneau](https://www.linkedin.com/in/s%C3%A9bastien-fraigneau-82826a2) | `end user` | Using Microcks to mock SOAP services for the French healthcare system. REST is coming.| +| [CloudAPPi](https://cloudappi.net) | [Marco Antonio Sanz Molina Prados](https://www.linkedin.com/in/marco-antonio-sanz-molina-prados-09733518/)| `Service Provider` | We manage over 40 APIs strategies in medium and big companies and install Microcks as a mock server. | +| [Sypid](https://www.sypid.com/) | [Zubair Aslam](https://www.linkedin.com/in/zubes1/)| `Consultancy` | Sypid consultants are highly experienced in the Spec-first approach to API and integration design. We use Microcks to implement this approach effectively. We found the docker extension is specifically useful to get started quickly.| +| [Inetum Software](https://www.inetum.com/) | [Jérôme Palon](https://www.linkedin.com/in/jpalon/)| `end user` `Consultancy` | We use Microcks as an API centralisation and mock server for the social and civil security division, paving the path for migration to microservices and event-driven cloud architecture.| +| [Fluent CI](https://fluentci.io/) | [Tsiry Sandratraina](https://github.com/tsirysndr)| `Another project` | We use Microcks to mock and test our REST and GraphQL APIs. We also provide an open source [Microcks module](https://github.com/fluent-ci-templates/microcks-pipeline) for [Dagger](https://dagger.io) and [Fluent CI](https://fluentci.io).| +| [Nordic Semiconductor](https://nordicsemi.com) | [Patrick Barnes](https://www.linkedin.com/in/patrick-barnes-pdx/) | `end user` | We use Microcks mainly via testcontainers to test the REST APIs (and probably soon, our async, event-based APIs) for the microservices that comprise our IoT platform, [nRFCloud.com](https://nrfcloud.com/). Microcks has been invaluable, and the Microcks team a real pleasure to work with!| +| [Office national de l'emploi (ONEm)](https://www.onem.be/) | [Samuel Antoine](https://www.linkedin.com/in/samuel-antoine-07347b171/) [Christophe Lopez](https://www.linkedin.com/in/aeoncl/) | `end user` | We use Microcks as both OpenAPI contract testing tool and mock provider.| +| [Bitso](https://bitso.com/) | [Caio Amaral](https://www.linkedin.com/in/camaral) | `end user` | Microcks supports Bitso's integration and capacity testing infrastructure, allowing us to isolate the applications through well defined gRPC contracts.| +| [Catena Clearing](https://catenaclearing.io/) | [Andre Sionek](https://www.linkedin.com/in/andresionek) | `end user` | We use Microcks to mock 3rd party APIs so we can develop against them without hitting the real endpoints, Microcks is also used to run integration tests.| +| [Traefik](https://traefik.io/) | [Emile Vauge](https://github.com/emilevauge) | `Another project` `Service Provider` | We use Microcks in [Traefik’s API sandbox solution](https://traefik.io/solutions/api-mocking/) to enable developers create, publish, and consume mock APIs with production-like UX and SLAs.| +| [APIQuality](https://apiquality.io/) | [Marco Antonio Sanz](https://www.linkedin.com/in/marco-antonio-sanz-molina-prados-09733518/) [Omar del Valle](https://www.linkedin.com/in/omardelvalle/) | `Service Provider` | Your low code tool to integrate APIOps. Develops secure and quality APIs implementing API First methodology. We use Microcks as our default tool for mocking. Create secure and functional APIs from a single tool!| +| [Banco PAN](https://www.bancopan.com.br/) | [Renan Elias Saraiva](https://www.linkedin.com/in/renan-elias-28781894/) | `end user` | We leverage Microcks to create a virtual environment, simulating API and AsyncAPI services while seamlessly connecting with our external Kafka cluster for testing purposes. | +|[ Office des Postes et Télécommunications de Nouvelle-Calédonie](https://www.opt.nc/) | [Adrien SALES](https://www.linkedin.com/in/adrien-sales/) [Vinh Faucher](https://www.linkedin.com/in/vinh-faucher/) [Michèle BARRE](https://www.linkedin.com/in/michelebarre/)| `end user` | API & Kafka Mocking (end users & Github.com CI), see [Microcks for dummies](https://dev.to/optnc/microcks-for-dummies-1imn) on [dev.to/optnc](https://dev.to/optnc) for more information.| +|[ Bump.sh](https://bump.sh/) | [Christophe Dujarric](https://www.linkedin.com/in/christophedujarric/) [Sebastien Charrier](https://www.linkedin.com/in/sebastiencharrier/)| `Service Provider` | Using Bump.sh and Microcks together allows you to create a clear feedback cycle of [API documentation](https://bump.sh/), testing, simulation, trial implementation, and iteration. [Read more about this](https://bump.sh/blog/microcks-bump-sh-testing-mocking-docs).| +|[Michelin](https://www.michelin.fr/) | [Alex Picarle](https://github.com/AlexP63) | `End-User` | We use Microcks to elaborate our PoCs, to offer api sandbox or discovery trials and overall promote #DesignFirst approach.| +|[Amway](https://www.amway.com/) | [Brian Hibma](https://www.linkedin.com/in/brianhibma/) [Sai Bommakanti](https://www.linkedin.com/in/saiabhinay-bommakanti-31014943/) | `end user` | We use Microcks primarily as a platform for API and Event mocking and discovery, integrating it into our DevOps and CI/CD practices, and unblocking parallel, contract-based development from teams. | +|[BITMARCK](https://www.bitmarck.de) | [Michael Goll](https://github.com/goll-michael) | `end user` | We use Microcks to mock the APIs that correspond to the generated or manually created OpenAPI contracts.| diff --git a/jdk_21_maven/cs/rest-gui/microcks/BACKERS.md b/jdk_21_maven/cs/rest-gui/microcks/BACKERS.md new file mode 100644 index 000000000..bf55f6056 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/BACKERS.md @@ -0,0 +1,46 @@ +# Sponsors & Backers + +[Microcks](https://microcks.io/) is a [Cloud Native Computing Foundation Sandbox](https://landscape.cncf.io/?selected=microcks) project 🚀 with its ongoing development made possible entirely by the support of the awesome sponsors and backers listed in this file. If you'd like to join them, please consider [sponsoring](https://opencollective.com/microcks) Microcks's development. + +**BTW 📢 _If you're using Microcks in your organization, please add your company name to the adopters list. 🙏 It really helps the project to gain momentum and credibility. It's a small contribution back to the project with a big impact._** +https://github.com/microcks/.github/blob/main/ADOPTERS.md + +> Platinum💎, gold🥇 and silver🥈 **sponsors**, [join us](https://opencollective.com/microcks) 🙌
+> **Invest** in **your supply chain!** + +If you run a business and is using Microcks in a revenue-generating product/service, it would make business sense to sponsor Microcks development: it ensures the project that you rely on a healthy, strong and engaged community. It can also help your exposure in the open source community and makes it easier to attract new talents. + +If you'd like to help and support Microcks to [level up](https://www.cncf.io/project-metrics/) within the [CNCF](https://www.cncf.io) and keep growing the open source way, please consider: + +- [Become a backer or sponsor on OpenCollective](https://opencollective.com/microcks). + +## Special Sponsors +[image width="350"]: # +

+ Postman logo +

+ +## Platinum Sponsors +[image width="300"]: # +[Become the first platinum sponsor](https://opencollective.com/microcks/contribute/platinum-sponsors-61341/checkout?interval=month&amount=2000&name=&legalName=&email=) + +## Gold Sponsors +[image width="250"]: # +[Become the first gold sponsor](hhttps://opencollective.com/microcks/contribute/gold-sponsors-61340/checkout?interval=month&amount=1000&name=&legalName=&email=) + +## Silver Sponsors +[image width="200"]: # +[Become the first silver sponsor](https://opencollective.com/microcks/contribute/silver-sponsors-61339/checkout?interval=month&amount=500&name=&legalName=&email=) + +## Bronze Sponsors +[image width="150"]: # +

+ Bitso logo + Fern logo +

+ +[Become a bronze sponsor](https://opencollective.com/microcks/contribute/bronze-sponsors-61338/checkout?interval=month&amount=100&name=&legalName=&email=) + +## OpenCollective Generous Backers + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/BUILDING.md b/jdk_21_maven/cs/rest-gui/microcks/BUILDING.md new file mode 100644 index 000000000..1aecc07bd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/BUILDING.md @@ -0,0 +1,75 @@ +# Building guide + +**Want to contribute and build stuff? Great!** ✨ We try to make it easy, and all contributions, even the smaller ones, are more than welcome. This includes bug reports, fixes, documentation, examples... + +First, you may need to read our [global contribution guide](https://github.com/microcks/.github/blob/master/CONTRIBUTING.md) and then to read this page. + +## Reporting an issue + +This project uses GitHub issues to manage the issues. Open an issue directly in GitHub. + +If you believe you found a bug, and it's likely possible, please indicate a way to reproduce it, what you are seeing and what you would expect to see. +Don't forget to indicate your Java, Maven and/or Docker version. + +## Setup + +## Build + +### Build the whole project + +You need to have [Apache Maven](https://maven.apache.org) (version >= 3.5) up and running as well as a valid Java Development Kit (version >= 21) install to build the project. + +``` +$ git clone https://github.com/microcks/microcks.git +[...] +$ cd microcks +$ mvn clean install +[...] +[INFO] ------------------------------------------------------------------------ +[INFO] Reactor Summary for Microcks 1.12.0-SNAPSHOT: +[INFO] +[INFO] Microcks ........................................... SUCCESS [ 0.408 s] +[INFO] Microcks Model ..................................... SUCCESS [ 1.771 s] +[INFO] Microcks Util ...................................... SUCCESS [ 4.590 s] +[INFO] Microcks EL ........................................ SUCCESS [ 1.018 s] +[INFO] Microcks App ....................................... SUCCESS [ 40.540 s] +[INFO] Microcks Async Minion .............................. SUCCESS [ 8.425 s] +[INFO] Microcks Distros ................................... SUCCESS [ 0.034 s] +[INFO] Microcks Uber App .................................. SUCCESS [ 0.850 s] +[INFO] Microcks Uber Async Minion ......................... SUCCESS [ 3.818 s] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 01:01 min +[INFO] Finished at: 2025-04-07T18:07:39+02:00 +[INFO] ------------------------------------------------------------------------ +``` + +### Build and run webapp jar + +For now, there's still a problem with Frontend integration tests configuration so you should disable them using the following flag: + +``` +$ cd webapp +$ mvn -Pprod package +[...] +$ java -jar target/microcks-x.y.z-SNAPSHOT.jar +``` + +### Build and run webapp Docker image + +``` +$ cd webapp +$ mvn -Pprod clean package && docker build -f src/main/docker/Dockerfile -t microcks/microcks-uber:nightly . +[...] +$ cd ../install/docker-compose +$ docker-compose -f docker-compose.yml up -d +``` +After spinning up the containers, you will now have access to Keycloak for account management, and microcks webapp to setup mocking, etc. + +You can login to keycloak on `http://localhost:18080/` with username and password `admin`. +You can login to microcks webapp with the username `admin` and password `microcks123`. + +## Before you contribute + +To contribute, use GitHub Pull Requests, from your **own** fork. \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/CHANGELOG.md b/jdk_21_maven/cs/rest-gui/microcks/CHANGELOG.md new file mode 100644 index 000000000..efa012d9d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/CHANGELOG.md @@ -0,0 +1,17 @@ +# CHANGELOG + +## Introduction + +This file provides a high-level summary of the changes and updates made to the Microcks project. + +## Releases + +For a comprehensive list of changes, please visit the [official release page](https://github.com/microcks/microcks/releases) + +## For More Information + +For more detailed release notes, please refer to the [Microcks blog page](https://microcks.io/blog/). + +## For specific changes on this repo + +Refer to the [activity view](https://docs.github.com/en/repositories/viewing-activity-and-data-for-your-repository/using-the-activity-view-to-see-changes-to-a-repository) of the current repository for detailed information on all recent changes. diff --git a/jdk_21_maven/cs/rest-gui/microcks/CODEOWNERS b/jdk_21_maven/cs/rest-gui/microcks/CODEOWNERS new file mode 100644 index 000000000..4f11dde4d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/CODEOWNERS @@ -0,0 +1,9 @@ +# This file provides an overview of code owners in this repository. + +# Each line is a file pattern followed by one or more owners. +# The last matching pattern has the most precedence. +# For more details, read the following article on GitHub: https://help.github.com/articles/about-codeowners/. + +# The default owners are automatically added as reviewers when you open a pull request unless different owners are specified in the file. + +* @lbroudoux @yada \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/CODE_OF_CONDUCT.md b/jdk_21_maven/cs/rest-gui/microcks/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..bbbbad7c3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/CODE_OF_CONDUCT.md @@ -0,0 +1,35 @@ +# Microcks Community Code of Conduct + +Microcks project adheres to the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md), aligning with the values of collaboration, transparency, and inclusivity that define open source. + +## Our Pledge + +As contributors and maintainers of the Microcks project, we are committed to fostering an open, welcoming, and inclusive environment. By participating in our community, we pledge to: + +- Promote a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, experience level, nationality, personal appearance, race, religion, or sexual identity and orientation. +- Support a spirit of mutual respect, empathy, and cooperation, ensuring that every contribution—big or small—is valued. +- Encourage collaboration, knowledge-sharing, and continuous learning, as these are the cornerstones of any successful open-source community. + +## Our Standards + +To ensure a productive, positive experience for all community members, we ask everyone to: + +- **Be respectful and considerate**: Engage with others constructively, appreciating the diverse perspectives and ideas within the community. +- **Be collaborative**: Open-source thrives on shared efforts and diverse input. Offer help where needed and seek help when necessary. +- **Lead by example**: Encourage new contributors and foster an environment where everyone feels welcome to participate. +- **Value inclusivity**: Open source is for everyone. Actively support a diverse range of contributors and voices, regardless of background. + +## Enforcement + +Instances of unacceptable behavior—including harassment, abusive language, or any other forms of misconduct—should be reported to the Microcks project team at **info@microcks.io**. + +The project team will: +- Review and investigate all reports confidentially. +- Take action deemed appropriate for the situation, including potential temporary or permanent bans from the community. +- Keep the identity of the reporter confidential, ensuring a safe and respectful process for all involved. + +## A Community-Driven Open Source + +Microcks thrives because of its open-source community. We encourage all members to actively contribute, share knowledge, and help make our project better for everyone. By following this Code of Conduct, we can create a community that supports innovation, collaboration, and the open-source values we all believe in. + +Thank you for being part of the Microcks community! diff --git a/jdk_21_maven/cs/rest-gui/microcks/CONTRIBUTING.md b/jdk_21_maven/cs/rest-gui/microcks/CONTRIBUTING.md new file mode 100644 index 000000000..500bfc70b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/CONTRIBUTING.md @@ -0,0 +1,85 @@ +# Contributing to Microcks + +We love your input! We want to make contributing to this project as easy and transparent as possible. + +## Contribution recogniton + +We plan to use [All Contributors](https://allcontributors.org/docs/en/specification) specification to handle recognitions. + +## Summary of the contribution flow + +The following is a summary of the ideal contribution flow. Please, note that Pull Requests can also be rejected by the maintainers when appropriate. + +``` + ┌───────────────────────┐ + │ │ + │ Open an issue │ + │ (a bug report or a │ + │ feature request) │ + │ │ + └───────────────────────┘ + ⇩ + ┌───────────────────────┐ + │ │ + │ Open a Pull Request │ + │ (only after issue │ + │ is approved) │ + │ │ + └───────────────────────┘ + ⇩ + ┌───────────────────────┐ + │ │ + │ Your changes will │ + │ be merged and │ + │ published on the next │ + │ release │ + │ │ + └───────────────────────┘ +``` + +## Code of Conduct + +Microcks has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](CODE_OF_CONDUCT.md) so that you can understand what sort of behaviour is expected. + +## Our Development Process + +We use Github to host code, to track issues and feature requests, as well as accept pull requests. + +## Issues + +[Open an issue](https://github.com/microcks/microcks/issues/new) **only** if you want to report a bug or a feature. Don't open issues for questions or support, instead join our [Discord #support channel](https://microcks.io/discord-invite) or our [GitHub discussions](https://github.com/orgs/microcks/discussions) and ask there. + +## Bug Reports and Feature Requests + +Please use our issues templates that provide you with hints on what information we need from you to help you out. + +## Pull Requests + +**Please, make sure you open an issue before starting with a Pull Request, unless it's a typo or a really obvious error.** Pull requests are the best way to propose changes to the specification. Take time to check the current working branch for the repository you want to contribute on before working :wink: + +## Conventional commits + +Our repositories follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) specification. Releasing to GitHub and NPM is done with the support of [semantic-release](https://semantic-release.gitbook.io/semantic-release/). + +Pull requests should have a title that follows the specification, otherwise, merging is blocked. If you are not familiar with the specification simply ask maintainers to modify. You can also use this cheatsheet if you want: + +- `fix: ` prefix in the title indicates that PR is a bug fix and PATCH release must be triggered. +- `feat: ` prefix in the title indicates that PR is a feature and MINOR release must be triggered. +- `docs: ` prefix in the title indicates that PR is only related to the documentation and there is no need to trigger release. +- `chore: ` prefix in the title indicates that PR is only related to cleanup in the project and there is no need to trigger release. +- `test: ` prefix in the title indicates that PR is only related to tests and there is no need to trigger release. +- `refactor: ` prefix in the title indicates that PR is only related to refactoring and there is no need to trigger release. + +What about MAJOR release? just add `!` to the prefix, like `fix!: ` or `refactor!: ` + +Prefix that follows specification is not enough though. Remember that the title must be clear and descriptive with usage of [imperative mood](https://chris.beams.io/posts/git-commit/#imperative). + +Happy contributing :heart: + +## License + +When you submit changes, your submissions are understood to be under the same [Apache 2.0 License](https://github.com/microcks/microcks/blob/master/LICENSE) that covers the project. Feel free to [contact the maintainers](https://github.com/microcks/.github/blob/main/MAINTAINERS.md) if that's a concern. + +## References + +This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/master/CONTRIBUTING.md). diff --git a/jdk_21_maven/cs/rest-gui/microcks/DEPENDENCY_POLICY.md b/jdk_21_maven/cs/rest-gui/microcks/DEPENDENCY_POLICY.md new file mode 100644 index 000000000..10c51d6bf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/DEPENDENCY_POLICY.md @@ -0,0 +1,64 @@ +# Microcks External Dependency Policy + +## External dependencies dashboard + +The list of external dependencies in this Microcks repository with their current version is available at +[network/dependencies](../../network/dependencies) + +## Declaring external dependencies + +Depending on the Microcks Core or Module/extension technology stack, all external dependencies should be declared +in either [pom.xml](pom.xml), [package.json](package.json) or [go.mod](go.mod) root files. + +Dependency declarations must: + +* Provide a meaningful project name and URL. +* State the version in the `version` field. String interpolation can be used to aggregate all versions declaration at the same place. +* Versions should prefer release versions over main branch GitHub SHA tarballs. A comment is necessary if the latter is used. + This comment should contain the reason that a non-release version is being used. + +## New external dependencies + +The criteria below are used to evaluate new dependencies. They apply to all core dependencies and any extension +that is robust to untrusted downstream or upstream traffic. The criteria are guidelines, exceptions may be granted +with solid rationale. Precedent from existing extensions does not apply; there are extant extensions in violation +of this policy which we will be addressing over time, they do not provide grounds to ignore policy criteria below. + +|Criteria|Requirement|Mnemonic|Weight|Rationale| +|--------|-----------|--------|------|---------| +|Cloud Native Computing Foundation (CNCF) [approved license](https://github.com/cncf/foundation/blob/master/allowed-third-party-license-policy.md#approved-licenses-for-allowlist)|MUST|License|High|| +|Dependencies must not substantially increase the binary size unless they are optional (i.e. confined to specific extensions)|MUST|BinarySize|High|Microcks Uber is sensitive to binary size. We should pick dependencies that are used in core with this criteria in mind.| +|No duplication of existing dependencies|MUST|NoDuplication|High|Avoid maintenance cost of multiple utility libs with same goals (ex: JSON parsers)| +|CVE history appears reasonable, no pathological CVE arcs|MUST|SoundCVEs|High|Avoid dependencies that are CVE heavy in the same area (e.g. buffer overflow) +|Security vulnerability process exists, with contact details and reporting/disclosure process|MUST|SecPolicy|High|Lack of a policy implies security bugs are open zero days| +|Code review (ideally PRs) before merge|MUST|Code-Review|Normal|Consistent code reviews| +|> 1 contributor responsible for a non-trivial number of commits|MUST|Contributors|Normal|Avoid bus factor of 1| +|Tests run in CI|MUST|CI-Tests|Normal|Changes gated on tests| +|Hosted on a git repository and the archive fetch must directly reference this repository. We will NOT support intermediate artifacts built by-hand located on GCS, S3, etc.|MUST|Source|Normal|Flows based on manual updates are fragile (they are not tested until needed), often suffer from missing documentation and shared exercise, may fail during emergency zero day updates and have no audit trail (i.e. it's unclear how the artifact we depend upon came to be at a later date).| +|High test coverage (also static/dynamic analysis, fuzzing)|SHOULD|Test-Coverage|Normal|Key dependencies must meet the same quality bar as Envoy| +|Do other significant projects have shared fate by using this dependency?|SHOULD|SharedFate|High|Increased likelihood of security community interest, many eyes.| +|Releases (with release notes)|SHOULD|Releases|Normal|Discrete upgrade points, clear understanding of security implications. We have many counterexamples today (e.g. CEL, re2).| +|Commits/releases in last 90 days|SHOULD|Active|Normal|Avoid unmaintained deps, not compulsory since some code bases are “done”| + + +## Maintaining existing dependencies + +We rely on community volunteers to help track the latest versions of dependencies. On a best effort +basis: + +* Core Microcks dependencies will be updated by the Microcks maintainers/security team. + +* Module/extension [CODEOWNERS](./CODEOWNERS) should update extension specific dependencies. + +Where possible, we prefer the latest release version for external dependencies, rather than main branch GitHub SHA tarballs. + +If you intend to update a dependency, please assign the relevant ticket to yourself and/or associate any Pull Request (eg by adding `chore: #1234`) with the issue. + + +## Policy exceptions + +The following dependencies are exempt from the policy: + +* Any developer-only facing tooling or the documentation build. + +* Transitive build time dependencies. \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/GOVERNANCE.md b/jdk_21_maven/cs/rest-gui/microcks/GOVERNANCE.md new file mode 100644 index 000000000..f288a49ca --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/GOVERNANCE.md @@ -0,0 +1,91 @@ +# Microcks Governance + +This document defines governance policies for the Microcks project. + +## Principles +The Microcks project community adheres to the following principles: + +- **Open**: The Microcks community strives to be open, accessible and welcoming to everyone. Anyone may contribute, and contributions are available to all users according to open source values and licenses. +- **Transparent** and **accessible**: Any changes to the Microcks source code and collaborations on the project are publicly accessible (GitHub code, issues, PRs, and discussions). +- **Merit**: Ideas and contributions are accepted according to their technical merit and alignment with project objectives, scope, and design principles. +- **Vendor-neutral**: Microcks is designed and maintained to be fully aligned with the [principles](https://contribute.cncf.io/maintainers/community/vendor-neutrality/) of the Cloud Native Computing Foundation (CNCF). + +Join us 👉 https://microcks.io/community/ + +## Maintainers, Code Owners, Contributors and Adopters +The Microcks project has four roles. All project members operate in one (or more) of these roles: + +| Level | Role | Responsibilities | +| :--- | :--- | :--- | +| 1 | **Maintainer** | Vote, Develop roadmap and contribution guidelines; Review, Approve/Reject, Merge, and Manage repositories. Maintainers are elected or removed by the current maintainers. A Maintainer has authority over the entire Microcks project: the organization and every project, sub-project and repo within the organization.| +| 2 | **Code Owner**| Have special expertise in a particular domain within the Microcks project. The domain may be a sub-project, repo or other responsibility as defined by the Maintainers. The maintainers grant a code owner (alias Domain Maintainers) a set of authorities and responsibilities for the domain. Code owners are expected to join maintainer and community meetings when required. A code owner has no responsibilities for the entire project, organization or projects outside their domain. Code owners role, refer to [GitHub CODEOWNERS](https://docs.github.com/fr/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) capabilities.| +| 3 | **Contributor** | Contribute code, test and document the project. A contributor’s authority applies to one or more sub-projects. Microcks is a very welcoming community and is eager to onboard and help anyone from the open source community to contribute to the project. | +| 4 | **Adopter** | Use the Microcks project, with or without contributing to the project. Adopters are encouraged to raise issues, provide feedback and participate in discussions on sub-projects within a public forum and community. | + +- Maintainer and Code Owners list: https://github.com/microcks/.github/blob/main/MAINTAINERS.md +- Contributors list: (DevStats) [new contributors over the last 6 months](https://microcks.devstats.cncf.io/d/52/new-contributors-table?orgId=1&from=now-6M&to=now) / (GitHub) [contributors on Microcks main repo](https://github.com/microcks/microcks/graphs/contributors). +- Adopters (public) list: https://github.com/microcks/.github/blob/main/ADOPTERS.md +> 📢 If you're using Microcks in your organization, please add your company name to this [list](https://github.com/microcks/.github/blob/main/ADOPTERS.md) 🙏 It really helps the project to gain momentum and credibility. It's a small contribution back to the project with a significant impact. + +## Contributor ladder +To become a maintainer, you need to get involved with the Microcks project on GitHub and demonstrate commitment and qualities: + + * Participation: For three months or more. Examples include participation in discussions, contributions and code or documentation reviews. + * Collaboration: Demonstrate the ability to work with others, take on new ideas and help others succeed. + * Availability (ideally full-time): Be available on Slack, Discord, GitHub, and email so you can help move the project forward in a timely way. + * Respect: Alignment with Microcks and CNCF code of conduct and guiding principles. + +### Voting in and voting out maintainers + +1. Maintainers make a public announcement during community meetings, +2. During this meeting, a maintainer nominates to add a new maintainer or remove an existing maintainer, +3. The nominator will open a PR to the [centralized](https://github.com/microcks/.github/blob/main/MAINTAINERS.md) Maintainer and Code Owners list, +4. Maintainers vote via GitHub PR comments, with a 2 week deadline. Anyone in the community is welcome to comment. Community comments will be considered but not counted toward the vote, +5. After two weeks, any maintainer who abstains from voting will not be counted towards the vote, +6. Decision is approved with a super-majority: 66% 2/3) or more of maintainers who have voted within two weeks, +7. If 66% (2/3) of all maintainers have approved within two weeks, the voting is closed early. + +For maintainers voted in, permissions are immediately added. For maintainers, voted-out permissions are immediately removed. + +### Becoming a Code Owner +A Code Owner (alias Domain Maintainers) is appointed by the maintainers to recognize a contributor with expertise and authority in a specific domain. Code Owners are appointed to have elevated privileges, authority and specific responsibilities. The code owner role is part of the Microcks contributor ladder and is the primary path from contributor to maintainer. The roles and responsibilities of code owners are scoped. A person can have one or more code owner responsibilities. + +Code owners are enabled to act independently. They do not have responsibilities or voting rights over the entire project or organization. They are expected to participate with the community, but they are not expected to participate in maintainer meetings unless requested. + +### Remaining a Maintainer or Code Owner + +If a maintainer or code owner can no longer fulfill their commitments, they should consult with the maintainers and either take a sabbatical or step down from their role. All maintainers share the responsibility of ensuring the group operates with consistent dedication. If a maintainer or code owner fails to meet their commitments, they may be voted out by the maintainers and transitioned to emeritus status. + +## Adding or Removing Sub Projects +Microcks maintainers have the authority to add or remove sub-projects or repositories as needed. We follow a careful approach when making these changes: any new sub-project must serve a long-term purpose that is clearly distinct from existing ones, while sub-projects slated for removal must be shown to have either outlived their usefulness, become deprecated or unmaintainable. + +When a sub-project is removed, it will be archived as-is within the Microcks-archive organization, along with its associated repositories, ensuring transparency and historical reference. + +## Conflict Resolutions +Typically, it is assumed that disputes will be resolved amicably by those involved. However, if the situation becomes more serious, conflicts will be resolved through a voting process. A supermajority of votes from project maintainers is required to make a decision, and the project lead has the final say in the ruling. + +## Community Meetings +[Microcks](https://microcks.io/) hosts two monthly community meetings tailored for different time zones: + +- **APAC-friendly Meeting:** Second Thursday of each month + - Time: 9–10 a.m. CET / 1–2 p.m. Bengaluru +- **America-friendly Meeting:** Fourth Thursday of each month + - Time: 6–7 p.m. CET / 1–2 p.m. EST / 9–10 a.m. PST + +Here’s how to join and participate: https://github.com/microcks/community/blob/main/JOIN-OUR-MEETINGS.md + +The maintainers will also have closed meetings to discuss security reports or Code of Conduct violations. Any maintainer in charge should schedule such meetings upon receiving a security issue or CoC report. All current Maintainers must be invited to such closed meetings, except for any maintainer accused of a CoC violation. + +## Governance Changes +Changes to governance policy and any supporting documents must be agreed upon and approved by 66% (2/3) of the maintainers either by vote or by review and approval of a PR on the document. + +This Project Governance is a living document. As the Microcks community and project continue to evolve, maintainers are **committed** to improving and openly sharing our governance model, ensuring transparency and collaboration every step of the way. + +## Code of Conduct +Microcks follow the [Code of Conduct](CODE_OF_CONDUCT.md), which is aligned with the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). + +## Credits +Thanks to [Dawn Foster](https://github.com/geekygirldawn) for the inspiring talk and valuable insights at KubeCon Europe 2022: "Good Governance Practices for CNCF Projects": +[Info](https://contribute.cncf.io/resources/videos/2022/good-governance-practices/), [Recording](https://youtu.be/x0tgEpIER1M?si=0EMgdfA1j5kxpXlW) and [slide deck](https://static.sched.com/hosted_files/kccnceu2022/7c/Good_Governance_CNCF_Projects.pdf) 👀 + +Sections of this document have been borrowed and inspired from the [CoreDNS](https://github.com/coredns/coredns/blob/master/GOVERNANCE.md), [Kyverno](https://github.com/kyverno/kyverno/blob/main/GOVERNANCE.md), [OpenEBS](https://github.com/openebs/community/blob/72506ee3b885bd06324b82a650fcd3a61e93eef0/GOVERNANCE.md) and [fluxcd](https://github.com/fluxcd/community/blob/main/GOVERNANCE.md) projects. diff --git a/jdk_21_maven/cs/rest-gui/microcks/LICENSE b/jdk_21_maven/cs/rest-gui/microcks/LICENSE new file mode 100644 index 000000000..e06d20818 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/jdk_21_maven/cs/rest-gui/microcks/MAINTAINERS.md b/jdk_21_maven/cs/rest-gui/microcks/MAINTAINERS.md new file mode 100644 index 000000000..40fecf3a4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/MAINTAINERS.md @@ -0,0 +1,34 @@ +> [!Important] +> Microcks governance, roles and policies are defined in the [GOVERNANCE](https://github.com/microcks/.github/blob/main/GOVERNANCE.md) file. +> This MAINTAINERS file applies to every sub-project, repository and file existing within the [Microcks GitHub organization](https://github.com/microcks/). +> Please keep the lists sorted in ascending alphabetical order. + +## Overview + +This document provides an alphabetical list of Microcks' maintainers and code owners. If you want to contribute and become a maintainer or code owner, please refer to [CONTRIBUTING](CONTRIBUTING.md). + +## Maintainers + +The following members are Top-level [maintainers](https://github.com/microcks/.github/blob/main/GOVERNANCE.md#maintainers-code-owners-contributors-and-adopters) of the Microcks Parent Org, Parent Project, all repos, sub-repos, projects, sub-projects and forks contained within and under the entire Microcks parent org; with Full Binding Vote status. + +| Name | GitHub ID | Affiliation | +|----------------------------------------------------------|--------------------------------------------------------------|-------------------| +| Laurent Broudoux | [lbroudoux](https://github.com/lbroudoux) | Sponsored by Postman | +| Yacine Kheddache | [yada](https://github.com/yada) | Sponsored by Postman | + +## Code Owners + +The following members are [code owners](https://github.com/microcks/.github/blob/main/GOVERNANCE.md#maintainers-code-owners-contributors-and-adopters) with specialized expertise in specific domains within the Microcks project. The domain may be a sub-project, repo or other responsibility as defined by the Maintainers. + +| Name | GitHub ID | Affiliation | Sub-Projects | +|----------------------------------------------------------|-------------------------------------------------------------|-------------------|-------------------| +| Hugo Guerrero | [hguerrero](https://github.com/hguerrero) | Kong (Ex Red Hat) | [Docker Desktop Extension](https://github.com/microcks/microcks-docker-desktop-extension) | +| Julien Breux | [JulienBreux](https://github.com/JulienBreux) | Google | [Microcks CLI](https://github.com/microcks/microcks-cli), [Microcks Go Client](https://github.com/microcks/microcks-go-client) | +| Sebastien DEGODEZ | [SebastienDegodez](https://github.com/SebastienDegodez) | AXA France | [Testcontainers .NET](https://github.com/microcks/microcks-testcontainers-dotnet) | + +## Emeritus + +Below the list of Emeritus members are those who are no longer active but are recognized for their past contributions to the project. + +| Name | GitHub ID | Affiliation | Sub-Projects | +|----------------------------------------------------------|-------------------------------------------------------------|-------------------|-------------------| diff --git a/jdk_21_maven/cs/rest-gui/microcks/README.md b/jdk_21_maven/cs/rest-gui/microcks/README.md new file mode 100644 index 000000000..7d6e57d71 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/README.md @@ -0,0 +1,76 @@ + + +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/microcks/microcks/build-verify.yml?logo=github&style=for-the-badge)](https://github.com/microcks/microcks/actions) +[![Container](https://img.shields.io/badge/dynamic/json?color=blueviolet&logo=docker&style=for-the-badge&label=Quay.io&query=tags[1].name&url=https://quay.io/api/v1/repository/microcks/microcks/tag/?limit=10&page=1&onlyActiveTags=true)](https://quay.io/repository/microcks/microcks?tab=tags) +[![Version](https://img.shields.io/maven-central/v/io.github.microcks/microcks?color=blue&style=for-the-badge)]((https://search.maven.org/artifact/io.github.microcks/microcks)) +[![License](https://img.shields.io/github/license/microcks/microcks?style=for-the-badge&logo=apache)](https://www.apache.org/licenses/LICENSE-2.0) +[![Project Chat](https://img.shields.io/badge/discord-microcks-pink.svg?color=7289da&style=for-the-badge&logo=discord)](https://microcks.io/discord-invite/) +[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/microcks&style=for-the-badge)](https://artifacthub.io/packages/search?repo=microcks) +[![CNCF Landscape](https://img.shields.io/badge/CNCF%20Landscape-5699C6?style=for-the-badge&logo=cncf)](https://landscape.cncf.io/?item=app-definition-and-development--application-definition-image-build--microcks) + +# Microcks - Kubernetes native tool for API Mocking & Testing + +Microcks is a platform for turning your API and microservices assets - *OpenAPI specs*, *AsyncAPI specs*, *gRPC protobuf*, *GraphQL schema*, *Postman collections*, *SoapUI projects* - into live mocks in seconds. + +It also reuses these assets to run compliance and non-regression tests against your API implementation. We provide integrations with *Jenkins*, *GitHub Actions*, *Tekton* and many others through a simple CLI. + +[![LFX Health Score](https://img.shields.io/static/v1?label=Health%20Score&message=Healthy&color=A7F3D0&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/microcks/repository/microcks-microcks) [![LFX Contributors](https://img.shields.io/static/v1?label=Contributors&message=761&color=0094FF&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/microcks/repository/microcks-microcks) [![LFX Active Contributors](https://img.shields.io/static/v1?label=Active%20contributors%20(1Y)&message=208&color=0094FF&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/microcks/repository/microcks-microcks) + +## Getting Started + +* [Documentation](https://microcks.io/documentation/tutorials/getting-started/) +* [Microcks Community](https://github.com/microcks/community) and community meeting + +To get involved with our community, please familiarize yourself with the project's [Code of Conduct](./CODE_OF_CONDUCT.md). + +## Build Status + +The current development version is `1.12.2-SNAPSHOT` on branch `1.12.x`. + +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/microcks/microcks/build-verify.yml?branch=1.11.x&logo=github&style=for-the-badge)](https://github.com/microcks/microcks/actions) + +#### Sonarcloud Quality metrics + +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=microcks_microcks&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=microcks_microcks) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=microcks_microcks&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=microcks_microcks) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=microcks_microcks&metric=bugs)](https://sonarcloud.io/summary/new_code?id=microcks_microcks) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=microcks_microcks&metric=coverage)](https://sonarcloud.io/summary/new_code?id=microcks_microcks) +[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=microcks_microcks&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=microcks_microcks) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=microcks_microcks&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=microcks_microcks) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=microcks_microcks&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=microcks_microcks) + +#### Fossa license and security scans + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fmicrocks%2Fmicrocks.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fmicrocks%2Fmicrocks?ref=badge_shield&issueType=license) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fmicrocks%2Fmicrocks.svg?type=shield&issueType=security)](https://app.fossa.com/projects/git%2Bgithub.com%2Fmicrocks%2Fmicrocks?ref=badge_shield&issueType=security) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fmicrocks%2Fmicrocks.svg?type=small)](https://app.fossa.com/projects/git%2Bgithub.com%2Fmicrocks%2Fmicrocks?ref=badge_small) + +#### Signature, Provenance, SBOM + +[![Static Badge](https://img.shields.io/badge/supply_chain-documentation-blue?logo=securityscorecard&label=Supply%20Chain&link=https%3A%2F%2Fmicrocks.io%2Fdocumentation%2Freferences%2Fcontainer-images%23software-supply-chain-security)](https://microcks.io/documentation/references/container-images#software-supply-chain-security) + +#### OpenSSF best practices + +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/7513/badge)](https://bestpractices.coreinfrastructure.org/projects/7513) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/microcks/microcks/badge)](https://securityscorecards.dev/viewer/?uri=github.com/microcks/microcks) + + +## Versions + +Here are the naming conventions we're using for current releases, ongoing development maintenance activities. + +| Status | Version | Branch | Container images tags | +| ----------- |-------------------|----------|-----------------------| +| Stable | `1.12.1` | `master` | `1.12.1`, `latest` | +| Dev | `1.12.2-SNAPSHOT` | `1.12.x` | `nightly` | +| Maintenance | `1.11.3-SNAPSHOT` | `1.11.x` | `maintenance` | + + +## How to build Microcks + +The build instructions are available in the [building guide](BUILDING.md). + +## Thanks to the community! + +[![Stargazers repo roster for @microcks/microcks](http://reporoster.com/stars/microcks/microcks)](http://github.com/microcks/microcks/stargazers) +[![Forkers repo roster for @microcks/microcks](http://reporoster.com/forks/microcks/microcks)](http://github.com/microcks/microcks/network/members) diff --git a/jdk_21_maven/cs/rest-gui/microcks/ROADMAP.md b/jdk_21_maven/cs/rest-gui/microcks/ROADMAP.md new file mode 100644 index 000000000..85f6ab5d9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/ROADMAP.md @@ -0,0 +1,8 @@ +# Microcks Feature Planning and Tracking (roadmap) 🗓️ + +Microcks features are planned and tracked through the Project Tracker board on GitHub. + +👉 You can view the roadmap by [area](https://github.com/orgs/microcks/projects/1/views/1) or by [status](https://github.com/orgs/microcks/projects/1/views/2). + +The full release roadmaps of the main repo are managed via [release milestones](https://github.com/microcks/microcks/milestones?direction=asc&sort=due_date&state=open) on GitHub. + diff --git a/jdk_21_maven/cs/rest-gui/microcks/SECURITY-INSIGHTS.yml b/jdk_21_maven/cs/rest-gui/microcks/SECURITY-INSIGHTS.yml new file mode 100644 index 000000000..07414468a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/SECURITY-INSIGHTS.yml @@ -0,0 +1,56 @@ +header: + schema-version: 1.0.0 + last-updated: '2024-09-30' + last-reviewed: '2024-09-30' + expiration-date: '2025-09-30T01:00:00.000Z' + project-url: https://github.com/microcks/microcks + project-release: '1.10.1' + changelog: https://github.com/microcks/microcks/blob/main/CHANGELOG.md + license: https://github.com/microcks/microcks/blob/main/LICENSE +project-lifecycle: + status: active + roadmap: https://github.com/microcks/microcks/blob/main/ROADMAP.md + bug-fixes-only: false + core-maintainers: + - github:lbroudoux + - github:yada +contribution-policy: + accepts-pull-requests: true + accepts-automated-pull-requests: true + code-of-conduct: https://github.com/microcks/.github/blob/master/CODE_OF_CONDUCT.md + contributing-policy: https://github.com/microcks/.github/blob/master/CONTRIBUTING.md +documentation: + - https://microcks.io +distribution-points: + - https://microcks.io + - https://github.com/microcks/microcks + - https://quay.io/microcks +security-artifacts: + threat-model: + threat-model-created: false +security-testing: + - tool-type: sca + tool-name: Dependabot + tool-version: latest + integration: + ad-hoc: true + ci: false + before-release: false + comment: | + Dependabot is enabled for this repo on a weekly scheduled basis. +security-contacts: + - type: email + value: security@microcks.io +vulnerability-reporting: + accepts-vulnerability-reports: true + security-policy: https://github.com/microcks/microcks/security/policy + email-contact: security@microcks.io + comment: | + To report a security issue for one of the libraries owned by the Microcks community, write an email with a detailed description of the issue to security@microcks.io. +dependencies: + third-party-packages: true + dependencies-lists: + - https://github.com/microcks/microcks/network/dependencies + - https://app.fossa.com/projects/git%2Bgithub.com%2Fmicrocks%2Fmicrocks/refs/branch/master/4eca8d6eccc7456c079e1e0e374d7ff783e32ce2/browse/dependencies + env-dependencies-policy: + policy-url: https://github.com/microcks/microcks/blob/main/DEPENDENCY_POLICY.md \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/SECURITY.md b/jdk_21_maven/cs/rest-gui/microcks/SECURITY.md new file mode 100644 index 000000000..b4f8b9dd7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Reporting a Vulnerability + +If you've found a vulnerability in our components or website or want additional information regarding how we manage security, please report it via a [GitHub discussion](https://github.com/microcks/microcks/discussions). + +If you do not want to publicly report a security issue for one of the libraries owned by the Microcks community, write an email with a detailed description of the issue to security@microcks.io. + +## Public Disclosure Timing + +We prefer to fully disclose the bug as soon as possible once a user mitigation is available. The Fix Lead drives the schedule using their best judgment based on severity, development time, and release manager feedback. If the Fix Lead deals with public disclosure, all timelines will be set as soon as possible (ASAP). + +## Supported Versions + +Microcks releases follow the [semver](https://semver.org/) specification. Security fixes are typically merged into the current development branch and are due for release in the next minor version. We may create a fix release upon request or, if deemed necessary, as part of a critical security fix. + +## Security Team + +The security team is made up of a subset of the project [maintainers](https://github.com/microcks/.github/blob/main/GOVERNANCE.md#maintainers-code-owners-contributors-and-adopters) and [code owners](https://github.com/microcks/.github/blob/main/GOVERNANCE.md#maintainers-code-owners-contributors-and-adopters) who are willing and able to respond to vulnerability reports. + +## Credits + +Sections of this document have been borrowed and inspired from the [OpenEBS](https://github.com/openebs/community/blob/72506ee3b885bd06324b82a650fcd3a61e93eef0/SECURITY.md) project. diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/APIExamples-v1alpha1-schema.json b/jdk_21_maven/cs/rest-gui/microcks/api/APIExamples-v1alpha1-schema.json new file mode 100644 index 000000000..4f795668d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/APIExamples-v1alpha1-schema.json @@ -0,0 +1,228 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://microcks.io/schemas/APIExamples-v1alpha1-schema.json", + "title": "The APIExamples JSON schema", + "description": "APIExamples is a specification for defining examples of API.", + "required": [ + "apiVersion", + "kind", + "metadata", + "operations" + ], + "properties": { + "apiVersion": { + "$id": "#/properties/apiVersion", + "type": "string", + "title": "The apiVersion of APIExamples", + "description": "The version of APIExamples description.", + "const": "mocks.microcks.io/v1alpha1" + }, + "kind": { + "$id": "#/properties/kind", + "type": "string", + "title": "The APIExamples kind schema", + "description": "Kind marker for APIExamples", + "const": "APIExamples" + }, + "metadata": { + "$id": "#/properties/metadata", + "type": "object", + "title": "The metadata of APIExamples", + "description": "Holds reference information about this Service/API examples relate.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "$id": "#/properties/metadata/properties/name", + "type": "string", + "title": "The name of API these examples relate to.", + "description": "Human readable name of the target API for examples." + }, + "version": { + "$id": "#/properties/metadata/properties/version", + "type": "string", + "title": "The version of API these examples relate to.", + "description": "Human readable version of the target API for examples." + } + } + }, + "operations": { + "$id": "#/properties/operations", + "type": "object", + "title": "The API operations", + "description": "The examples are organized using API operations.", + "patternProperties": { + "^.": { + "$ref": "#/definitions/operationItem" + } + } + } + }, + "definitions": { + "operationItem": { + "$id": "#/definitions/operationItem", + "type": "object", + "title": "One API operation examples", + "description": "Examples of an API operation.", + "patternProperties": { + "^.": { + "$ref": "#/definitions/exampleItem" + } + } + }, + "exampleItem": { + "$id": "#/definitions/operationItem", + "title": "One API example item", + "description": "One example item of an API operation.", + "oneOf": [ + { + "$ref": "#/definitions/requestResponsePair" + }, + { + "$ref": "#/definitions/unidirectionalMessage" + } + ] + }, + "requestResponsePair": { + "$id": "#/definitions/requestResponsePair", + "type": "object", + "title": "Request/Response example", + "description": "A request/response pair example for an API operation.", + "required": [ + "request", + "response" + ], + "properties": { + "request": { + "$ref": "#/definitions/request" + }, + "response": { + "$ref": "#/definitions/response" + } + }, + "additionalProperties": false + }, + "unidirectionalMessage": { + "$id": "#/definitions/unidirectionalMessage", + "type": "object", + "title": "Unidirectional message example", + "description": "A unidirectional message example for an API operation.", + "required": [ + "eventMessage" + ], + "properties": { + "eventMessage": { + "$ref": "#/definitions/eventMessage" + } + }, + "additionalProperties": false + }, + "request": { + "$id": "#/definitions/request", + "type": "object", + "title": "The request of a request/response pair", + "description": "The request part of a request/response pair.", + "properties": { + "parameters": { + "type": "object", + "title": "The request parameters", + "description": "The parameters (query or path located) of the request.", + "additionalProperties": true + }, + "headers": { + "type": "object", + "title": "The request headers", + "description": "The headers of the request.", + "additionalProperties": true + }, + "body": { + "title": "The request body", + "description": "The body of the request.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "additionalProperties": false + }, + "response": { + "$id": "#/definitions/response", + "type": "object", + "title": "The response of a request/response pair", + "description": "The response part of a request/response pair.", + "properties": { + "headers": { + "type": "object", + "title": "The response headers", + "additionalProperties": true + }, + "mediaType": { + "type": "string", + "title": "The response media type", + "description": "The media type of the response body (like application/json)." + }, + "status": { + "type": "string", + "title": "The response status", + "description": "The status of the response (typically the Http code)." + }, + "body": { + "title": "The response body", + "description": "The body of the response.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "additionalProperties": false + }, + "eventMessage": { + "$id": "#/definitions/eventMessage", + "type": "object", + "title": "The event message", + "description": "The event message of a unidirectional message example.", + "properties": { + "headers": { + "type": "object", + "title": "The event message headers", + "description": "The headers of the event message.", + "additionalProperties": true + }, + "payload": { + "title": "The event message payload", + "description": "The payload of the event message.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/APIMetadata-v1alpha1-schema.json b/jdk_21_maven/cs/rest-gui/microcks/api/APIMetadata-v1alpha1-schema.json new file mode 100644 index 000000000..3b2edca2f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/APIMetadata-v1alpha1-schema.json @@ -0,0 +1,158 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://microcks.io/schemas/APIMetadata-v1alpha1-schema.json", + "title": "The APIMetadata JSON schema", + "description": "APIMetadata is a specification for customizing metadata of API.", + "required": [ + "apiVersion", + "kind", + "metadata", + "operations" + ], + "properties": { + "apiVersion": { + "$id": "#/properties/apiVersion", + "type": "string", + "title": "The apiVersion of APIExamples", + "description": "The version of APIExamples description.", + "const": "mocks.microcks.io/v1alpha1" + }, + "kind": { + "$id": "#/properties/kind", + "type": "string", + "title": "The APIMetadata kind schema", + "description": "Kind marker for APIMetadata", + "const": "APIMetadata" + }, + "metadata": { + "$id": "#/properties/metadata", + "type": "object", + "title": "The metadata of APIMetadata", + "description": "Holds reference information about this Service/API metadata relate.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "$id": "#/properties/metadata/properties/name", + "type": "string", + "title": "The name of API these metadata relate to.", + "description": "Human readable name of the target API for metadata." + }, + "version": { + "$id": "#/properties/metadata/properties/version", + "type": "string", + "title": "The version of API these metadata relate to.", + "description": "Human readable version of the target API for examples." + }, + "labels": { + "$id": "#/properties/metadata/properties/labels", + "type": "object", + "title": "The metadata labels for API", + "description": "The metadata labels to apply on the API.", + "additionalProperties": { + "type": "string" + } + } + } + }, + "operations": { + "$id": "#/properties/operations", + "type": "object", + "title": "The API operations", + "description": "The metadata are organized using API operations.", + "patternProperties": { + "^.": { + "$ref": "#/definitions/operationItem" + } + } + } + }, + "definitions": { + "operationItem": { + "$id": "#/definitions/operationItem", + "type": "object", + "title": "One API operation metadata", + "description": "Metadata for an API operation.", + "properties": { + "delay": { + "$id": "#/definitions/operationItem/properties/delay", + "type": "integer", + "title": "The delay of the operation", + "description": "The response delay of the operation in milliseconds." + }, + "frequency": { + "$id": "#/definitions/operationItem/properties/frequency", + "type": "integer", + "title": "The frequency of the operation", + "description": "The publication frequency of the operation in seconds." + }, + "dispatcher": { + "$id": "#/definitions/operationItem/properties/dispatcher", + "type": "string", + "title": "The dispatcher of the operation", + "description": "The dispatcher strategy to use for the operation." + }, + "dispatcherRules": { + "$id": "#/definitions/operationItem/properties/dispatcherRules", + "type": "string", + "title": "The dispatcherRules of the operation", + "description": "The dispatcher rules to use to configure the dispatcher." + }, + "parameterConstraints": { + "$id": "#/definitions/operationItem/properties/parameterConstraints", + "type": "array", + "title": "The parameterConstraints of the operation", + "description": "The constraints to apply on operation parameters.", + "items": { + "$ref": "#/definitions/parameterConstraint" + } + } + } + }, + "parameterConstraint": { + "$id": "#/definitions/parameterConstraint", + "type": "object", + "title": "A parameter constraint", + "description": "A constraint to apply on an operation parameter.", + "properties": { + "name": { + "$id": "#/definitions/parameterConstraint/properties/name", + "type": "string", + "title": "The name of the parameter", + "description": "The name of the parameter constraint applies to." + }, + "in": { + "$id": "#/definitions/parameterConstraint/properties/in", + "type": "string", + "enum": [ + "path", + "query", + "header" + ], + "title": "The location of the parameter", + "description": "Whether the parameter is in path, query, header or body." + }, + "required": { + "$id": "#/definitions/parameterConstraint/properties/required", + "type": "boolean", + "title": "Is this parameter required?", + "description": "Whether the parameter is required or not." + }, + "recopy": { + "$id": "#/definitions/parameterConstraint/properties/recopy", + "type": "boolean", + "title": "Should this parameter be recopied?", + "description": "Whether the parameter should be recopied in response." + }, + "mustMatchRegexp": { + "$id": "#/definitions/parameterConstraint/properties/mustMatchRegexp", + "type": "string", + "title": "A regular expression to match", + "description": "A regular expression parameter value must match." + } + } + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-asyncapi-v1.10.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-asyncapi-v1.10.yaml new file mode 100644 index 000000000..a2da89ecc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-asyncapi-v1.10.yaml @@ -0,0 +1,417 @@ +asyncapi: 3.0.0 +info: + title: Microcks Events API v1.10 + version: 1.10.1 + description: "Events API offered by Microcks, the Kubernetes native tool for API and microservices\ + \ mocking and testing (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/microcks + email: laurent@microcks.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + x-logo: + backgroundColor: '#ffffff' + url: https://microcks.io/images/microcks-logo-blue.png +defaultContentType: application/json +channels: + service-changes: + description: A channel where Services changes are published + messages: + serviceChangeEvent: + $ref: '#/components/messages/serviceChangeEvent' + bindings: + kafka: + key: + type: string + bindings: + ws: + method: POST + kafka: + topic: 'microcks-services-updates' +operations: + receivedServiceChanges: + action: receive + channel: + $ref: '#/channels/service-changes' + summary: Receive information about Service changes + messages: + - $ref: '#/channels/service-changes/messages/serviceChangeEvent' +components: + messages: + serviceChangeEvent: + description: An event describing that Service has been modified (created, updated or deleted) + payload: + $ref: '#/components/schemas/ServiceChangeEvent' + schemas: + ServiceChangeEvent: + type: object + required: + - serviceId + - serviceView + - changeType + - timestamp + properties: + serviceId: + type: string + serviceView: + type: object + schema: + $ref: '#/components/schemas/ServiceView' + changeType: + type: string + enum: + - CREATED + - UPDATED + - DELETED + timestamp: + type: number + format: int32 + additionalProperties: true + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + type: object + required: + - service + - messagesMap + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + type: object + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + additionalProperties: + $ref: '#/components/schemas/MessageArray' + additionalProperties: true + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + - sourceArtifact + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + type: string + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + sourceArtifact: + description: Short name of the main/primary artifact this service was created + from + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + type: object + required: + - createdOn + - lastUpdate + properties: + createdOn: + description: Creation date of attached object + type: number + readOnly: true + lastUpdate: + description: Last update of attached object + type: number + readOnly: true + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + Operation: + description: An Operation of a Service or API + type: object + required: + - name + - method + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + type: object + required: + - name + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + type: string + enum: + - path + - query + - header + Binding: + description: Protocol binding details for asynchronous operations + type: object + required: + - type + - destinationName + properties: + type: + description: Protocol binding identifier + type: string + enum: + - KAFKA + - MQTT + - WS + - AMQP + - NATS + - GOOGLEPUBSUB + - SQS + - SNS + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: type + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + type: object + required: + - type + properties: + type: + description: Discriminant type for identifying kind of exchange + type: string + enum: + - reqRespPair + - unidirEvent + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + required: + - request + - response + - $ref: '#/components/schemas/AbstractExchange' + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - type: object + required: + - eventMessage + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + EventMessage: + description: A mock event message + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.0.yaml new file mode 100644 index 000000000..99d252c65 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.0.yaml @@ -0,0 +1,1463 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.0 + version: 1.0.0 + description: API offered by Microcks, the mock and testing platform for API and + microservices (microcks.github.io) + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - user:identity + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - user + operationId: ActivateImportJob + summary: Activate an ImportJob + description: Make an ImportJob active, so that it is executed + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - user + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - user + operationId: StopImportJob + summary: Stop an ImportJob + description: Stopping an ImportJob desactivate it, so that it won't execute + at next schedule + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: Deals with artifacts (SOAP UI project, Postman collection or OpenAPI + Specification) to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact (SOAP UI project, Postman collection or OpenAPI + Specification) to be imported by Microcks. + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: 'Already used labels: keys are label Keys, values are array + of label Values' + security: + - jwt-bearer: + - admin + operationId: GetServicesLabels + summary: Get the already used labels for Services + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + type: array + items: + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + type: string + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - EVENT + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Wether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Wether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + ArtifactUpload: + title: Root Type for ArtifactUpload + description: |- + Artifact (SOAP UI project, Postman collection or OpenAPI Specification) to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + title: Root Type for Counter + description: The root of the Counter type's schema. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + - request + - response + properties: + code: + description: Return code for test (0 means Success, 1 means Failure) + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + - requestName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: string + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + TestResult: + description: Represents the result of a Service or API test run by Microcks. + Tests are related to a service and made of multiple test cases corresponding + to each operations / actions composing service. Tests are run against a specific + endpoint named testedEndpoint. It holds global markers telling if test still + ran, is a success, how many times is has taken and so on ... + required: + - id + - version + - testNumber + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispactherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: Map of messages for this Service. Keys are operation name, + values are array of messages for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + scopes: + user: "" + manager: "" + admin: "" + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.1.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.1.yaml new file mode 100644 index 000000000..7093d5417 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.1.yaml @@ -0,0 +1,1526 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.1 + version: 1.1.0 + description: API offered by Microcks, the mock and testing platform for API and + microservices (microcks.github.io) + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - user:identity + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - user + operationId: ActivateImportJob + summary: Activate an ImportJob + description: Make an ImportJob active, so that it is executed + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - user + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - user + operationId: StopImportJob + summary: Stop an ImportJob + description: Stopping an ImportJob desactivate it, so that it won't execute + at next schedule + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: Deals with artifacts (SOAP UI project, Postman collection or OpenAPI + Specification) to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact (SOAP UI project, Postman collection or OpenAPI + Specification) to be imported by Microcks. + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: 'Already used labels: keys are label Keys, values are array + of label Values' + security: + - jwt-bearer: + - admin + operationId: GetServicesLabels + summary: Get the already used labels for Services + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - EVENT + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Wether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Wether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + ArtifactUpload: + title: Root Type for ArtifactUpload + description: |- + Artifact (SOAP UI project, Postman collection or OpenAPI Specification) to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + title: Root Type for Counter + description: The root of the Counter type's schema. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: Return code for test (0 means Success, 1 means Failure) + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: string + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispactherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: Map of messages for this Service. Keys are operation name, + values are array of messages for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + type: array + items: + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + type: string + OperationHeaders: + description: Specification of additional headers for a Service/API operations. + Keys are operation name or "globals" (if header applies to all), values are + Header objects. + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Header' + TestResult: + description: Represents the result of a Service or API test run by Microcks. + Tests are related to a service and made of multiple test cases corresponding + to each operations / actions composing service. Tests are run against a specific + endpoint named testedEndpoint. It holds global markers telling if test still + ran, is a success, how many times is has taken and so on ... + required: + - id + - version + - testNumber + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + scopes: + user: "" + manager: "" + admin: "" + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.10.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.10.yaml new file mode 100644 index 000000000..b38f40ad3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.10.yaml @@ -0,0 +1,2296 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.10 + version: 1.10.1 + description: "API offered by Microcks, the Kubernetes native tool for API and microservices\ + \ mocking and testing (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/microcks + email: laurent@microcks.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + x-logo: + backgroundColor: '#ffffff' + url: https://microcks.io/images/microcks-logo-blue.png +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + operationId: GetImportJob + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + operationId: UpdateImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: Deals with artifacts to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + parameters: + - name: mainArtifact + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + schema: + type: boolean + in: query + required: true + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - manager + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact to be imported by Microcks. + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + requestBody: + description: The Secret to update + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + required: true + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true + /features/config: + summary: Optional features configuration + get: + tags: + - config + responses: + "200": + content: + application/json: {} + description: Get features configuration + operationId: GetFeaturesConfig + summary: Get features configuration + /import: + summary: Deals with repository snapshot to import in Microcks + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SnapshotUpload' + required: true + tags: + - mock + responses: + "201": + description: Snasphot has been correctly imported + security: + - jwt-bearer: + - admin + operationId: importSnapshot + summary: Import a snapshot + description: Import a repository snapshot previsouly exported into Microcks + /export: + summary: Deals with repository snapshot to import from Microcks + get: + tags: + - mock + parameters: + - name: serviceIds + description: List of service identifiers to export + schema: + type: array + items: + type: string + in: query + required: true + responses: + "200": + headers: + Content-Disposition: + schema: + type: string + examples: + filename: + value: attachment; filename=microcks-repository.json + content: + application/json: + schema: + format: binary + type: string + description: Snapshot file representing the export of requested services + security: + - jwt-bearer: + - admin + operationId: exportSnapshot + summary: Export a snapshot + description: Export a repostiory snapshot with requested services + /metrics/invocations/global: + summary: Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Aggregated invocation statistics for specified day + operationId: GetAggregatedInvocationsStats + summary: Get aggregated invocation statistics for a day + /metrics/conformance/aggregate: + summary: Aggregation of Test conformance metrics + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WeightedMetricValue' + description: Get aggregated coverage metric value + operationId: GetConformanceMetricsAggregation + summary: Get aggregation of conformance metrics + /metrics/conformance/service/{serviceId}: + summary: Test Conformance metrics on API and Services + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestConformanceMetric' + description: Test coverage metric for Service + operationId: GetServiceTestConformanceMetric + summary: Get conformance metrics for a Service + parameters: + - name: serviceId + description: Unique Services identifier this metrics are related to + schema: + type: string + in: path + required: true + /metrics/invocations/top: + summary: Top Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + - name: limit + description: The number of top invoked mocks to return + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Top invocations for a defined day + operationId: GetTopIvnocationsStatsByDay + summary: Get top invocation statistics for a day + /metrics/invocations/{serviceName}/{serviceVersion}: + summary: Invocation Statistics for API and Services + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Invocation statistics for service for specified day + operationId: GetInvocationStatsByService + summary: Get invocation statistics for Service + parameters: + - name: serviceName + description: Name of service to get statistics for + schema: + type: string + in: path + required: true + - name: serviceVersion + description: Version of service to get statistics for + schema: + type: string + in: path + required: true + /metrics/invocations/global/latest: + summary: Latest Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to get back in time. Default is 20. + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/CounterMap' + description: A map where keys are day (formatted using yyyyMMdd pattern) + and values are counter of invocations on this day + operationId: GetLatestAggregatedInvocationsStats + summary: Get aggregated invocations statistics for latest days + /metrics/tests/latest: + summary: Lastest Test results across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to consider for test results to return. Default + is 7 (one week) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResultSummary' + description: Test results summary for specified last days. + operationId: GetLatestTestResults + summary: Get latest tests results + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - manager + - admin + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - user + operationId: GetServicesLabels + summary: Get the already used labels for Services + /artifact/download: + summary: Deals with artifacts to be downloaded and imported by Microcks. + post: + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ArtifactDownload' + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No url attribute found in download data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - manager + operationId: downloadArtifact + summary: Download an artifact + description: Ask Microcks to download an artifact and import it + /secrets/search: + get: + tags: + - config + parameters: + - name: name + description: Search using this name-like criterion + schema: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets (filtered according search criteria) + security: + - jwt-bearer: + - admin + operationId: SearchSecrets + summary: Search for Secrets +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + - enabled + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: boolean + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + enabled: + description: Whether Keycloak authentification and usage is enabled + type: boolean + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: number + readOnly: true + lastUpdate: + description: Last update of attached object + type: number + readOnly: true + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + oAuth2Context: + $ref: '#/components/schemas/OAuth2ClientContent' + description: An OAuth2 context to use for retrieving an access token prior + invoking the tested endpoint + operationsHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relative path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + - GRAPHQL_SCHEMA + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ArtifactUpload: + description: |- + Artifact to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + description: A simple Counter type. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + FeaturesConfig: + title: Root Type for FeaturesConfig + description: Representation of optional features configuration used by Microcks + server + type: object + properties: + repository-filter: + description: Repository filtering feature properties + type: object + properties: + label-label: + type: string + enabled: + type: string + label-list: + type: string + label-key: + type: string + microcks-hub: + description: Microcks Hub feature properties + type: object + properties: + allowed-roles: + type: string + endpoint: + type: string + enabled: + type: string + repository-tenancy: + description: Repository tenancy feature properties + type: object + properties: + enabled: + type: string + artifact-import-allowed-roles: + type: string + async-api: + description: Asynchronous feature properties + type: object + properties: + default-binding: + type: string + endpoint-MQTT: + type: string + endpoint-WS: + type: string + endpoint-KAFKA: + type: string + endpoint-AMQP: + type: string + endpoint-NATS: + type: string + endpoint-GOOGLEPUBSUB: + type: string + enabled: + type: string + frequencies: + type: string + additionalProperties: true + example: + repository-filter: + label-label: Domain + enabled: "true" + label-list: "domain,status" + label-key: domain + microcks-hub: + allowed-roles: "admin,manager,manager-any" + endpoint: https://hub.microcks.io/api + enabled: "true" + repository-tenancy: + enabled: "false" + artifact-import-allowed-roles: "admin,manager,manager-any" + async-api: + default-binding: KAFKA + endpoint-MQTT: my-mqtt-broker.apps.try.microcks.io + endpoint-WS: localhost:8081 + endpoint-KAFKA: my-cluster-kafka-bootstrap.apps.try.microcks.io + enabled: "false" + frequencies: "3,10,30" + SnapshotUpload: + description: Upload of a repository snapshot file + required: + - file + type: object + properties: + file: + format: binary + description: The repository snapshot file + type: string + HeaderDTO: + description: Data Transfert Object for headers of both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this header (comma separated strings) + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects DTO." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/HeaderDTO' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + - sourceArtifact + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + sourceArtifact: + description: Short name of the main/primary artifact this service was created + from + type: string + Trend: + description: Evolution trend qualifier + enum: + - DOWN + - LOW_DOWN + - STABLE + - LOW_UP + - UP + type: string + WeightedMetricValue: + description: Value of a metric with an associated weight + required: + - name + - weight + - value + type: object + properties: + name: + description: Metric name or serie name + type: string + weight: + description: Weight of this metric value (typically a percentage) + type: integer + value: + description: The value of this metric + type: integer + DailyInvocationStatistic: + description: The daily statistic of a service mock invocations + required: + - id + - day + - serviceName + - serviceVersion + - dailyCount + type: object + properties: + id: + description: Unique identifier of this statistic object + type: string + day: + description: The day (formatted as yyyyMMdd string) represented by this + statistic + type: string + serviceName: + description: The name of the service this statistic is related to + type: string + serviceVersion: + description: The version of the service this statistic is related to + type: string + dailyCount: + description: The number of service mock invocations on this day + type: number + hourlyCount: + description: The number of service mock invocations per hour of the day + (keys range from 0 to 23) + type: object + additionalProperties: true + minuteCount: + description: The number of service mock invocations per minute of the day + (keys range from 0 to 1439) + type: object + additionalProperties: true + TestConformanceMetric: + description: "Represents the test conformance metrics (current score, history\ + \ and evolution trend) of a Service" + required: + - id + - serviceId + - currentScore + - maxPossibleScore + type: object + properties: + id: + description: Unique identifier of coverage metric + type: string + serviceId: + description: Unique identifier of the Service this metric is related to + type: string + aggregationLabelValue: + description: Value of the label used for metrics aggregation (if any) + type: string + maxPossibleScore: + format: double + description: Maximum conformance score that can be reached (depends on samples + expresiveness) + type: number + currentScore: + format: double + description: Current test conformance score for the related Service + type: number + lastUpdateDay: + description: The day of latest score update (in yyyyMMdd format) + type: string + latestTrend: + $ref: '#/components/schemas/Trend' + description: Evolution trend of currentScore + latestScores: + description: "History of latest scores (key is date with format yyyyMMdd,\ + \ value is score as double)" + type: object + additionalProperties: + type: number + TestResultSummary: + description: 'Represents the summary result of a Service or API test run by + Microcks. ' + required: + - id + - testDate + - serviceId + - success + properties: + id: + description: Unique identifier of TestResult + type: string + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + serviceId: + description: Unique identifier of service tested + type: string + success: + description: Flag telling if test is a success + type: boolean + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testDate + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + authorizedClient: + $ref: '#/components/schemas/OAuth2AuthorizedClient' + description: The OAuth2 authorized client that performed the test + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + - AMQP + - NATS + - GOOGLEPUBSUB + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + ResourceType: + description: Types of managed resources for Services or APIs + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + - GRAPHQL_SCHEMA + - POSTMAN_COLLECTION + type: string + CounterMap: + description: A generic map of counter + type: object + additionalProperties: + type: number + OAuth2ClientContent: + description: Represents a volatile OAuth2 client context usually associated + with a Test request + required: + - clientId + - clientSecret + - tokenUri + type: object + properties: + clientId: + description: Id for connecting to OAuth2 identity provider + type: string + clientSecret: + format: password + description: Secret for connecting to OAuth2 identity provider + type: string + tokenUri: + description: URI for retrieving an access token from OAuth2 identity provider + type: string + username: + description: Username in case you're using the Resource Owner Password flow + type: string + password: + description: User password in case you're suing the Resource Owner password + flow + type: string + refreshToken: + description: Refresh token in case you're using the Refresh Token rotation + flow + type: string + OAuth2GrantType: + description: Enumeration for the different supported grants/flows of OAuth2 + enum: + - PASSWORD + - CLIENT_CREDENTIALS + - REFRESH_TOKEN + type: string + OAuth2AuthorizedClient: + description: OAuth2 authorized client that performed a test + required: + - grantType + - principalName + - tokenUri + type: object + properties: + grantType: + $ref: '#/components/schemas/OAuth2GrantType' + description: OAuth2 authorization flow/grant type applied + principalName: + description: Name of authorized principal (clientId or username in the case + of Password grant type) + type: string + tokenUri: + description: Identity Provider URI used for token retrieval + type: string + scopes: + description: Included scopes (separated using space) + type: string + ArtifactDownload: + description: Artifact Download specification to be imported by Microcks. + required: + - url + type: object + properties: + url: + description: The URL of remote artifact to download and import + type: string + mainArtifact: + description: Whether this remote artifact should be imported as main/primary + or secondary artifact. Default is true. + type: boolean + secretName: + description: The name of a secret that can be used to authenticated when + downloading remote artifact. + type: string + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: int64 + description: Creation timestamp for this ImportJob + type: integer + lastImportDate: + format: int64 + description: Timestamp of the last import + type: integer + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + scopes: + user: Simple authenticated user + manager: Services & APIs content manager + admin: Administrator of the Microcks instance + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration +- name: metrics + description: Operations related to metrics diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.11.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.11.yaml new file mode 100644 index 000000000..bdc376c2b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.11.yaml @@ -0,0 +1,2301 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.11 + version: 1.11.0 + description: "API offered by Microcks, the Kubernetes native tool for API and microservices\ + \ mocking and testing (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/microcks + email: laurent@microcks.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + x-logo: + backgroundColor: '#ffffff' + url: https://microcks.io/images/microcks-logo-blue.png +servers: + - url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + operationId: GetImportJob + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + operationId: UpdateImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + requestBody: + description: The Secret to update + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + required: true + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true + /features/config: + summary: Optional features configuration + get: + tags: + - config + responses: + "200": + content: + application/json: {} + description: Get features configuration + operationId: GetFeaturesConfig + summary: Get features configuration + /import: + summary: Deals with repository snapshot to import in Microcks + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SnapshotUpload' + required: true + tags: + - mock + responses: + "201": + description: Snasphot has been correctly imported + security: + - jwt-bearer: + - admin + operationId: importSnapshot + summary: Import a snapshot + description: Import a repository snapshot previsouly exported into Microcks + /export: + summary: Deals with repository snapshot to import from Microcks + get: + tags: + - mock + parameters: + - name: serviceIds + description: List of service identifiers to export + schema: + type: array + items: + type: string + in: query + required: true + responses: + "200": + headers: + Content-Disposition: + schema: + type: string + examples: + filename: + value: attachment; filename=microcks-repository.json + content: + application/json: + schema: + format: binary + type: string + description: Snapshot file representing the export of requested services + security: + - jwt-bearer: + - admin + operationId: exportSnapshot + summary: Export a snapshot + description: Export a repostiory snapshot with requested services + /metrics/invocations/global: + summary: Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Aggregated invocation statistics for specified day + operationId: GetAggregatedInvocationsStats + summary: Get aggregated invocation statistics for a day + /metrics/conformance/aggregate: + summary: Aggregation of Test conformance metrics + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WeightedMetricValue' + description: Get aggregated coverage metric value + operationId: GetConformanceMetricsAggregation + summary: Get aggregation of conformance metrics + /metrics/conformance/service/{serviceId}: + summary: Test Conformance metrics on API and Services + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestConformanceMetric' + description: Test coverage metric for Service + operationId: GetServiceTestConformanceMetric + summary: Get conformance metrics for a Service + parameters: + - name: serviceId + description: Unique Services identifier this metrics are related to + schema: + type: string + in: path + required: true + /metrics/invocations/top: + summary: Top Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + - name: limit + description: The number of top invoked mocks to return + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Top invocations for a defined day + operationId: GetTopIvnocationsStatsByDay + summary: Get top invocation statistics for a day + /metrics/invocations/{serviceName}/{serviceVersion}: + summary: Invocation Statistics for API and Services + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Invocation statistics for service for specified day + operationId: GetInvocationStatsByService + summary: Get invocation statistics for Service + parameters: + - name: serviceName + description: Name of service to get statistics for + schema: + type: string + in: path + required: true + - name: serviceVersion + description: Version of service to get statistics for + schema: + type: string + in: path + required: true + /metrics/invocations/global/latest: + summary: Latest Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to get back in time. Default is 20. + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/CounterMap' + description: A map where keys are day (formatted using yyyyMMdd pattern) + and values are counter of invocations on this day + operationId: GetLatestAggregatedInvocationsStats + summary: Get aggregated invocations statistics for latest days + /metrics/tests/latest: + summary: Lastest Test results across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to consider for test results to return. Default + is 7 (one week) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResultSummary' + description: Test results summary for specified last days. + operationId: GetLatestTestResults + summary: Get latest tests results + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - manager + - admin + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - user + operationId: GetServicesLabels + summary: Get the already used labels for Services + /artifact/download: + summary: Deals with artifacts to be downloaded and imported by Microcks. + post: + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ArtifactDownload' + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No url attribute found in download data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - manager + operationId: downloadArtifact + summary: Download an artifact + description: Ask Microcks to download an artifact and import it + /secrets/search: + get: + tags: + - config + parameters: + - name: name + description: Search using this name-like criterion + schema: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets (filtered according search criteria) + security: + - jwt-bearer: + - admin + operationId: SearchSecrets + summary: Search for Secrets + /artifact/upload: + summary: Deals with artifacts to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + parameters: + - name: mainArtifact + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + schema: + type: boolean + in: query + required: true + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - manager + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact to be imported by Microcks. +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + - enabled + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: boolean + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + enabled: + description: Whether Keycloak authentification and usage is enabled + type: boolean + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: number + readOnly: true + lastUpdate: + description: Last update of attached object + type: number + readOnly: true + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Exchanges for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + oAuth2Context: + $ref: '#/components/schemas/OAuth2ClientContent' + description: An OAuth2 context to use for retrieving an access token prior + invoking the tested endpoint + operationsHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relative path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + - GRAPHQL_SCHEMA + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ArtifactUpload: + description: |- + Artifact to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + description: A simple Counter type. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + FeaturesConfig: + title: Root Type for FeaturesConfig + description: Representation of optional features configuration used by Microcks + server + type: object + properties: + repository-filter: + description: Repository filtering feature properties + type: object + properties: + label-label: + type: string + enabled: + type: string + label-list: + type: string + label-key: + type: string + microcks-hub: + description: Microcks Hub feature properties + type: object + properties: + allowed-roles: + type: string + endpoint: + type: string + enabled: + type: string + repository-tenancy: + description: Repository tenancy feature properties + type: object + properties: + enabled: + type: string + artifact-import-allowed-roles: + type: string + async-api: + description: Asynchronous feature properties + type: object + properties: + default-binding: + type: string + endpoint-MQTT: + type: string + endpoint-WS: + type: string + endpoint-KAFKA: + type: string + endpoint-AMQP: + type: string + endpoint-NATS: + type: string + endpoint-GOOGLEPUBSUB: + type: string + enabled: + type: string + frequencies: + type: string + additionalProperties: true + example: + repository-filter: + label-label: Domain + enabled: "true" + label-list: "domain,status" + label-key: domain + microcks-hub: + allowed-roles: "admin,manager,manager-any" + endpoint: https://hub.microcks.io/api + enabled: "true" + repository-tenancy: + enabled: "false" + artifact-import-allowed-roles: "admin,manager,manager-any" + async-api: + default-binding: KAFKA + endpoint-MQTT: my-mqtt-broker.apps.try.microcks.io + endpoint-WS: localhost:8081 + endpoint-KAFKA: my-cluster-kafka-bootstrap.apps.try.microcks.io + enabled: "false" + frequencies: "3,10,30" + SnapshotUpload: + description: Upload of a repository snapshot file + required: + - file + type: object + properties: + file: + format: binary + description: The repository snapshot file + type: string + HeaderDTO: + description: Data Transfert Object for headers of both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this header (comma separated strings) + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects DTO." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/HeaderDTO' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + - sourceArtifact + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + sourceArtifact: + description: Short name of the main/primary artifact this service was created + from + type: string + Trend: + description: Evolution trend qualifier + enum: + - DOWN + - LOW_DOWN + - STABLE + - LOW_UP + - UP + type: string + WeightedMetricValue: + description: Value of a metric with an associated weight + required: + - name + - weight + - value + type: object + properties: + name: + description: Metric name or serie name + type: string + weight: + description: Weight of this metric value (typically a percentage) + type: integer + value: + description: The value of this metric + type: integer + DailyInvocationStatistic: + description: The daily statistic of a service mock invocations + required: + - id + - day + - serviceName + - serviceVersion + - dailyCount + type: object + properties: + id: + description: Unique identifier of this statistic object + type: string + day: + description: The day (formatted as yyyyMMdd string) represented by this + statistic + type: string + serviceName: + description: The name of the service this statistic is related to + type: string + serviceVersion: + description: The version of the service this statistic is related to + type: string + dailyCount: + description: The number of service mock invocations on this day + type: number + hourlyCount: + description: The number of service mock invocations per hour of the day + (keys range from 0 to 23) + type: object + additionalProperties: true + minuteCount: + description: The number of service mock invocations per minute of the day + (keys range from 0 to 1439) + type: object + additionalProperties: true + TestConformanceMetric: + description: "Represents the test conformance metrics (current score, history\ + \ and evolution trend) of a Service" + required: + - id + - serviceId + - currentScore + - maxPossibleScore + type: object + properties: + id: + description: Unique identifier of coverage metric + type: string + serviceId: + description: Unique identifier of the Service this metric is related to + type: string + aggregationLabelValue: + description: Value of the label used for metrics aggregation (if any) + type: string + maxPossibleScore: + format: double + description: Maximum conformance score that can be reached (depends on samples + expresiveness) + type: number + currentScore: + format: double + description: Current test conformance score for the related Service + type: number + lastUpdateDay: + description: The day of latest score update (in yyyyMMdd format) + type: string + latestTrend: + $ref: '#/components/schemas/Trend' + description: Evolution trend of currentScore + latestScores: + description: "History of latest scores (key is date with format yyyyMMdd,\ + \ value is score as double)" + type: object + additionalProperties: + type: number + TestResultSummary: + description: 'Represents the summary result of a Service or API test run by + Microcks. ' + required: + - id + - testDate + - serviceId + - success + properties: + id: + description: Unique identifier of TestResult + type: string + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + serviceId: + description: Unique identifier of service tested + type: string + success: + description: Flag telling if test is a success + type: boolean + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testDate + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + authorizedClient: + $ref: '#/components/schemas/OAuth2AuthorizedClient' + description: The OAuth2 authorized client that performed the test + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + - AMQP + - NATS + - GOOGLEPUBSUB + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + ResourceType: + description: Types of managed resources for Services or APIs + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + - GRAPHQL_SCHEMA + - POSTMAN_COLLECTION + type: string + CounterMap: + description: A generic map of counter + type: object + additionalProperties: + type: number + OAuth2ClientContent: + description: Represents a volatile OAuth2 client context usually associated + with a Test request + required: + - clientId + - clientSecret + - tokenUri + type: object + properties: + clientId: + description: Id for connecting to OAuth2 identity provider + type: string + clientSecret: + format: password + description: Secret for connecting to OAuth2 identity provider + type: string + tokenUri: + description: URI for retrieving an access token from OAuth2 identity provider + type: string + username: + description: Username in case you're using the Resource Owner Password flow + type: string + password: + description: User password in case you're suing the Resource Owner password + flow + type: string + refreshToken: + description: Refresh token in case you're using the Refresh Token rotation + flow + type: string + OAuth2GrantType: + description: Enumeration for the different supported grants/flows of OAuth2 + enum: + - PASSWORD + - CLIENT_CREDENTIALS + - REFRESH_TOKEN + type: string + OAuth2AuthorizedClient: + description: OAuth2 authorized client that performed a test + required: + - grantType + - principalName + - tokenUri + type: object + properties: + grantType: + $ref: '#/components/schemas/OAuth2GrantType' + description: OAuth2 authorization flow/grant type applied + principalName: + description: Name of authorized principal (clientId or username in the case + of Password grant type) + type: string + tokenUri: + description: Identity Provider URI used for token retrieval + type: string + scopes: + description: Included scopes (separated using space) + type: string + ArtifactDownload: + description: Artifact Download specification to be imported by Microcks. + required: + - url + type: object + properties: + url: + description: The URL of remote artifact to download and import + type: string + mainArtifact: + description: Whether this remote artifact should be imported as main/primary + or secondary artifact. Default is true. + type: boolean + secretName: + description: The name of a secret that can be used to authenticated when + downloading remote artifact. + type: string + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: int64 + description: Creation timestamp for this ImportJob + type: integer + lastImportDate: + format: int64 + description: Timestamp of the last import + type: integer + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + Message: + description: "Common structure for Request, Response and EventMessage" + required: + - name + - operationId + type: object + properties: + id: + description: Unique identifier of this message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + Request: + description: A mock invocation or test request + allOf: + - type: object + properties: + responseId: + description: Unique identifier of Response this Request is attached + type: string + queryParameters: + description: Query parameters for this Request if any + type: array + items: + $ref: '#/components/schemas/Parameter' + - $ref: '#/components/schemas/Message' + Response: + description: A mock invocation or test response + allOf: + - type: object + properties: + status: + description: Status of this Response + type: string + mediaType: + description: Content type of this Response + type: string + dispatchCriteria: + description: Dispatch criteria of this Response (in case of a mock) + type: string + isFault: + description: Whether this Response represents a Fault + type: boolean + - $ref: '#/components/schemas/Message' + EventMessage: + description: A simple event message published or received in an asynchronous + exchange + type: object + allOf: + - required: + - id + - mediaType + type: object + properties: + mediaType: + description: Content type of message + type: string + dispatchCriteria: + description: Dispatch criteria of this message (in case of a mock) + type: string + - $ref: '#/components/schemas/Message' + Parameter: + description: Companion objects for Request representing query parameter + required: + - name + - value + type: object + properties: + name: + description: The name of the parameter + type: string + value: + description: The value of this parameter + type: string + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + scopes: + user: Simple authenticated user + manager: Services & APIs content manager + admin: Administrator of the Microcks instance + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: + - jwt-bearer: [] +tags: + - name: mock + description: Operations related to API and Services mocks + - name: test + description: Operations related to API and Services tests + - name: job + description: Operations related to Jobs for discovering mocks and tests + - name: config + description: Operations related to configuration + - name: metrics + description: Operations related to metrics diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.12.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.12.yaml new file mode 100644 index 000000000..7b0b77d86 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.12.yaml @@ -0,0 +1,2314 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.12 + version: 1.12.0 + description: "API offered by Microcks, the Kubernetes native tool for API and microservices\ + \ mocking and testing (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/microcks + email: laurent@microcks.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + x-logo: + backgroundColor: '#ffffff' + url: https://microcks.io/images/microcks-logo-blue.png +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + operationId: GetImportJob + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + operationId: UpdateImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + requestBody: + description: The Secret to update + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + required: true + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true + /features/config: + summary: Optional features configuration + get: + tags: + - config + responses: + "200": + content: + application/json: {} + description: Get features configuration + operationId: GetFeaturesConfig + summary: Get features configuration + /import: + summary: Deals with repository snapshot to import in Microcks + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SnapshotUpload' + required: true + tags: + - mock + responses: + "201": + description: Snasphot has been correctly imported + security: + - jwt-bearer: + - admin + operationId: importSnapshot + summary: Import a snapshot + description: Import a repository snapshot previsouly exported into Microcks + /export: + summary: Deals with repository snapshot to import from Microcks + get: + tags: + - mock + parameters: + - name: serviceIds + description: List of service identifiers to export + schema: + type: array + items: + type: string + in: query + required: true + responses: + "200": + headers: + Content-Disposition: + schema: + type: string + examples: + filename: + value: attachment; filename=microcks-repository.json + content: + application/json: + schema: + format: binary + type: string + description: Snapshot file representing the export of requested services + security: + - jwt-bearer: + - admin + operationId: exportSnapshot + summary: Export a snapshot + description: Export a repostiory snapshot with requested services + /metrics/invocations/global: + summary: Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Aggregated invocation statistics for specified day + operationId: GetAggregatedInvocationsStats + summary: Get aggregated invocation statistics for a day + /metrics/conformance/aggregate: + summary: Aggregation of Test conformance metrics + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WeightedMetricValue' + description: Get aggregated coverage metric value + operationId: GetConformanceMetricsAggregation + summary: Get aggregation of conformance metrics + /metrics/conformance/service/{serviceId}: + summary: Test Conformance metrics on API and Services + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestConformanceMetric' + description: Test coverage metric for Service + operationId: GetServiceTestConformanceMetric + summary: Get conformance metrics for a Service + parameters: + - name: serviceId + description: Unique Services identifier this metrics are related to + schema: + type: string + in: path + required: true + /metrics/invocations/top: + summary: Top Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + - name: limit + description: The number of top invoked mocks to return + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Top invocations for a defined day + operationId: GetTopIvnocationsStatsByDay + summary: Get top invocation statistics for a day + /metrics/invocations/{serviceName}/{serviceVersion}: + summary: Invocation Statistics for API and Services + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Invocation statistics for service for specified day + operationId: GetInvocationStatsByService + summary: Get invocation statistics for Service + parameters: + - name: serviceName + description: Name of service to get statistics for + schema: + type: string + in: path + required: true + - name: serviceVersion + description: Version of service to get statistics for + schema: + type: string + in: path + required: true + /metrics/invocations/global/latest: + summary: Latest Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to get back in time. Default is 20. + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/CounterMap' + description: A map where keys are day (formatted using yyyyMMdd pattern) + and values are counter of invocations on this day + operationId: GetLatestAggregatedInvocationsStats + summary: Get aggregated invocations statistics for latest days + /metrics/tests/latest: + summary: Lastest Test results across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to consider for test results to return. Default + is 7 (one week) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResultSummary' + description: Test results summary for specified last days. + operationId: GetLatestTestResults + summary: Get latest tests results + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - manager + - admin + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - user + operationId: GetServicesLabels + summary: Get the already used labels for Services + /artifact/download: + summary: Deals with artifacts to be downloaded and imported by Microcks. + post: + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ArtifactDownload' + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No url attribute found in download data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - manager + operationId: downloadArtifact + summary: Download an artifact + description: Ask Microcks to download an artifact and import it + /secrets/search: + get: + tags: + - config + parameters: + - name: name + description: Search using this name-like criterion + schema: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets (filtered according search criteria) + security: + - jwt-bearer: + - admin + operationId: SearchSecrets + summary: Search for Secrets + /artifact/upload: + summary: Deals with artifacts to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + parameters: + - name: mainArtifact + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + schema: + type: boolean + in: query + required: true + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - manager + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact to be imported by Microcks. +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + - enabled + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: boolean + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + enabled: + description: Whether Keycloak authentification and usage is enabled + type: boolean + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: number + readOnly: true + lastUpdate: + description: Last update of attached object + type: number + readOnly: true + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Exchanges for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + format: int64 + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + oAuth2Context: + $ref: '#/components/schemas/OAuth2ClientContext' + description: An OAuth2 context to use for retrieving an access token prior + invoking the tested endpoint + operationsHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relative path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + - GRAPHQL_SCHEMA + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ArtifactUpload: + description: |- + Artifact to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + description: A simple Counter type. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + FeaturesConfig: + title: Root Type for FeaturesConfig + description: Representation of optional features configuration used by Microcks + server + type: object + properties: + repository-filter: + description: Repository filtering feature properties + type: object + properties: + label-label: + type: string + enabled: + type: string + label-list: + type: string + label-key: + type: string + microcks-hub: + description: Microcks Hub feature properties + type: object + properties: + allowed-roles: + type: string + endpoint: + type: string + enabled: + type: string + repository-tenancy: + description: Repository tenancy feature properties + type: object + properties: + enabled: + type: string + artifact-import-allowed-roles: + type: string + async-api: + description: Asynchronous feature properties + type: object + properties: + default-binding: + type: string + endpoint-MQTT: + type: string + endpoint-WS: + type: string + endpoint-KAFKA: + type: string + endpoint-AMQP: + type: string + endpoint-NATS: + type: string + endpoint-GOOGLEPUBSUB: + type: string + enabled: + type: string + frequencies: + type: string + additionalProperties: true + example: + repository-filter: + label-label: Domain + enabled: "true" + label-list: "domain,status" + label-key: domain + microcks-hub: + allowed-roles: "admin,manager,manager-any" + endpoint: https://hub.microcks.io/api + enabled: "true" + repository-tenancy: + enabled: "false" + artifact-import-allowed-roles: "admin,manager,manager-any" + async-api: + default-binding: KAFKA + endpoint-MQTT: my-mqtt-broker.apps.try.microcks.io + endpoint-WS: localhost:8081 + endpoint-KAFKA: my-cluster-kafka-bootstrap.apps.try.microcks.io + enabled: "false" + frequencies: "3,10,30" + SnapshotUpload: + description: Upload of a repository snapshot file + required: + - file + type: object + properties: + file: + format: binary + description: The repository snapshot file + type: string + HeaderDTO: + description: Data Transfert Object for headers of both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this header (comma separated strings) + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects DTO." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/HeaderDTO' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + - sourceArtifact + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + sourceArtifact: + description: Short name of the main/primary artifact this service was created + from + type: string + Trend: + description: Evolution trend qualifier + enum: + - DOWN + - LOW_DOWN + - STABLE + - LOW_UP + - UP + type: string + WeightedMetricValue: + description: Value of a metric with an associated weight + required: + - name + - weight + - value + type: object + properties: + name: + description: Metric name or serie name + type: string + weight: + description: Weight of this metric value (typically a percentage) + type: integer + value: + description: The value of this metric + type: integer + DailyInvocationStatistic: + description: The daily statistic of a service mock invocations + required: + - id + - day + - serviceName + - serviceVersion + - dailyCount + type: object + properties: + id: + description: Unique identifier of this statistic object + type: string + day: + description: The day (formatted as yyyyMMdd string) represented by this + statistic + type: string + serviceName: + description: The name of the service this statistic is related to + type: string + serviceVersion: + description: The version of the service this statistic is related to + type: string + dailyCount: + description: The number of service mock invocations on this day + type: number + hourlyCount: + description: The number of service mock invocations per hour of the day + (keys range from 0 to 23) + type: object + additionalProperties: true + minuteCount: + description: The number of service mock invocations per minute of the day + (keys range from 0 to 1439) + type: object + additionalProperties: true + TestConformanceMetric: + description: "Represents the test conformance metrics (current score, history\ + \ and evolution trend) of a Service" + required: + - id + - serviceId + - currentScore + - maxPossibleScore + type: object + properties: + id: + description: Unique identifier of coverage metric + type: string + serviceId: + description: Unique identifier of the Service this metric is related to + type: string + aggregationLabelValue: + description: Value of the label used for metrics aggregation (if any) + type: string + maxPossibleScore: + format: double + description: Maximum conformance score that can be reached (depends on samples + expresiveness) + type: number + currentScore: + format: double + description: Current test conformance score for the related Service + type: number + lastUpdateDay: + description: The day of latest score update (in yyyyMMdd format) + type: string + latestTrend: + $ref: '#/components/schemas/Trend' + description: Evolution trend of currentScore + latestScores: + description: "History of latest scores (key is date with format yyyyMMdd,\ + \ value is score as double)" + type: object + additionalProperties: + type: number + TestResultSummary: + description: 'Represents the summary result of a Service or API test run by + Microcks. ' + required: + - id + - testDate + - serviceId + - success + properties: + id: + description: Unique identifier of TestResult + type: string + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + serviceId: + description: Unique identifier of service tested + type: string + success: + description: Flag telling if test is a success + type: boolean + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testDate + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + authorizedClient: + $ref: '#/components/schemas/OAuth2AuthorizedClient' + description: The OAuth2 authorized client that performed the test + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + - AMQP + - NATS + - GOOGLEPUBSUB + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + ResourceType: + description: Types of managed resources for Services or APIs + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + - GRAPHQL_SCHEMA + - POSTMAN_COLLECTION + type: string + CounterMap: + description: A generic map of counter + type: object + additionalProperties: + type: number + OAuth2GrantType: + description: Enumeration for the different supported grants/flows of OAuth2 + enum: + - PASSWORD + - CLIENT_CREDENTIALS + - REFRESH_TOKEN + type: string + OAuth2AuthorizedClient: + description: OAuth2 authorized client that performed a test + required: + - grantType + - principalName + - tokenUri + type: object + properties: + grantType: + $ref: '#/components/schemas/OAuth2GrantType' + description: OAuth2 authorization flow/grant type applied + principalName: + description: Name of authorized principal (clientId or username in the case + of Password grant type) + type: string + tokenUri: + description: Identity Provider URI used for token retrieval + type: string + scopes: + description: Included scopes (separated using space) + type: string + ArtifactDownload: + description: Artifact Download specification to be imported by Microcks. + required: + - url + type: object + properties: + url: + description: The URL of remote artifact to download and import + type: string + mainArtifact: + description: Whether this remote artifact should be imported as main/primary + or secondary artifact. Default is true. + type: boolean + secretName: + description: The name of a secret that can be used to authenticated when + downloading remote artifact. + type: string + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: int64 + description: Creation timestamp for this ImportJob + type: integer + lastImportDate: + format: int64 + description: Timestamp of the last import + type: integer + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + Message: + description: "Common structure for Request, Response and EventMessage" + required: + - name + - operationId + type: object + properties: + id: + description: Unique identifier of this message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + Request: + description: A mock invocation or test request + allOf: + - type: object + properties: + responseId: + description: Unique identifier of Response this Request is attached + type: string + queryParameters: + description: Query parameters for this Request if any + type: array + items: + $ref: '#/components/schemas/Parameter' + - $ref: '#/components/schemas/Message' + Response: + description: A mock invocation or test response + allOf: + - type: object + properties: + status: + description: Status of this Response + type: string + mediaType: + description: Content type of this Response + type: string + dispatchCriteria: + description: Dispatch criteria of this Response (in case of a mock) + type: string + isFault: + description: Whether this Response represents a Fault + type: boolean + - $ref: '#/components/schemas/Message' + EventMessage: + description: A simple event message published or received in an asynchronous + exchange + type: object + allOf: + - required: + - id + - mediaType + type: object + properties: + mediaType: + description: Content type of message + type: string + dispatchCriteria: + description: Dispatch criteria of this message (in case of a mock) + type: string + - $ref: '#/components/schemas/Message' + Parameter: + description: Companion objects for Request representing query parameter + required: + - name + - value + type: object + properties: + name: + description: The name of the parameter + type: string + value: + description: The value of this parameter + type: string + OAuth2ClientContext: + description: Represents a volatile OAuth2 client context usually associated + with a Test request + required: + - clientId + - clientSecret + - tokenUri + - grantType + type: object + properties: + clientId: + description: Id for connecting to OAuth2 identity provider + type: string + clientSecret: + format: password + description: Secret for connecting to OAuth2 identity provider + type: string + tokenUri: + description: URI for retrieving an access token from OAuth2 identity provider + type: string + username: + description: Username in case you're using the Resource Owner Password flow + type: string + password: + description: User password in case you're suing the Resource Owner password + flow + type: string + refreshToken: + description: Refresh token in case you're using the Refresh Token rotation + flow + type: string + scopes: + description: "The OAuth scopes to have in claimed token (delimited by space,\ + \ \"openid\" is applied by default)" + type: string + grantType: + description: The OAuth2 grant type to apply for retrieving the token + enum: + - PASSWORD + - CLIENT_CREDENTIALS + - REFRESH_TOKEN + type: string + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + scopes: + user: Simple authenticated user + manager: Services & APIs content manager + admin: Administrator of the Microcks instance + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration +- name: metrics + description: Operations related to metrics diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.2.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.2.yaml new file mode 100644 index 000000000..690cc2f06 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.2.yaml @@ -0,0 +1,1518 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.2 + version: 1.2.0 + description: "API offered by Microcks, the mock and testing platform for API and\ + \ microservices (microcks.github.io)" + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - user:identity + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - user + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - user + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - user + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: "Deals with artifacts (SOAP UI project, Postman collection or OpenAPI\ + \ Specification) to be imported by Microcks." + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: "POST /api/artifact/upload HTTP/1.1\nHost: microcks\nUser-Agent:\ + \ curl/7.54.0\nAccept: */*\nAuthorization: Bearer \nContent-Length:\ + \ 2743\nExpect: 100-continue\nContent-Type: multipart/form-data;\ + \ boundary=------------------------8af8cbb56dd4bde0\n\n--------------------------8af8cbb56dd4bde0\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"github.json\"\ + \nContent-Type: application/octet-stream\n\nTHE ARTIFACT HERE\n\n\ + --------------------------8af8cbb56dd4bde0--\n" + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + operationId: uploadArtifact + summary: Upload an artifact + description: "Uploads an artifact (SOAP UI project, Postman collection or OpenAPI\ + \ Specification) to be imported by Microcks." + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - admin + operationId: GetServicesLabels + summary: Get the already used labels for Services + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - EVENT + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Wether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Wether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: "{\n \"secretId\": \"5be58fb51ed744d1b87481bd\",\n \"name\":\ + \ \"Gogs internal\"\n}" + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: "{\n \"id\": \"5be58fb51ed744d1b87481bd\",\n \"name\": \"Gogs\ + \ internal\",\n \"description\": \"Gogs internal corporate repository\"\ + ,\n \"username\": \"team\",\n \"password\": \"team\",\n \"caCertPem\"\ + : \"-----BEGIN CERTIFICATE-----\\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\\\ + nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\\\ + nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\\\ + nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\\\ + n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\\\ + nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\\\ + nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\\\ + ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\\\ + nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\\n-----END CERTIFICATE-----\"\n}" + ArtifactUpload: + title: Root Type for ArtifactUpload + description: "Artifact (SOAP UI project, Postman collection or OpenAPI Specification)\ + \ to be imported by Microcks.\nThis structure represents a mime-multipart\ + \ file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/)" + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + title: Root Type for Counter + description: The root of the Counter type's schema. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: "{\n \"counter\": 12\n}" + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: string + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispactherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + type: array + items: + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Header' + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + scopes: + user: "" + manager: "" + admin: "" + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.3.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.3.yaml new file mode 100644 index 000000000..9b3f12504 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.3.yaml @@ -0,0 +1,1634 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.3 + version: 1.3.0 + description: "API offered by Microcks, the mock and testing platform for API and\ + \ microservices (microcks.github.io)" + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - user:identity + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - user + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - user + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - user + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: "Deals with artifacts (SOAP UI project, Postman collection or OpenAPI\ + \ Specification) to be imported by Microcks." + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: "POST /api/artifact/upload HTTP/1.1\nHost: microcks\nUser-Agent:\ + \ curl/7.54.0\nAccept: */*\nAuthorization: Bearer \nContent-Length:\ + \ 2743\nExpect: 100-continue\nContent-Type: multipart/form-data;\ + \ boundary=------------------------8af8cbb56dd4bde0\n\n--------------------------8af8cbb56dd4bde0\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"github.json\"\ + \nContent-Type: application/octet-stream\n\nTHE ARTIFACT HERE\n\n\ + --------------------------8af8cbb56dd4bde0--\n" + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - user + operationId: uploadArtifact + summary: Upload an artifact + description: "Uploads an artifact (SOAP UI project, Postman collection or OpenAPI\ + \ Specification) to be imported by Microcks." + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - admin + operationId: GetServicesLabels + summary: Get the already used labels for Services + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - EVENT + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Wether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Wether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: "{\n \"secretId\": \"5be58fb51ed744d1b87481bd\",\n \"name\":\ + \ \"Gogs internal\"\n}" + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: "{\n \"id\": \"5be58fb51ed744d1b87481bd\",\n \"name\": \"Gogs\ + \ internal\",\n \"description\": \"Gogs internal corporate repository\"\ + ,\n \"username\": \"team\",\n \"password\": \"team\",\n \"caCertPem\"\ + : \"-----BEGIN CERTIFICATE-----\\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\\\ + nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\\\ + nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\\\ + nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\\\ + n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\\\ + nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\\\ + nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\\\ + ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\\\ + nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\\n-----END CERTIFICATE-----\"\n}" + ArtifactUpload: + title: Root Type for ArtifactUpload + description: "Artifact (SOAP UI project, Postman collection or OpenAPI Specification)\ + \ to be imported by Microcks.\nThis structure represents a mime-multipart\ + \ file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/)" + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + mainArtifact: + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + type: boolean + Counter: + title: Root Type for Counter + description: The root of the Counter type's schema. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: "{\n \"counter\": 12\n}" + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: string + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispactherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Header' + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + type: array + items: + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + type: string + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relatvie path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + ResourceType: + description: Types of managed resources for Services or APIs + type: array + items: + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + type: string + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + scopes: + user: "" + manager: "" + admin: "" + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.4.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.4.yaml new file mode 100644 index 000000000..759795a74 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.4.yaml @@ -0,0 +1,1651 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.4 + version: 1.4.0 + description: "API offered by Microcks, the mock and testing platform for API and\ + \ microservices (microcks.github.io)" + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - user:identity + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - user + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - user + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - user + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: "Deals with artifacts (SOAP UI project, Postman collection or OpenAPI\ + \ Specification) to be imported by Microcks." + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - user + operationId: uploadArtifact + summary: Upload an artifact + description: "Uploads an artifact (SOAP UI project, Postman collection or OpenAPI\ + \ Specification) to be imported by Microcks." + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - admin + operationId: GetServicesLabels + summary: Get the already used labels for Services + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + ArtifactUpload: + title: Root Type for ArtifactUpload + description: |- + Artifact (SOAP UI project, Postman collection or OpenAPI Specification) to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + mainArtifact: + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + type: boolean + Counter: + title: Root Type for Counter + description: The root of the Counter type's schema. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: string + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispactherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Header' + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + type: array + items: + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + type: string + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relatvie path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + ResourceType: + description: Types of managed resources for Services or APIs + type: array + items: + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + type: string + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - EVENT + - GRPC + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + scopes: + user: "" + manager: "" + admin: "" + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.5.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.5.yaml new file mode 100644 index 000000000..e14450565 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.5.yaml @@ -0,0 +1,1813 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.5 + version: 1.5.1 + description: "API offered by Microcks, the mock and testing platform for API and\ + \ microservices (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: + - url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - user:identity + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - user + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - user + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - user + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: Deals with artifacts to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - user + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact to be imported by Microcks. + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - admin + operationId: GetServicesLabels + summary: Get the already used labels for Services + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true + /features/config: + summary: Optional features configuration + get: + tags: + - config + responses: + "200": + content: + application/json: {} + description: Get features configuration + operationId: GetFeaturesConfiguration + summary: Get features configuration + /import: + summary: Deals with repository snapshot to import in Microcks + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SnapshotUpload' + required: true + tags: + - mock + responses: + "201": + description: Snasphot has been correctly imported + security: + - jwt-bearer: + - admin + operationId: importSnapshot + summary: Import a snapshot + description: Import a repository snapshot previsouly exported into Microcks + /export: + summary: Deals with repository snapshot to import from Microcks + get: + tags: + - mock + parameters: + - name: serviceIds + description: List of service identifiers to export + schema: + type: array + items: + type: string + in: query + required: true + responses: + "200": + headers: + Content-Disposition: + schema: + type: string + examples: + filename: + value: attachment; filename=microcks-repository.json + content: + application/json: + schema: + format: binary + type: string + description: Snapshot file representing the export of requested services + security: + - jwt-bearer: + - admin + operationId: exportSnapshot + summary: Export a snapshot + description: Export a repostiory snapshot with requested services +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: string + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relatvie path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + - AMQP + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - EVENT + - GRPC + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + type: array + items: + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + - GRAPHQL_SCHEMA + type: string + ResourceType: + description: Types of managed resources for Services or APIs + type: array + items: + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + - GRAPHQL_SCHEMA + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ArtifactUpload: + description: |- + Artifact to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + mainArtifact: + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + type: boolean + Counter: + description: The root of the Counter type's schema. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + FeaturesConfig: + title: Root Type for FeaturesConfig + description: Representation of optional features configuration used by Microcks + server + type: object + properties: + repository-filter: + description: Repository filtering feature properties + type: object + properties: + label-label: + type: string + enabled: + type: string + label-list: + type: string + label-key: + type: string + microcks-hub: + description: Microcks Hub feature properties + type: object + properties: + allowed-roles: + type: string + endpoint: + type: string + enabled: + type: string + repository-tenancy: + description: Repository tenancy feature properties + type: object + properties: + enabled: + type: string + artifact-import-allowed-roles: + type: string + async-api: + description: Asynchronous feature properties + type: object + properties: + default-binding: + type: string + endpoint-MQTT: + type: string + endpoint-WS: + type: string + endpoint-KAFKA: + type: string + enabled: + type: string + frequencies: + type: string + additionalProperties: true + example: + repository-filter: + label-label: Domain + enabled: "true" + label-list: "domain,status" + label-key: domain + microcks-hub: + allowed-roles: "admin,manager,manager-any" + endpoint: https://hub.microcks.io/api + enabled: "true" + repository-tenancy: + enabled: "false" + artifact-import-allowed-roles: "admin,manager,manager-any" + async-api: + default-binding: KAFKA + endpoint-MQTT: my-mqtt-broker.apps.try.microcks.io + endpoint-WS: localhost:8081 + endpoint-KAFKA: my-cluster-kafka-bootstrap.apps.try.microcks.io + enabled: "false" + frequencies: "3,10,30" + SnapshotUpload: + description: Upload of a repository snapshot file + required: + - file + type: object + properties: + file: + format: binary + description: The repository snapshot file + type: string + HeaderDTO: + description: Data Transfert Object for headers of both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this header (comma separated strings) + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects DTO." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/HeaderDTO' + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + scopes: + user: "" + manager: "" + admin: "" + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: + - jwt-bearer: [] +tags: + - name: mock + description: Operations related to API and Services mocks + - name: test + description: Operations related to API and Services tests + - name: job + description: Operations related to Jobs for discovering mocks and tests + - name: config + description: Operations related to configuration diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.6.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.6.yaml new file mode 100644 index 000000000..a96160b55 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.6.yaml @@ -0,0 +1,2122 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.6 + version: 1.6.0 + description: "API offered by Microcks, the mock and testing platform for API and\ + \ microservices (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - user:identity + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - user + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - user + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - user + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: Deals with artifacts to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + parameters: + - name: mainArtifact + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + schema: + type: boolean + in: query + required: true + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - user + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact to be imported by Microcks. + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - admin + operationId: GetServicesLabels + summary: Get the already used labels for Services + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true + /features/config: + summary: Optional features configuration + get: + tags: + - config + responses: + "200": + content: + application/json: {} + description: Get features configuration + operationId: GetFeaturesConfiguration + summary: Get features configuration + /import: + summary: Deals with repository snapshot to import in Microcks + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SnapshotUpload' + required: true + tags: + - mock + responses: + "201": + description: Snasphot has been correctly imported + security: + - jwt-bearer: + - admin + operationId: importSnapshot + summary: Import a snapshot + description: Import a repository snapshot previsouly exported into Microcks + /export: + summary: Deals with repository snapshot to import from Microcks + get: + tags: + - mock + parameters: + - name: serviceIds + description: List of service identifiers to export + schema: + type: array + items: + type: string + in: query + required: true + responses: + "200": + headers: + Content-Disposition: + schema: + type: string + examples: + filename: + value: attachment; filename=microcks-repository.json + content: + application/json: + schema: + format: binary + type: string + description: Snapshot file representing the export of requested services + security: + - jwt-bearer: + - admin + operationId: exportSnapshot + summary: Export a snapshot + description: Export a repostiory snapshot with requested services + /metrics/invocations/global: + summary: Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Aggregated invocation statistics for specified day + operationId: GetAggregatedInvocationsStats + summary: Get aggregated invocation statistics for a day + /metrics/conformance/aggregate: + summary: Aggregation of Test conformance metrics + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WeightedMetricValue' + description: Get aggregated coverage metric value + operationId: GetConformanceMetricsAggregation + summary: Get aggregation of conformance metrics + /metrics/conformance/service/{serviceId}: + summary: Test Conformance metrics on API and Services + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestConformanceMetric' + description: Test coverage metric for Service + operationId: GetServiceTestConformanceMetric + summary: Get conformance metrics for a Service + parameters: + - name: serviceId + description: Unique Services identifier this metrics are related to + schema: + type: string + in: path + required: true + /metrics/invocations/top: + summary: Top Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + - name: limit + description: The number of top invoked mocks to return + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Top invocations for a defined day + operationId: GetTopIvnocationsStatsByDay + summary: Get top invocation statistics for a day + /metrics/invocations/{serviceName}/{serviceVersion}: + summary: Invocation Statistics for API and Services + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Invocation statistics for service for specified day + operationId: GetInvocationStatsByService + summary: Get invocation statistics for Service + parameters: + - name: serviceName + description: Name of service to get statistics for + schema: + type: string + in: path + required: true + - name: serviceVersion + description: Version of service to get statistics for + schema: + type: string + in: path + required: true + /metrics/invocations/global/latest: + summary: Latest Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to get back in time. Default is 20. + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/CounterMap' + description: A map where keys are day (formatted using yyyyMMdd pattern) + and values are counter of invocations on this day + operationId: GetLatestAggregatedInvocationsStats + summary: Get aggregated invocations statistics for latest days + /metrics/tests/latest: + summary: Lastest Test results across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to consider for test results to return. Default + is 7 (one week) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResultSummary' + description: Test results summary for specified last days. + operationId: GetLatestTestResults + summary: Get latest tests results +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + - enabled + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: string + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + enabled: + description: Whether Keycloak authentification and usage is enabled + type: boolean + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relatvie path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + - AMQP + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + - GRAPHQL_SCHEMA + type: string + ResourceType: + description: Types of managed resources for Services or APIs + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + - GRAPHQL_SCHEMA + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ArtifactUpload: + description: |- + Artifact to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + description: A simple Counter type. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + FeaturesConfig: + title: Root Type for FeaturesConfig + description: Representation of optional features configuration used by Microcks + server + type: object + properties: + repository-filter: + description: Repository filtering feature properties + type: object + properties: + label-label: + type: string + enabled: + type: string + label-list: + type: string + label-key: + type: string + microcks-hub: + description: Microcks Hub feature properties + type: object + properties: + allowed-roles: + type: string + endpoint: + type: string + enabled: + type: string + repository-tenancy: + description: Repository tenancy feature properties + type: object + properties: + enabled: + type: string + artifact-import-allowed-roles: + type: string + async-api: + description: Asynchronous feature properties + type: object + properties: + default-binding: + type: string + endpoint-MQTT: + type: string + endpoint-WS: + type: string + endpoint-KAFKA: + type: string + enabled: + type: string + frequencies: + type: string + additionalProperties: true + example: + repository-filter: + label-label: Domain + enabled: "true" + label-list: "domain,status" + label-key: domain + microcks-hub: + allowed-roles: "admin,manager,manager-any" + endpoint: https://hub.microcks.io/api + enabled: "true" + repository-tenancy: + enabled: "false" + artifact-import-allowed-roles: "admin,manager,manager-any" + async-api: + default-binding: KAFKA + endpoint-MQTT: my-mqtt-broker.apps.try.microcks.io + endpoint-WS: localhost:8081 + endpoint-KAFKA: my-cluster-kafka-bootstrap.apps.try.microcks.io + enabled: "false" + frequencies: "3,10,30" + SnapshotUpload: + description: Upload of a repository snapshot file + required: + - file + type: object + properties: + file: + format: binary + description: The repository snapshot file + type: string + HeaderDTO: + description: Data Transfert Object for headers of both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this header (comma separated strings) + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects DTO." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/HeaderDTO' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + Trend: + description: Evolution trend qualifier + enum: + - DOWN + - LOW_DOWN + - STABLE + - LOW_UP + - UP + type: string + WeightedMetricValue: + description: Value of a metric with an associated weight + required: + - name + - weight + - value + type: object + properties: + name: + description: MEtric name or serie name + type: string + weight: + description: Weight of this metric value (typically a percentage) + type: integer + value: + description: The value of this metric + type: integer + DailyInvocationStatistic: + description: The daily statistic of a service mock invocations + required: + - id + - day + - serviceName + - serviceVersion + - dailyCount + type: object + properties: + id: + description: Unique identifier of this statistic object + type: string + day: + description: The day (formatted as yyyyMMdd string) represented by this + statistic + type: string + serviceName: + description: The name of the service this statistic is related to + type: string + serviceVersion: + description: The version of the service this statistic is related to + type: string + dailyCount: + description: The number of service mock invocations on this day + type: number + hourlyCount: + description: The number of service mock invocations per hour of the day + (keys range from 0 to 23) + type: object + additionalProperties: true + minuteCount: + description: The number of service mock invocations per minute of the day + (keys range from 0 to 1439) + type: object + additionalProperties: true + TestConformanceMetric: + description: "Represents the test conformance metrics (current score, history\ + \ and evolution trend) of a Service" + required: + - id + - serviceId + - currentScore + - maxPossibleScore + type: object + properties: + id: + description: Unique identifier of coverage metric + type: string + serviceId: + description: Unique identifier of the Service this metric is related to + type: string + aggregationLabelValue: + description: Value of the label used for metrics aggregation (if any) + type: string + maxPossibleScore: + format: double + description: Maximum conformance score that can be reached (depends on samples + expresiveness) + type: number + currentScore: + format: double + description: Current test conformance score for the related Service + type: number + lastUpdateDay: + description: The day of latest score update (in yyyyMMdd format) + type: string + latestTrend: + $ref: '#/components/schemas/Trend' + description: Evolution trend of currentScore + latestScores: + description: "History of latest scores (key is date with format yyyyMMdd,\ + \ value is score as double)" + type: object + additionalProperties: + type: number + CounterMap: + description: A generic map of counter + type: object + additionalProperties: + format: integer + type: number + TestResultSummary: + description: 'Represents the summary result of a Service or API test run by + Microcks. ' + required: + - id + - testDate + - serviceId + - success + properties: + id: + description: Unique identifier of TestResult + type: string + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + serviceId: + description: Unique identifier of service tested + type: string + success: + description: Flag telling if test is a success + type: boolean + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testDate + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/auth/realms/microcks/protocol/openid-connect/token + scopes: + user: "" + manager: "" + admin: "" + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration +- name: metrics + description: Operations related to metrics diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.7.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.7.yaml new file mode 100644 index 000000000..e467e6ec1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.7.yaml @@ -0,0 +1,2132 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.7 + version: 1.7.1 + description: "API offered by Microcks, the Kubernetes native tools for API and microservices\ + \ mocking and testing (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/microcks + email: laurent@microcks.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + x-logo: + backgroundColor: '#ffffff' + url: https://microcks.io/images/microcks-logo-blue.png +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - user + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - user + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - user + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: Deals with artifacts to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + parameters: + - name: mainArtifact + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + schema: + type: boolean + in: query + required: true + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - user + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact to be imported by Microcks. + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - admin + operationId: GetServicesLabels + summary: Get the already used labels for Services + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true + /features/config: + summary: Optional features configuration + get: + tags: + - config + responses: + "200": + content: + application/json: {} + description: Get features configuration + operationId: GetFeaturesConfiguration + summary: Get features configuration + /import: + summary: Deals with repository snapshot to import in Microcks + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SnapshotUpload' + required: true + tags: + - mock + responses: + "201": + description: Snasphot has been correctly imported + security: + - jwt-bearer: + - admin + operationId: importSnapshot + summary: Import a snapshot + description: Import a repository snapshot previsouly exported into Microcks + /export: + summary: Deals with repository snapshot to import from Microcks + get: + tags: + - mock + parameters: + - name: serviceIds + description: List of service identifiers to export + schema: + type: array + items: + type: string + in: query + required: true + responses: + "200": + headers: + Content-Disposition: + schema: + type: string + examples: + filename: + value: attachment; filename=microcks-repository.json + content: + application/json: + schema: + format: binary + type: string + description: Snapshot file representing the export of requested services + security: + - jwt-bearer: + - admin + operationId: exportSnapshot + summary: Export a snapshot + description: Export a repostiory snapshot with requested services + /metrics/invocations/global: + summary: Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Aggregated invocation statistics for specified day + operationId: GetAggregatedInvocationsStats + summary: Get aggregated invocation statistics for a day + /metrics/conformance/aggregate: + summary: Aggregation of Test conformance metrics + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WeightedMetricValue' + description: Get aggregated coverage metric value + operationId: GetConformanceMetricsAggregation + summary: Get aggregation of conformance metrics + /metrics/conformance/service/{serviceId}: + summary: Test Conformance metrics on API and Services + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestConformanceMetric' + description: Test coverage metric for Service + operationId: GetServiceTestConformanceMetric + summary: Get conformance metrics for a Service + parameters: + - name: serviceId + description: Unique Services identifier this metrics are related to + schema: + type: string + in: path + required: true + /metrics/invocations/top: + summary: Top Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + - name: limit + description: The number of top invoked mocks to return + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Top invocations for a defined day + operationId: GetTopIvnocationsStatsByDay + summary: Get top invocation statistics for a day + /metrics/invocations/{serviceName}/{serviceVersion}: + summary: Invocation Statistics for API and Services + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Invocation statistics for service for specified day + operationId: GetInvocationStatsByService + summary: Get invocation statistics for Service + parameters: + - name: serviceName + description: Name of service to get statistics for + schema: + type: string + in: path + required: true + - name: serviceVersion + description: Version of service to get statistics for + schema: + type: string + in: path + required: true + /metrics/invocations/global/latest: + summary: Latest Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to get back in time. Default is 20. + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/CounterMap' + description: A map where keys are day (formatted using yyyyMMdd pattern) + and values are counter of invocations on this day + operationId: GetLatestAggregatedInvocationsStats + summary: Get aggregated invocations statistics for latest days + /metrics/tests/latest: + summary: Lastest Test results across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to consider for test results to return. Default + is 7 (one week) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResultSummary' + description: Test results summary for specified last days. + operationId: GetLatestTestResults + summary: Get latest tests results +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + - enabled + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: boolean + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + enabled: + description: Whether Keycloak authentification and usage is enabled + type: boolean + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relatvie path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + - GRAPHQL_SCHEMA + type: string + ResourceType: + description: Types of managed resources for Services or APIs + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + - GRAPHQL_SCHEMA + - POSTMAN_COLLECTION + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ArtifactUpload: + description: |- + Artifact to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + description: A simple Counter type. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + FeaturesConfig: + title: Root Type for FeaturesConfig + description: Representation of optional features configuration used by Microcks + server + type: object + properties: + repository-filter: + description: Repository filtering feature properties + type: object + properties: + label-label: + type: string + enabled: + type: string + label-list: + type: string + label-key: + type: string + microcks-hub: + description: Microcks Hub feature properties + type: object + properties: + allowed-roles: + type: string + endpoint: + type: string + enabled: + type: string + repository-tenancy: + description: Repository tenancy feature properties + type: object + properties: + enabled: + type: string + artifact-import-allowed-roles: + type: string + async-api: + description: Asynchronous feature properties + type: object + properties: + default-binding: + type: string + endpoint-MQTT: + type: string + endpoint-WS: + type: string + endpoint-KAFKA: + type: string + endpoint-AMQP: + type: string + endpoint-NATS: + type: string + endpoint-GOOGLEPUBSUB: + type: string + enabled: + type: string + frequencies: + type: string + additionalProperties: true + example: + repository-filter: + label-label: Domain + enabled: "true" + label-list: "domain,status" + label-key: domain + microcks-hub: + allowed-roles: "admin,manager,manager-any" + endpoint: https://hub.microcks.io/api + enabled: "true" + repository-tenancy: + enabled: "false" + artifact-import-allowed-roles: "admin,manager,manager-any" + async-api: + default-binding: KAFKA + endpoint-MQTT: my-mqtt-broker.apps.try.microcks.io + endpoint-WS: localhost:8081 + endpoint-KAFKA: my-cluster-kafka-bootstrap.apps.try.microcks.io + enabled: "false" + frequencies: "3,10,30" + SnapshotUpload: + description: Upload of a repository snapshot file + required: + - file + type: object + properties: + file: + format: binary + description: The repository snapshot file + type: string + HeaderDTO: + description: Data Transfert Object for headers of both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this header (comma separated strings) + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects DTO." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/HeaderDTO' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + Trend: + description: Evolution trend qualifier + enum: + - DOWN + - LOW_DOWN + - STABLE + - LOW_UP + - UP + type: string + WeightedMetricValue: + description: Value of a metric with an associated weight + required: + - name + - weight + - value + type: object + properties: + name: + description: Metric name or serie name + type: string + weight: + description: Weight of this metric value (typically a percentage) + type: integer + value: + description: The value of this metric + type: integer + DailyInvocationStatistic: + description: The daily statistic of a service mock invocations + required: + - id + - day + - serviceName + - serviceVersion + - dailyCount + type: object + properties: + id: + description: Unique identifier of this statistic object + type: string + day: + description: The day (formatted as yyyyMMdd string) represented by this + statistic + type: string + serviceName: + description: The name of the service this statistic is related to + type: string + serviceVersion: + description: The version of the service this statistic is related to + type: string + dailyCount: + description: The number of service mock invocations on this day + type: number + hourlyCount: + description: The number of service mock invocations per hour of the day + (keys range from 0 to 23) + type: object + additionalProperties: true + minuteCount: + description: The number of service mock invocations per minute of the day + (keys range from 0 to 1439) + type: object + additionalProperties: true + TestConformanceMetric: + description: "Represents the test conformance metrics (current score, history\ + \ and evolution trend) of a Service" + required: + - id + - serviceId + - currentScore + - maxPossibleScore + type: object + properties: + id: + description: Unique identifier of coverage metric + type: string + serviceId: + description: Unique identifier of the Service this metric is related to + type: string + aggregationLabelValue: + description: Value of the label used for metrics aggregation (if any) + type: string + maxPossibleScore: + format: double + description: Maximum conformance score that can be reached (depends on samples + expresiveness) + type: number + currentScore: + format: double + description: Current test conformance score for the related Service + type: number + lastUpdateDay: + description: The day of latest score update (in yyyyMMdd format) + type: string + latestTrend: + $ref: '#/components/schemas/Trend' + description: Evolution trend of currentScore + latestScores: + description: "History of latest scores (key is date with format yyyyMMdd,\ + \ value is score as double)" + type: object + additionalProperties: + type: number + CounterMap: + description: A generic map of counter + type: object + additionalProperties: + type: number + TestResultSummary: + description: 'Represents the summary result of a Service or API test run by + Microcks. ' + required: + - id + - testDate + - serviceId + - success + properties: + id: + description: Unique identifier of TestResult + type: string + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + serviceId: + description: Unique identifier of service tested + type: string + success: + description: Flag telling if test is a success + type: boolean + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testDate + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + - AMQP + - NATS + - GOOGLEPUBSUB + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + scopes: + user: Simple authenticated user + manager: Services & APIs content manager + admin: Administrator of the Microcks instance + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration +- name: metrics + description: Operations related to metrics diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.8.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.8.yaml new file mode 100644 index 000000000..20128f988 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.8.yaml @@ -0,0 +1,2176 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.8 + version: 1.8.1 + description: "API offered by Microcks, the Kubernetes native tool for API and microservices\ + \ mocking and testing (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/microcks + email: laurent@microcks.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + x-logo: + backgroundColor: '#ffffff' + url: https://microcks.io/images/microcks-logo-blue.png +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: Deals with artifacts to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + parameters: + - name: mainArtifact + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + schema: + type: boolean + in: query + required: true + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - user + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact to be imported by Microcks. + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true + /features/config: + summary: Optional features configuration + get: + tags: + - config + responses: + "200": + content: + application/json: {} + description: Get features configuration + operationId: GetFeaturesConfiguration + summary: Get features configuration + /import: + summary: Deals with repository snapshot to import in Microcks + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SnapshotUpload' + required: true + tags: + - mock + responses: + "201": + description: Snasphot has been correctly imported + security: + - jwt-bearer: + - admin + operationId: importSnapshot + summary: Import a snapshot + description: Import a repository snapshot previsouly exported into Microcks + /export: + summary: Deals with repository snapshot to import from Microcks + get: + tags: + - mock + parameters: + - name: serviceIds + description: List of service identifiers to export + schema: + type: array + items: + type: string + in: query + required: true + responses: + "200": + headers: + Content-Disposition: + schema: + type: string + examples: + filename: + value: attachment; filename=microcks-repository.json + content: + application/json: + schema: + format: binary + type: string + description: Snapshot file representing the export of requested services + security: + - jwt-bearer: + - admin + operationId: exportSnapshot + summary: Export a snapshot + description: Export a repostiory snapshot with requested services + /metrics/invocations/global: + summary: Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Aggregated invocation statistics for specified day + operationId: GetAggregatedInvocationsStats + summary: Get aggregated invocation statistics for a day + /metrics/conformance/aggregate: + summary: Aggregation of Test conformance metrics + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WeightedMetricValue' + description: Get aggregated coverage metric value + operationId: GetConformanceMetricsAggregation + summary: Get aggregation of conformance metrics + /metrics/conformance/service/{serviceId}: + summary: Test Conformance metrics on API and Services + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestConformanceMetric' + description: Test coverage metric for Service + operationId: GetServiceTestConformanceMetric + summary: Get conformance metrics for a Service + parameters: + - name: serviceId + description: Unique Services identifier this metrics are related to + schema: + type: string + in: path + required: true + /metrics/invocations/top: + summary: Top Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + - name: limit + description: The number of top invoked mocks to return + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Top invocations for a defined day + operationId: GetTopIvnocationsStatsByDay + summary: Get top invocation statistics for a day + /metrics/invocations/{serviceName}/{serviceVersion}: + summary: Invocation Statistics for API and Services + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Invocation statistics for service for specified day + operationId: GetInvocationStatsByService + summary: Get invocation statistics for Service + parameters: + - name: serviceName + description: Name of service to get statistics for + schema: + type: string + in: path + required: true + - name: serviceVersion + description: Version of service to get statistics for + schema: + type: string + in: path + required: true + /metrics/invocations/global/latest: + summary: Latest Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to get back in time. Default is 20. + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/CounterMap' + description: A map where keys are day (formatted using yyyyMMdd pattern) + and values are counter of invocations on this day + operationId: GetLatestAggregatedInvocationsStats + summary: Get aggregated invocations statistics for latest days + /metrics/tests/latest: + summary: Lastest Test results across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to consider for test results to return. Default + is 7 (one week) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResultSummary' + description: Test results summary for specified last days. + operationId: GetLatestTestResults + summary: Get latest tests results + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - manager + - admin + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - user + operationId: GetServicesLabels + summary: Get the already used labels for Services +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + - enabled + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: boolean + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + enabled: + description: Whether Keycloak authentification and usage is enabled + type: boolean + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: integer + lastUpdate: + description: Last update of attached object + type: integer + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + oAuth2Context: + $ref: '#/components/schemas/OAuth2ClientContent' + description: An OAuth2 context to use for retrieving an access token prior + invoking the tested endpoint + operationsHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relatvie path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + - GRAPHQL_SCHEMA + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ArtifactUpload: + description: |- + Artifact to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + description: A simple Counter type. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + FeaturesConfig: + title: Root Type for FeaturesConfig + description: Representation of optional features configuration used by Microcks + server + type: object + properties: + repository-filter: + description: Repository filtering feature properties + type: object + properties: + label-label: + type: string + enabled: + type: string + label-list: + type: string + label-key: + type: string + microcks-hub: + description: Microcks Hub feature properties + type: object + properties: + allowed-roles: + type: string + endpoint: + type: string + enabled: + type: string + repository-tenancy: + description: Repository tenancy feature properties + type: object + properties: + enabled: + type: string + artifact-import-allowed-roles: + type: string + async-api: + description: Asynchronous feature properties + type: object + properties: + default-binding: + type: string + endpoint-MQTT: + type: string + endpoint-WS: + type: string + endpoint-KAFKA: + type: string + endpoint-AMQP: + type: string + endpoint-NATS: + type: string + endpoint-GOOGLEPUBSUB: + type: string + enabled: + type: string + frequencies: + type: string + additionalProperties: true + example: + repository-filter: + label-label: Domain + enabled: "true" + label-list: "domain,status" + label-key: domain + microcks-hub: + allowed-roles: "admin,manager,manager-any" + endpoint: https://hub.microcks.io/api + enabled: "true" + repository-tenancy: + enabled: "false" + artifact-import-allowed-roles: "admin,manager,manager-any" + async-api: + default-binding: KAFKA + endpoint-MQTT: my-mqtt-broker.apps.try.microcks.io + endpoint-WS: localhost:8081 + endpoint-KAFKA: my-cluster-kafka-bootstrap.apps.try.microcks.io + enabled: "false" + frequencies: "3,10,30" + SnapshotUpload: + description: Upload of a repository snapshot file + required: + - file + type: object + properties: + file: + format: binary + description: The repository snapshot file + type: string + HeaderDTO: + description: Data Transfert Object for headers of both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this header (comma separated strings) + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects DTO." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/HeaderDTO' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + Trend: + description: Evolution trend qualifier + enum: + - DOWN + - LOW_DOWN + - STABLE + - LOW_UP + - UP + type: string + WeightedMetricValue: + description: Value of a metric with an associated weight + required: + - name + - weight + - value + type: object + properties: + name: + description: Metric name or serie name + type: string + weight: + description: Weight of this metric value (typically a percentage) + type: integer + value: + description: The value of this metric + type: integer + DailyInvocationStatistic: + description: The daily statistic of a service mock invocations + required: + - id + - day + - serviceName + - serviceVersion + - dailyCount + type: object + properties: + id: + description: Unique identifier of this statistic object + type: string + day: + description: The day (formatted as yyyyMMdd string) represented by this + statistic + type: string + serviceName: + description: The name of the service this statistic is related to + type: string + serviceVersion: + description: The version of the service this statistic is related to + type: string + dailyCount: + description: The number of service mock invocations on this day + type: number + hourlyCount: + description: The number of service mock invocations per hour of the day + (keys range from 0 to 23) + type: object + additionalProperties: true + minuteCount: + description: The number of service mock invocations per minute of the day + (keys range from 0 to 1439) + type: object + additionalProperties: true + TestConformanceMetric: + description: "Represents the test conformance metrics (current score, history\ + \ and evolution trend) of a Service" + required: + - id + - serviceId + - currentScore + - maxPossibleScore + type: object + properties: + id: + description: Unique identifier of coverage metric + type: string + serviceId: + description: Unique identifier of the Service this metric is related to + type: string + aggregationLabelValue: + description: Value of the label used for metrics aggregation (if any) + type: string + maxPossibleScore: + format: double + description: Maximum conformance score that can be reached (depends on samples + expresiveness) + type: number + currentScore: + format: double + description: Current test conformance score for the related Service + type: number + lastUpdateDay: + description: The day of latest score update (in yyyyMMdd format) + type: string + latestTrend: + $ref: '#/components/schemas/Trend' + description: Evolution trend of currentScore + latestScores: + description: "History of latest scores (key is date with format yyyyMMdd,\ + \ value is score as double)" + type: object + additionalProperties: + type: number + TestResultSummary: + description: 'Represents the summary result of a Service or API test run by + Microcks. ' + required: + - id + - testDate + - serviceId + - success + properties: + id: + description: Unique identifier of TestResult + type: string + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + serviceId: + description: Unique identifier of service tested + type: string + success: + description: Flag telling if test is a success + type: boolean + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testDate + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + - AMQP + - NATS + - GOOGLEPUBSUB + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + ResourceType: + description: Types of managed resources for Services or APIs + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + - GRAPHQL_SCHEMA + - POSTMAN_COLLECTION + type: string + CounterMap: + description: A generic map of counter + type: object + additionalProperties: + type: number + OAuth2ClientContent: + description: Represents a volatile OAuth2 client context usually associated + with a Test request + required: + - clientId + - clientSecret + - tokenUri + type: object + properties: + clientId: + description: Id for connecting to OAuth2 identity provider + type: string + clientSecret: + format: password + description: Secret for connecting to OAuth2 identity provider + type: string + tokenUri: + description: URI for retrieving an access token from OAuth2 identity provider + type: string + username: + description: Username in case you're using the Resource Owner Password flow + type: string + password: + description: User password in case you're suing the Resource Owner password + flow + type: string + refreshToken: + description: Refresh token in case you're using the Refresh Token rotation + flow + type: string + OAuth2GrantType: + description: Enumeration for the different supported grants/flows of OAuth2 + enum: + - PASSWORD + - CLIENT_CREDENTIALS + - REFRESH_TOKEN + type: string + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + scopes: + user: Simple authenticated user + manager: Services & APIs content manager + admin: Administrator of the Microcks instance + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration +- name: metrics + description: Operations related to metrics diff --git a/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.9.yaml b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.9.yaml new file mode 100644 index 000000000..3aae0455d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/api/microcks-openapi-v1.9.yaml @@ -0,0 +1,2207 @@ +--- +openapi: 3.0.2 +info: + title: Microcks API v1.9 + version: 1.9.0 + description: "API offered by Microcks, the Kubernetes native tool for API and microservices\ + \ mocking and testing (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/microcks + email: laurent@microcks.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + x-logo: + backgroundColor: '#ffffff' + url: https://microcks.io/images/microcks-logo-blue.png +servers: +- url: http://microcks.example.com/api + description: "" +paths: + /services: + summary: This path operations deal with Services + get: + tags: + - mock + parameters: + - name: page + description: Page of Services to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of Services to include in a response + (defaults to 20) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + description: List of found Services + security: + - jwt-bearer: + - user + operationId: GetServices + summary: Get Services and APIs + /tests: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TestRequest' + required: true + tags: + - test + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Created TestResult (empty shell cause tests are executed asynchronously) + security: + - jwt-bearer: + - user + operationId: CreateTest + summary: Create a new Test + /services/count: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Services in datastore + security: + - jwt-bearer: + - user + operationId: GetServicesCounter + summary: Get the Services counter + /jobs: + summary: This path operations deal with ImportJobs + get: + tags: + - job + parameters: + - name: page + description: Page of ImportJobs to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + - name: size + description: Size of a page. Maximum number of ImportJobs to include in a + response (defaults to 20) + schema: + type: integer + in: query + - name: name + description: Name like criterion for query + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ImportJob' + description: List of found ImportJobs + security: + - jwt-bearer: + - user + operationId: GetImportJobs + summary: Get ImportJobs + description: Retrieve a list of ImportJobs + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Created ImportJob + security: + - jwt-bearer: + - user + operationId: CreateImportJob + summary: Create ImportJob + description: Create a new ImportJob + /jobs/{id}: + summary: This path or subpaths operations deal with specific ImportJob having + given id + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Found ImportJob + security: + - jwt-bearer: + - user + summary: Get ImportJob + description: Retrieve an ImportJob using its identifier + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + required: true + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Updated ImportJob + summary: Update ImportJob + description: Update an ImportJob + delete: + tags: + - job + responses: + "200": + content: + application/json: {} + description: ImportJob deleted + security: + - jwt-bearer: + - admin + operationId: DeleteImportJob + summary: Delete ImportJob + description: Delete an ImportJob + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/{id}: + get: + tags: + - mock + parameters: + - name: messages + description: Whether to include details on services messages into result. + Default is false + schema: + type: boolean + in: query + responses: + "200": + $ref: '#/components/responses/ServiceResponse' + security: + - jwt-bearer: + - user + operationId: GetService + summary: Get Service + delete: + tags: + - mock + responses: + "200": + description: Service has been deleted + security: + - jwt-bearer: + - admin + - manager + operationId: DeleteService + summary: Delete Service + description: Delete a Service + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /artifact/upload: + summary: Deals with artifacts to be imported by Microcks. + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArtifactUpload' + examples: + Artifact upload: + value: | + POST /api/artifact/upload HTTP/1.1 + Host: microcks + User-Agent: curl/7.54.0 + Accept: */* + Authorization: Bearer + Content-Length: 2743 + Expect: 100-continue + Content-Type: multipart/form-data; boundary=------------------------8af8cbb56dd4bde0 + + --------------------------8af8cbb56dd4bde0 + Content-Disposition: form-data; name="file"; filename="github.json" + Content-Type: application/octet-stream + + THE ARTIFACT HERE + + --------------------------8af8cbb56dd4bde0-- + required: true + tags: + - job + parameters: + - name: mainArtifact + description: Flag telling if this should be considered as primary or secondary + artifact. Default to 'true' + schema: + type: boolean + in: query + required: true + responses: + "201": + content: + text/plain: + schema: + type: string + description: Artifact was imported and Service found + "204": + description: No file attribute found in uploaded data + "400": + content: + text/plain: + schema: + type: string + description: Artifact content is invalid and not understood + security: + - jwt-bearer: + - user + operationId: uploadArtifact + summary: Upload an artifact + description: Uploads an artifact to be imported by Microcks. + /jobs/count: + summary: Count ImportJobs + get: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of ImportJobs in datastore + security: + - jwt-bearer: + - user + operationId: GetImportJobCounter + summary: Get the ImportJobs counter + /secrets: + summary: This path operations deal with Secrets + get: + tags: + - config + parameters: + - name: page + description: Page of Secrets to retrieve (starts at and defaults to 0) + schema: + type: integer + in: query + required: false + - name: size + description: Size of a page. Maximum number of Secrets to include in a response + (defaults to 20) + schema: + type: integer + in: query + required: false + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: List of found Secrets + security: + - jwt-bearer: + - user + operationId: GetSecrets + summary: Get Secrets + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + tags: + - config + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Created Secret + security: + - jwt-bearer: + - admin + operationId: CreateSecret + summary: Create a new Secret + /secrets/{id}: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: Found Secret + security: + - jwt-bearer: + - admin + operationId: GetSecret + summary: Get Secret + description: Retrieve a Secret + put: + tags: + - config + responses: + "200": + description: Updated Secret + security: + - jwt-bearer: + - admin + operationId: UpdateSecret + summary: Update Secret + description: Update a Secret + delete: + tags: + - config + responses: + "200": + description: Secret has been deleted + security: + - jwt-bearer: + - admin + operationId: DeleteSecret + summary: Delete Secret + description: Delete a Secret + parameters: + - name: id + description: Unique identifier of Secret to manage + schema: + type: string + in: path + required: true + /secrets/count: + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of Secrets in datastore + security: + - jwt-bearer: + - user + operationId: GetSecretsCounter + summary: Get the Secrets counter + /tests/service/{serviceId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResult' + description: List of TestResults for the Service having the requested id + security: + - jwt-bearer: + - user + operationId: GetTestResultsByService + summary: Get TestResults by Service + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/service/{serviceId}/count: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Counter' + description: Number of TestResults for this Service in datastore + security: + - jwt-bearer: + - user + operationId: GetTestResultsByServiceCounter + summary: Get the TestResults for Service counter + parameters: + - name: serviceId + description: Unique identifier of Service to manage TestResults for + schema: + type: string + in: path + required: true + /tests/{id}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestResult' + description: Requested TestResult + security: + - jwt-bearer: + - user + operationId: GetTestResult + summary: Get TestResult + description: "" + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/messages/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestResponsePair' + description: List of request and response messages for this TestCase + security: + - jwt-bearer: + - user + operationId: GetMessagesByTestCase + summary: Get messages for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /tests/{id}/testCaseResult: + post: + requestBody: + description: TestCase return wrapper object + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseReturnDTO' + required: true + tags: + - test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestCaseResult' + description: TestCaseResult is reported + operationId: ReportTestCaseResult + summary: Report and create a new TestCaseResult + description: Report a TestCaseResult (typically used by a Test runner) + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + /keycloak/config: + summary: Keycloak Authentification configuration + get: + tags: + - config + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/KeycloakConfig' + description: Get current configuration + operationId: GetKeycloakConfig + summary: Get authentification configuration + /services/{id}/operation: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationOverrideDTO' + required: true + tags: + - mock + parameters: + - name: operationName + description: Name of operation to update + schema: + type: string + in: query + required: true + responses: + "200": + description: Operation has been updated + "500": + description: Operation cannot be updated + security: + - jwt-bearer: + - admin + - manager + operationId: OverrideServiceOperation + summary: Override Service Operation + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/{id}/metadata: + put: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + required: true + tags: + - mock + responses: + "200": + description: Service metadata has been updated + "500": + description: Update of metadata failed + security: + - jwt-bearer: + - admin + - manager + operationId: UpdateServiceMetadata + summary: Update Service Metadata + parameters: + - name: id + description: Unique identifier of Service to managed + schema: + type: string + in: path + required: true + /services/search: + get: + tags: + - mock + parameters: + - name: queryMap + description: Map of criterion. Key can be simply 'name' with value as the + searched string. You can also search by label using keys like 'labels.x' + where 'x' is the label and value the label value + schema: + type: object + additionalProperties: + type: string + in: query + required: true + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + description: List of found Services (filtered according search criteria) + security: + - jwt-bearer: + - user + operationId: SearchServices + summary: Search for Services and APIs + /tests/{id}/events/{testCaseId}: + get: + tags: + - test + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnidirectionalEvent' + description: List of event messages for this TestCase + operationId: GetEventsByTestCase + summary: Get events for TestCase + parameters: + - name: id + description: Unique identifier of TestResult to manage + schema: + type: string + in: path + required: true + - name: testCaseId + description: Unique identifier of TetsCaseResult to manage + schema: + type: string + in: path + required: true + /resources/{name}: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: Retrieve the resource having this name + security: + - jwt-bearer: + - user + operationId: GetResource + summary: Get Resource + parameters: + - name: name + description: Unique name/business identifier of the Service or API resource + schema: + type: string + in: path + required: true + /resources/service/{serviceId}: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Resource' + description: List the resources attached to a Service or API + security: + - jwt-bearer: + - user + operationId: GetResourcesByService + summary: Get Resources by Service + parameters: + - name: serviceId + description: Unique identifier of the Service or API the resources are attached + to + schema: + type: string + in: path + required: true + /features/config: + summary: Optional features configuration + get: + tags: + - config + responses: + "200": + content: + application/json: {} + description: Get features configuration + operationId: GetFeaturesConfiguration + summary: Get features configuration + /import: + summary: Deals with repository snapshot to import in Microcks + post: + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SnapshotUpload' + required: true + tags: + - mock + responses: + "201": + description: Snasphot has been correctly imported + security: + - jwt-bearer: + - admin + operationId: importSnapshot + summary: Import a snapshot + description: Import a repository snapshot previsouly exported into Microcks + /export: + summary: Deals with repository snapshot to import from Microcks + get: + tags: + - mock + parameters: + - name: serviceIds + description: List of service identifiers to export + schema: + type: array + items: + type: string + in: query + required: true + responses: + "200": + headers: + Content-Disposition: + schema: + type: string + examples: + filename: + value: attachment; filename=microcks-repository.json + content: + application/json: + schema: + format: binary + type: string + description: Snapshot file representing the export of requested services + security: + - jwt-bearer: + - admin + operationId: exportSnapshot + summary: Export a snapshot + description: Export a repostiory snapshot with requested services + /metrics/invocations/global: + summary: Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Aggregated invocation statistics for specified day + operationId: GetAggregatedInvocationsStats + summary: Get aggregated invocation statistics for a day + /metrics/conformance/aggregate: + summary: Aggregation of Test conformance metrics + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WeightedMetricValue' + description: Get aggregated coverage metric value + operationId: GetConformanceMetricsAggregation + summary: Get aggregation of conformance metrics + /metrics/conformance/service/{serviceId}: + summary: Test Conformance metrics on API and Services + get: + tags: + - metrics + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/TestConformanceMetric' + description: Test coverage metric for Service + operationId: GetServiceTestConformanceMetric + summary: Get conformance metrics for a Service + parameters: + - name: serviceId + description: Unique Services identifier this metrics are related to + schema: + type: string + in: path + required: true + /metrics/invocations/top: + summary: Top Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + - name: limit + description: The number of top invoked mocks to return + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Top invocations for a defined day + operationId: GetTopIvnocationsStatsByDay + summary: Get top invocation statistics for a day + /metrics/invocations/{serviceName}/{serviceVersion}: + summary: Invocation Statistics for API and Services + get: + tags: + - metrics + parameters: + - name: day + description: The day to get statistics for (formatted with yyyyMMdd pattern). + Default to today if not provided. + schema: + type: string + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DailyInvocationStatistic' + description: Invocation statistics for service for specified day + operationId: GetInvocationStatsByService + summary: Get invocation statistics for Service + parameters: + - name: serviceName + description: Name of service to get statistics for + schema: + type: string + in: path + required: true + - name: serviceVersion + description: Version of service to get statistics for + schema: + type: string + in: path + required: true + /metrics/invocations/global/latest: + summary: Latest Invocation Statistics across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to get back in time. Default is 20. + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/CounterMap' + description: A map where keys are day (formatted using yyyyMMdd pattern) + and values are counter of invocations on this day + operationId: GetLatestAggregatedInvocationsStats + summary: Get aggregated invocations statistics for latest days + /metrics/tests/latest: + summary: Lastest Test results across Services and APIs + get: + tags: + - metrics + parameters: + - name: limit + description: Number of days to consider for test results to return. Default + is 7 (one week) + schema: + type: integer + in: query + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TestResultSummary' + description: Test results summary for specified last days. + operationId: GetLatestTestResults + summary: Get latest tests results + /jobs/{id}/start: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Started ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StartImportJob + summary: Start an ImportJob + description: Starting an ImportJob forces it to immediatly import mock definitions + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/stop: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: Stopped ImportJob + security: + - jwt-bearer: + - manager + - admin + operationId: StopImportJob + summary: Stop an ImportJob + description: "Stopping an ImportJob desactivate it, so that it won't execute\ + \ at next schedule" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /jobs/{id}/activate: + put: + tags: + - job + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportJob' + description: ImportJob is activated + security: + - jwt-bearer: + - manager + - admin + operationId: ActivateImportJob + summary: Activate an ImportJob + description: "Make an ImportJob active, so that it is executed" + parameters: + - name: id + description: Unique identifier of ImportJob to manage + schema: + type: string + in: path + required: true + /services/labels: + get: + tags: + - mock + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/LabelsMap' + description: "Already used labels: keys are label Keys, values are array\ + \ of label Values" + security: + - jwt-bearer: + - user + operationId: GetServicesLabels + summary: Get the already used labels for Services +components: + schemas: + TestCaseResult: + description: Companion objects for TestResult. Each TestCaseResult correspond + to a particuliar service operation / action reference by the operationName + field. TestCaseResults owns a collection of TestStepResults (one for every + request associated to service operation / action). + required: + - success + - elapsedTime + - operationName + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test case beginning + type: number + operationName: + description: Name of operation this test case is bound to + type: string + testStepResults: + description: Test steps associated to this test case + type: array + items: + $ref: '#/components/schemas/TestStepResult' + ServiceRef: + description: Lightweight reference of an existing Service + required: + - serviceId + - name + - version + properties: + serviceId: + description: Unique reference of a Service + type: string + name: + description: The Service name + type: string + version: + description: The Service version + type: string + SecretRef: + description: Lightweight reference for an existing Secret + required: + - secretId + - name + properties: + secretId: + description: Unique identifier or referenced Secret + type: string + name: + description: Distinct name of the referenced Secret + type: string + example: |- + { + "secretId": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal" + } + Secret: + description: A Secret allows grouping informations on how to access a restricted + resource such as a repsoitory URL. Secrets are typically used by ImpoortJobs. + required: + - name + - description + properties: + id: + description: Unique identifier of Secret + type: string + name: + description: Unique distinct name of Secret + type: string + username: + type: string + password: + type: string + token: + type: string + tokenHeader: + type: string + caCertPem: + type: string + description: + description: Description of this Secret + type: string + example: |- + { + "id": "5be58fb51ed744d1b87481bd", + "name": "Gogs internal", + "description": "Gogs internal corporate repository", + "username": "team", + "password": "team", + "caCertPem": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu\nc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIwHhcNMTgwNzE4MTA0ODIyWhcNMjMwNzE3\nMTA0ODIzWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE1MzE5MTA5MDIw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyaP1jlpnm8WpfCSnUa8Qt\nhdUynOLLgElUtpWoW25wB9tO2ZmEj+fVsTyzEsW8+nfXXfRsBEzPm2ze9uEMTPTB\nAY0k7DbLZfjmF1lCckUvvh1rR/8hoPuXETjXUuOdtm7gRHTOxLQyH2Qi/q0DYJAn\nprKyRCLa35pRnykL6v4bHkqFnqDEho63i29XHhm2703moh4YCl1iYa2Rh6D44cjn\n8lBCq6o7zoZSmc/aBamRkQrfZYcolR8OUtDS4oEB0zMftmea2ycashsLEMB+Cq4r\n64NI2QM7qOxdTuXsDivHfLl7RTuWEOozGaJXoiPaGU/3d/KnY0gKJ2TC1KXt6Xjn\nAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCeUmxfUzw0VAFG6HvVYIsfvumiIvsSWmclGIZqNJyfMHFD\nMy6xzPRNNfWe5aumGTJsuIzuXfDRcdO7KmH1d2/5brkvWpxp6svVrYPvcoXjl4VN\nQR2mv5Di4KHfiiwvP3eeewjUZj+uREGqX2fcbJPHTPy32Kpb0H8Uy09BklhjC7QP\ngRAGexPhU1oBL/CoOwbHKcQ6dxs/y1SxzI8gXEtec4z62CroI13iT7U0UjSqFBE4\nKfrJombfz0d68781Z40ll+8my251ZNfbLBhQ3UHW0JnkBEQkE1aBorUoj2iakYvx\nA2qZh+8q2b8MwMb2YsQ0dlxKd6c4tN3lmMnO4bnd\n-----END CERTIFICATE-----" + } + TestCaseReturnDTO: + required: + - operationName + properties: + operationName: + description: Name of related operation for this TestCase + type: string + TestReturn: + description: TestReturn is used for wrapping the return code of a test step + execution + required: + - code + - elapsedTime + properties: + code: + description: "Return code for test (0 means Success, 1 means Failure)" + type: integer + elapsedTime: + format: int64 + description: Elapsed time in milliseconds + type: integer + message: + description: Error message if any + type: string + request: + $ref: '#/components/schemas/Request' + description: Request sent for this test + response: + $ref: '#/components/schemas/Response' + description: Response returned for this test + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Event Message received for this test + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + TestStepResult: + description: TestStepResult is an entity embedded within TestCaseResult. They + are created for each request associated with an operation / action of a microservice. + required: + - success + - elapsedTome + properties: + success: + description: Flag telling if test case is a success + type: boolean + elapsedTime: + description: Elapsed time in milliseconds since the test step beginning + type: number + requestName: + description: Name of request this test step is bound to + type: string + message: + description: Error message that may be associated to this test step + type: string + eventMessageName: + description: Name of event this test step is bound to + type: string + KeycloakConfig: + description: Representation of Keycloak / SSO configuration used by Microcks + server + required: + - realm + - auth-server-url + - public-client + - ssl-required + - resource + - enabled + type: object + properties: + realm: + description: Authentication realm name + type: string + auth-server-url: + description: SSO Server authentication url + type: string + public-client: + description: Name of public-client that can be used for requesting OAuth + token + type: boolean + ssl-required: + description: SSL certificates requirements + enum: + - none + - external + resource: + description: Name of Keycloak resource/application used on client side + type: string + enabled: + description: Whether Keycloak authentification and usage is enabled + type: boolean + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - required: + - request + - response + type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + - $ref: '#/components/schemas/AbstractExchange' + OperationOverrideDTO: + description: Data Transfer object for grouping the mutable properties of an + Operation + type: object + properties: + dispatcher: + description: Type of dispatcher to apply for this operation + type: string + dispatcherRules: + description: Rules of dispatcher for this operation + type: string + defaultDelay: + description: Default delay in milliseconds to apply to mock responses on + this operation + type: integer + parameterConstraints: + description: Constraints that may apply to incoming parameters on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + required: + - name + type: object + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + enum: + - path + - query + - header + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + required: + - createdOn + - lastUpdate + type: object + properties: + createdOn: + description: Creation date of attached object + type: number + readOnly: true + lastUpdate: + description: Last update of attached object + type: number + readOnly: true + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + LabelsMap: + description: A map which keys are already used labels keys and values are already + used values for this key + type: object + additionalProperties: + $ref: '#/components/schemas/StringArray' + type: object + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - required: + - eventMessage + type: object + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + EventMessage: + description: "" + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' + StringArray: + description: A simple array of String + type: array + items: + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: + propertyName: type + mapping: + reqRespPair: '#/components/schemas/RequestResponsePair' + unidirEvent: '#/components/schemas/UnidirectionalEvent' + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + required: + - type + type: object + properties: + type: + description: Discriminant type for identifying kind of exchange + enum: + - reqRespPair + - unidirEvent + type: string + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + required: + - service + - messagesMap + type: object + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + type: object + additionalProperties: + $ref: '#/components/schemas/MessageArray' + TestRequest: + description: Test request is a minimalist wrapper for requesting the launch + of a new test + required: + - serviceId + - testEndpoint + - runnerType + - timeout + properties: + serviceId: + description: Unique identifier of service to test + type: string + testEndpoint: + description: Endpoint to test for this service + type: string + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + filteredOperations: + description: A restriction on service operations to test + type: array + items: + type: string + secretName: + description: The name of Secret to use for connecting the test endpoint + type: string + oAuth2Context: + $ref: '#/components/schemas/OAuth2ClientContent' + description: An OAuth2 context to use for retrieving an access token prior + invoking the tested endpoint + operationsHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + Resource: + description: "Resource represents a Service or API artifacts such as specification,\ + \ contract" + required: + - id + - name + - content + - type + - serviceId + type: object + properties: + id: + description: Uniquer identifier of this Service or API Resource + type: string + name: + description: Unique name/business identifier for this Service or API resource + type: string + content: + description: String content of this resource + type: string + type: + $ref: '#/components/schemas/ResourceType' + description: Type of this Service or API resource + serviceId: + description: Unique identifier of the Servoce or API this resource is attached + to + type: string + path: + description: Relative path of this resource regarding main resource + type: string + sourceArtifact: + description: Short name of the artifact this resource was extracted from + type: string + ImportJob: + description: An ImportJob allow defining a repository artifact to poll for discovering + Services and APIs mocks and tests + required: + - name + - repositoryUrl + properties: + id: + description: Unique identifier of ImportJob + type: string + name: + description: Unique distinct name of this ImportJob + type: string + repositoryUrl: + description: URL of mocks and tests repository artifact + type: string + repositoryDisableSSLValidation: + description: Whether to disable SSL certificate verification when checking + repository + type: boolean + frequency: + description: Reserved for future usage + type: string + createdDate: + format: date-time + description: Creation date for this ImportJob + type: string + lastImportDate: + format: date-time + description: Date last import was done + type: string + lastImportError: + description: Error message of last import (if any) + type: string + active: + description: Whether this ImportJob is active (ie. scheduled for execution) + type: boolean + etag: + description: Etag of repository URL during previous import. Is used for + not re-importing if no recent changes + type: string + serviceRefs: + description: References of Services discovered when checking repository + type: array + items: + $ref: '#/components/schemas/ServiceRef' + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of ImportJob + secretRef: + $ref: '#/components/schemas/SecretRef' + description: Reference of a Secret to used when checking repository + mainArtifact: + description: Flag telling if considered as primary or secondary artifact. + Default to `true` + type: boolean + TestRunnerType: + description: Type of test strategy (different strategies are implemented by + different runners) + enum: + - HTTP + - SOAP_HTTP + - SOAP_UI + - POSTMAN + - OPEN_API_SCHEMA + - ASYNC_API_SCHEMA + - GRPC_PROTOBUF + - GRAPHQL_SCHEMA + type: string + Operation: + description: An Operation of a Service or API + required: + - name + - method + type: object + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ArtifactUpload: + description: |- + Artifact to be imported by Microcks. + This structure represents a mime-multipart file upload (as specified here: https://swagger.io/docs/specification/describing-request-body/file-upload/) + required: + - file + type: object + properties: + file: + format: binary + description: The artifact to upload + type: string + Counter: + description: A simple Counter type. + type: object + properties: + counter: + format: int32 + description: Number of items in a resource collection + type: integer + example: |- + { + "counter": 12 + } + FeaturesConfig: + title: Root Type for FeaturesConfig + description: Representation of optional features configuration used by Microcks + server + type: object + properties: + repository-filter: + description: Repository filtering feature properties + type: object + properties: + label-label: + type: string + enabled: + type: string + label-list: + type: string + label-key: + type: string + microcks-hub: + description: Microcks Hub feature properties + type: object + properties: + allowed-roles: + type: string + endpoint: + type: string + enabled: + type: string + repository-tenancy: + description: Repository tenancy feature properties + type: object + properties: + enabled: + type: string + artifact-import-allowed-roles: + type: string + async-api: + description: Asynchronous feature properties + type: object + properties: + default-binding: + type: string + endpoint-MQTT: + type: string + endpoint-WS: + type: string + endpoint-KAFKA: + type: string + endpoint-AMQP: + type: string + endpoint-NATS: + type: string + endpoint-GOOGLEPUBSUB: + type: string + enabled: + type: string + frequencies: + type: string + additionalProperties: true + example: + repository-filter: + label-label: Domain + enabled: "true" + label-list: "domain,status" + label-key: domain + microcks-hub: + allowed-roles: "admin,manager,manager-any" + endpoint: https://hub.microcks.io/api + enabled: "true" + repository-tenancy: + enabled: "false" + artifact-import-allowed-roles: "admin,manager,manager-any" + async-api: + default-binding: KAFKA + endpoint-MQTT: my-mqtt-broker.apps.try.microcks.io + endpoint-WS: localhost:8081 + endpoint-KAFKA: my-cluster-kafka-bootstrap.apps.try.microcks.io + enabled: "false" + frequencies: "3,10,30" + SnapshotUpload: + description: Upload of a repository snapshot file + required: + - file + type: object + properties: + file: + format: binary + description: The repository snapshot file + type: string + HeaderDTO: + description: Data Transfert Object for headers of both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this header (comma separated strings) + type: string + OperationHeaders: + description: "Specification of additional headers for a Service/API operations.\ + \ Keys are operation name or \"globals\" (if header applies to all), values\ + \ are Header objects DTO." + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/HeaderDTO' + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + - sourceArtifact + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + type: string + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + sourceArtifact: + description: Short name of the main/primary artifact this service was created + from + type: string + Trend: + description: Evolution trend qualifier + enum: + - DOWN + - LOW_DOWN + - STABLE + - LOW_UP + - UP + type: string + WeightedMetricValue: + description: Value of a metric with an associated weight + required: + - name + - weight + - value + type: object + properties: + name: + description: Metric name or serie name + type: string + weight: + description: Weight of this metric value (typically a percentage) + type: integer + value: + description: The value of this metric + type: integer + DailyInvocationStatistic: + description: The daily statistic of a service mock invocations + required: + - id + - day + - serviceName + - serviceVersion + - dailyCount + type: object + properties: + id: + description: Unique identifier of this statistic object + type: string + day: + description: The day (formatted as yyyyMMdd string) represented by this + statistic + type: string + serviceName: + description: The name of the service this statistic is related to + type: string + serviceVersion: + description: The version of the service this statistic is related to + type: string + dailyCount: + description: The number of service mock invocations on this day + type: number + hourlyCount: + description: The number of service mock invocations per hour of the day + (keys range from 0 to 23) + type: object + additionalProperties: true + minuteCount: + description: The number of service mock invocations per minute of the day + (keys range from 0 to 1439) + type: object + additionalProperties: true + TestConformanceMetric: + description: "Represents the test conformance metrics (current score, history\ + \ and evolution trend) of a Service" + required: + - id + - serviceId + - currentScore + - maxPossibleScore + type: object + properties: + id: + description: Unique identifier of coverage metric + type: string + serviceId: + description: Unique identifier of the Service this metric is related to + type: string + aggregationLabelValue: + description: Value of the label used for metrics aggregation (if any) + type: string + maxPossibleScore: + format: double + description: Maximum conformance score that can be reached (depends on samples + expresiveness) + type: number + currentScore: + format: double + description: Current test conformance score for the related Service + type: number + lastUpdateDay: + description: The day of latest score update (in yyyyMMdd format) + type: string + latestTrend: + $ref: '#/components/schemas/Trend' + description: Evolution trend of currentScore + latestScores: + description: "History of latest scores (key is date with format yyyyMMdd,\ + \ value is score as double)" + type: object + additionalProperties: + type: number + TestResultSummary: + description: 'Represents the summary result of a Service or API test run by + Microcks. ' + required: + - id + - testDate + - serviceId + - success + properties: + id: + description: Unique identifier of TestResult + type: string + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + serviceId: + description: Unique identifier of service tested + type: string + success: + description: Flag telling if test is a success + type: boolean + TestResult: + description: "Represents the result of a Service or API test run by Microcks.\ + \ Tests are related to a service and made of multiple test cases corresponding\ + \ to each operations / actions composing service. Tests are run against a\ + \ specific endpoint named testedEndpoint. It holds global markers telling\ + \ if test still ran, is a success, how many times is has taken and so on ..." + required: + - id + - version + - testNumber + - testDate + - testedEndpoint + - serviceId + - success + - inProgress + - runnerType + properties: + id: + description: Unique identifier of TestResult + type: string + version: + description: Revision number of this test + type: number + testNumber: + description: Incremental number for tracking number of tests of a service + type: number + testDate: + format: int64 + description: Timestamp of creation date of this service + type: integer + testedEndpoint: + description: Endpoint used during test + type: string + serviceId: + description: Unique identifier of service tested + type: string + elapsedTime: + description: Elapsed time in milliseconds since test beginning + type: number + success: + description: Flag telling if test is a success + type: boolean + inProgress: + description: Flag telling is test is still in progress + type: boolean + runnerType: + $ref: '#/components/schemas/TestRunnerType' + description: Runner used for this test + testCaseResults: + description: TestCase results associated to this test + type: array + items: + $ref: '#/components/schemas/TestCaseResult' + secretRef: + $ref: '#/components/schemas/SecretRef' + description: The referrence of the Secret used for connecting to test endpoint + operationHeaders: + $ref: '#/components/schemas/OperationHeaders' + description: This test operations headers override + timeout: + description: The maximum time (in milliseconds) to wait for this test ends + type: integer + authorizedClient: + $ref: '#/components/schemas/OAuth2AuthorizedClient' + description: The OAuth2 authorized client that performed the test + Binding: + description: Protocol binding details for asynchronous operations + required: + - type + - destinationName + type: object + properties: + type: + description: Protocol binding identifier + enum: + - KAFKA + - MQTT + - WS + - AMQP + - NATS + - GOOGLEPUBSUB + type: string + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + ResourceType: + description: Types of managed resources for Services or APIs + enum: + - WSDL + - XSD + - JSON_SCHEMA + - OPEN_API_SPEC + - OPEN_API_SCHEMA + - ASYNC_API_SPEC + - ASYNC_API_SCHEMA + - AVRO_SCHEMA + - PROTOBUF_SCHEMA + - PROTOBUF_DESCRIPTION + - GRAPHQL_SCHEMA + - POSTMAN_COLLECTION + type: string + CounterMap: + description: A generic map of counter + type: object + additionalProperties: + type: number + OAuth2ClientContent: + description: Represents a volatile OAuth2 client context usually associated + with a Test request + required: + - clientId + - clientSecret + - tokenUri + type: object + properties: + clientId: + description: Id for connecting to OAuth2 identity provider + type: string + clientSecret: + format: password + description: Secret for connecting to OAuth2 identity provider + type: string + tokenUri: + description: URI for retrieving an access token from OAuth2 identity provider + type: string + username: + description: Username in case you're using the Resource Owner Password flow + type: string + password: + description: User password in case you're suing the Resource Owner password + flow + type: string + refreshToken: + description: Refresh token in case you're using the Refresh Token rotation + flow + type: string + OAuth2GrantType: + description: Enumeration for the different supported grants/flows of OAuth2 + enum: + - PASSWORD + - CLIENT_CREDENTIALS + - REFRESH_TOKEN + type: string + OAuth2AuthorizedClient: + description: OAuth2 authorized client that performed a test + required: + - grantType + - principalName + - tokenUri + type: object + properties: + grantType: + $ref: '#/components/schemas/OAuth2GrantType' + description: OAuth2 authorization flow/grant type applied + principalName: + description: Name of authorized principal (clientId or username in the case + of Password grant type) + type: string + tokenUri: + description: Identity Provider URI used for token retrieval + type: string + scopes: + description: Included scopes (separated using space) + type: string + responses: + ServiceResponse: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Service' + - $ref: '#/components/schemas/ServiceView' + description: "" + securitySchemes: + jwt-bearer: + flows: + clientCredentials: + tokenUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + refreshUrl: https://keycloak.example.com/realms/microcks/protocol/openid-connect/token + scopes: + user: Simple authenticated user + manager: Services & APIs content manager + admin: Administrator of the Microcks instance + type: oauth2 + description: JWT Bearer acquired using OAuth 2 Authentication flow or Direct + Access Grant +security: +- jwt-bearer: [] +tags: +- name: mock + description: Operations related to API and Services mocks +- name: test + description: Operations related to API and Services tests +- name: job + description: Operations related to Jobs for discovering mocks and tests +- name: config + description: Operations related to configuration +- name: metrics + description: Operations related to metrics diff --git a/jdk_21_maven/cs/rest-gui/microcks/benchmark/README.md b/jdk_21_maven/cs/rest-gui/microcks/benchmark/README.md new file mode 100644 index 000000000..47fb7e688 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/benchmark/README.md @@ -0,0 +1,109 @@ +# K6 load testing on Microcks instance + +This benchmark tool provides you an easy way of validating/sizing/evaluating changes on your Microcks instance. +It allows you to simulate Virtual Users on different usage scenarios and gather performance metrics of your instance. + +1. Start you Microck instance +2. Import MicrocksIO Samples APIs / Pastry API - 2.0 +3. Import MicrocksIO Samples APIs / Movie Graph API +4. Import MicrocksIO Samples APIs / HelloService Mock +5. Import any other mock if you want to have more APIs to browse + +## Settings that may impact your Microcks instance performance + +Please carefully consider those settings that may have a direct impact on your Microcks instance: + +* Instance sizing - when deployed on Kubernetes, check the `requests.cpu`, `limits.cpu`, `requests.memory` and `requests.memory` settings, +* Enabling/Disabling invocation statistics - by default, Microcks has a `mocks.enable-invocation-stats` configuration property that is set to `true`. +This can cause an overhead even if the processing is asynchronous, +* MongoDB database indexes - Microcks comes with no index by default, but we put some recommendations in [mongodb-indexes](./mongodb-indexes.md), +* Logging verbosity - moving from `DEBUG` to `INFO` or above levels may have a significant impact. + +## Required environment variables + +```sh +export MICROCKS_BASE_URL=http://172.31.243.54:8080 +export K6_VERSION=0.48.0 +export PROMETHEUS_RW_URL=http://172.31.243.54:9080/api/v1/write # Optional +``` + +If you're running Microcks locally (via docker-compose or other), you may use these ones: + +```sh +export MICROCKS_BASE_URL=http://host.docker.internal:8080 +export K6_VERSION=0.48.0 +``` + +## Simple script execution + +The command below launches a test script that last between 1 and 2 minutes depending on your instance: + +```sh +docker run --rm -i \ + -e BASE_URL=${MICROCKS_BASE_URL} \ + grafana/k6:${K6_VERSION} run \ + - < bench-microcks.js +``` + +Behind the scenes, it executes 4 different scenarios that simulates different activities: +* `browse` simulates Virtual Users that browse the Microcks API repository, +* `invokeRESTMocks` simulates VU/apps that invoke a bunch of mock REST endpoint of our **Pastry API - 2.0** sample, +* `invokeGraphQLMocks` simulates VU/apps that invoke a bunch of mock GraphQL endpoint of our **Movie Graph API** sample, +* `invokeSOAPMocks` simulates VU/apps that invoke a bunch of mock SOAP endpoint of our **HelloService Mock** sample. + + +## Override wait time & scenarios + +The `browse` scenario has a `WAIT_TIME` configuration that simulates pause time between user interaction. +The default value is `0.5` but you can customize it to suit your needs: + +```sh +docker run --rm -i \ + -e BASE_URL=${MICROCKS_BASE_URL} \ + -e WAIT_TIME=0.2 \ + grafana/k6:${K6_VERSION} run \ + - < bench-microcks.js +``` + +More over you can configure the number of Virtual Users and iteration for each scenario. That way, you can +use this benchmark to be truly representative to your API patrimony and expected load. Just edit the `bench-microcks.js` +and check the `scenarios` section: + +```json +[...] + scenarios: { + browse: { + [...] + }, + invokeRESTMocks: { + executor: 'per-vu-iterations', + exec: 'invokeRESTMocks', + vus: 40, + iterations: 200, + startTime: '5s', + maxDuration: '2m', + }, + invokeGraphQLMocks: { + [...] + }, + invokeSOAPMocks: { + [...] + }, + } +[...] +``` + +## Export metrics to prometheus endpoint + +The K6 scripts results may be exported to a running Prometheus instance like this: + +```sh +docker run --rm -i \ + -e K6_PROMETHEUS_RW_SERVER_URL=${PROMETHEUS_RW_URL} \ + -e K6_PROMETHEUS_RW_TREND_STATS="p(95),p(99),min,max" \ + -e BASE_URL=${MICROCKS_BASE_URL} \ + grafana/k6:${K6_VERSION} run \ + --tag testid=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -o experimental-prometheus-rw \ + - < bench-microcks.js +``` diff --git a/jdk_21_maven/cs/rest-gui/microcks/benchmark/bench-microcks.js b/jdk_21_maven/cs/rest-gui/microcks/benchmark/bench-microcks.js new file mode 100644 index 000000000..e8816588e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/benchmark/bench-microcks.js @@ -0,0 +1,54 @@ +import { browse, invokeRESTMocks, invokeGraphQLMocks, invokeSOAPMocks } from '../testsuite/commons.js'; + +// Define the options for your test +export let options = { + /* + stages: [ + { duration: '1m', target: 50 }, // Ramp up to 50 virtual users over 1 minute + { duration: '3m', target: 50 }, // Stay at 50 virtual users for 3 minutes + { duration: '30s', target: 0 } // Ramp down to 0 virtual users over 30 seconds + ], + */ + scenarios: { + browse: { + executor: 'constant-vus', + exec: 'browse', + vus: 20, + duration: '1m', + }, + invokeRESTMocks: { + executor: 'per-vu-iterations', + exec: 'invokeRESTMocks', + vus: 40, + iterations: 200, + startTime: '5s', + maxDuration: '2m', + }, + invokeGraphQLMocks: { + executor: 'per-vu-iterations', + exec: 'invokeGraphQLMocks', + vus: 20, + iterations: 100, + startTime: '10s', + maxDuration: '2m', + }, + invokeSOAPMocks: { + executor: 'per-vu-iterations', + exec: 'invokeSOAPMocks', + vus: 5, + iterations: 5, + startTime: '15s', + maxDuration: '2m', + } + } +}; + +// The default function runs all tests in sequence +export default function () { + invokeRESTMocks(); + invokeGraphQLMocks(); + invokeSOAPMocks(); + browse() + sleep(2); // pause between iterations +} +export { browse, invokeRESTMocks, invokeGraphQLMocks, invokeSOAPMocks }; diff --git a/jdk_21_maven/cs/rest-gui/microcks/benchmark/mongodb-indexes.md b/jdk_21_maven/cs/rest-gui/microcks/benchmark/mongodb-indexes.md new file mode 100644 index 000000000..e0fb8b8f2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/benchmark/mongodb-indexes.md @@ -0,0 +1,29 @@ +From a certain size of MongoDB, it can be interesting to add some indexes to your database. + +Here are below the indexes of interest with the commands on how to create them in your database: + +```shell +db.dailyStatistic.createIndex( {day: -1, serviceName: 1, serviceVersion: -1}, {name: "day-1_serviceName1_serviceVersion-1"} ); +db.eventMessage.createIndex( {operationId: -1}, {name: "operationId-1"} ); +db.eventMessage.createIndex( {testCaseId: -1}, {name: "testCaseId-1"} ); +db.request.createIndex( {operationId: -1}, {name: "operationId-1"} ); +db.request.createIndex( {testCaseId: -1}, {name: "testCaseId-1"} ); +db.response.createIndex( {operationId: -1}, {name: "operationId-1"} ); +db.response.createIndex( {testCaseId: -1}, {name: "testCaseId-1"} ); +db.testResult.createIndex( {serviceId: -1}, {name: "serviceId-1"} ); +db.testConformanceMetric.createIndex( {serviceId: -1}, {name: "serviceId-1"} ); +``` + +And how to drop them if you find they are not efficient enough: + +```shell +db.dailyStatistic.dropIndex("day-1_serviceName1_serviceVersion-1"); +db.eventMessage.dropIndex("operationId-1"); +db.eventMessage.dropIndex("testCaseId-1"); +db.request.dropIndex("operationId-1"); +db.request.dropIndex("testCaseId-1"); +db.response.dropIndex("operationId-1"); +db.response.dropIndex("testCaseId-1"); +db.testResult.dropIndex("serviceId-1"); +db.testConformanceMetric.dropIndex("serviceId-1"); +``` \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/commons/model/pom.xml new file mode 100644 index 000000000..5a7fac9bd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/pom.xml @@ -0,0 +1,79 @@ + + + + + + microcks + io.github.microcks + 1.12.2-SNAPSHOT + ../../pom.xml + + 4.0.0 + + Microcks Model + microcks-model + + + UTF-8 + ../.. + 21 + 5.10.2 + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + org.yaml + snakeyaml + + + + + com.fasterxml.jackson.core + jackson-annotations + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + -Xlint:deprecation + -Xlint:unchecked + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Binding.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Binding.java new file mode 100644 index 000000000..add16b416 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Binding.java @@ -0,0 +1,94 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Binding details for asynchronous operations. + * @author laurent + */ +public class Binding { + + private BindingType type; + private String keyType; + private String destinationType; + private String destinationName; + private String method; + private String qoS; + private boolean persistent; + + public Binding() { + } + + public Binding(BindingType type) { + this.type = type; + } + + public BindingType getType() { + return type; + } + + public void setType(BindingType type) { + this.type = type; + } + + public String getKeyType() { + return keyType; + } + + public void setKeyType(String keyType) { + this.keyType = keyType; + } + + public String getDestinationType() { + return destinationType; + } + + public void setDestinationType(String destinationType) { + this.destinationType = destinationType; + } + + public String getDestinationName() { + return destinationName; + } + + public void setDestinationName(String destinationName) { + this.destinationName = destinationName; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getQoS() { + return qoS; + } + + public void setQoS(String qoS) { + this.qoS = qoS; + } + + public boolean isPersistent() { + return persistent; + } + + public void setPersistent(boolean persistent) { + this.persistent = persistent; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/BindingType.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/BindingType.java new file mode 100644 index 000000000..140ae7627 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/BindingType.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Enumeration of types for asynchronous protocols bindings. + * @author laurent + */ +public enum BindingType { + KAFKA, + MQTT, + WS, + AMQP, + AMQP1, + NATS, + GOOGLEPUBSUB, + SQS, + SNS +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/DailyStatistic.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/DailyStatistic.java new file mode 100644 index 000000000..b115adb77 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/DailyStatistic.java @@ -0,0 +1,94 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +import java.util.HashMap; +import java.util.Map; + +/** + * Domain objects representing daily invocation stats of mocks served by Microcks. + * @author laurent + */ +public class DailyStatistic { + + @Id + private String id; + private String day; + private String serviceName; + private String serviceVersion; + private long dailyCount; + + private Map hourlyCount = new HashMap<>(); + private Map minuteCount = new HashMap<>(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDay() { + return day; + } + + public void setDay(String day) { + this.day = day; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getServiceVersion() { + return serviceVersion; + } + + public void setServiceVersion(String serviceVersion) { + this.serviceVersion = serviceVersion; + } + + public long getDailyCount() { + return dailyCount; + } + + public void setDailyCount(long dailyCount) { + this.dailyCount = dailyCount; + } + + public Map getHourlyCount() { + return hourlyCount; + } + + public void setHourlyCount(Map hourlyCount) { + this.hourlyCount = hourlyCount; + } + + public Map getMinuteCount() { + return minuteCount; + } + + public void setMinuteCount(Map minuteCount) { + this.minuteCount = minuteCount; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/EventMessage.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/EventMessage.java new file mode 100644 index 000000000..e60bcbb4b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/EventMessage.java @@ -0,0 +1,54 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +/** + * A simple event message published or sent in an asynchronous exchange. + * @author laurent + */ +public class EventMessage extends Message { + + @Id + private String id; + private String mediaType; + private String dispatchCriteria; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getMediaType() { + return mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public String getDispatchCriteria() { + return dispatchCriteria; + } + + public void setDispatchCriteria(String dispatchCriteria) { + this.dispatchCriteria = dispatchCriteria; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Exchange.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Exchange.java new file mode 100644 index 000000000..34776de8b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Exchange.java @@ -0,0 +1,29 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +@JsonSubTypes({ @JsonSubTypes.Type(value = RequestResponsePair.class, name = "reqRespPair"), + @JsonSubTypes.Type(value = UnidirectionalEvent.class, name = "unidirEvent") }) +/** + * Abstract bean representing a Service or API Exchange.. + * @author laurent + */ +public abstract class Exchange { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/GenericResource.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/GenericResource.java new file mode 100644 index 000000000..0069370e6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/GenericResource.java @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.domain; + +import org.bson.Document; +import org.springframework.data.annotation.Id; + +/** + * Domain class representing a GenericResource created for simple CRUD mocking. + * @author laurent + */ +public class GenericResource { + + @Id + private String id; + private String serviceId; + private Document payload; + private boolean reference = false; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public Document getPayload() { + return payload; + } + + public void setPayload(Document payload) { + this.payload = payload; + } + + public boolean isReference() { + return reference; + } + + public void setReference(boolean reference) { + this.reference = reference; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Header.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Header.java new file mode 100644 index 000000000..738345724 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Header.java @@ -0,0 +1,63 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.util.HashSet; +import java.util.Set; + +/** + * Companion objects for all messages implementations instances. Represent some transport headers for both requests and + * responses. + * @author laurent + */ +public class Header { + + private String name; + + private Set values = new HashSet<>(); + + /** + * Default empty constructor. + */ + public Header() { + } + + /** + * Build a new Header with a name and a set of values. + * @param name The header name + * @param values The set of values for this header + */ + public Header(String name, Set values) { + this.name = name; + this.values = values; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getValues() { + return values; + } + + public void setValues(Set values) { + this.values = values; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ImportJob.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ImportJob.java new file mode 100644 index 000000000..3c122d3a2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ImportJob.java @@ -0,0 +1,167 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +/** + * Domain object representing an import job within Microcks. Import jobs are responsible of periodically checking tests + * & mocks repository in order to update their definitions with Microcks own repository. They typically used the + * repositoryUrl attribute, associated with the etag marker in order to easily see if something has been updated. + * @author laurent + */ +public class ImportJob { + + @Id + private String id; + private String name; + private String repositoryUrl; + private boolean mainArtifact = true; + private boolean repositoryDisableSSLValidation = false; + private String frequency; + private Date createdDate; + private Date lastImportDate; + private String lastImportError; + private boolean active = false; + private String etag; + + private Metadata metadata; + private SecretRef secretRef; + private Set serviceRefs; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getRepositoryUrl() { + return repositoryUrl; + } + + public void setRepositoryUrl(String repositoryUrl) { + this.repositoryUrl = repositoryUrl; + } + + public boolean isMainArtifact() { + return mainArtifact; + } + + public void setMainArtifact(boolean mainArtifact) { + this.mainArtifact = mainArtifact; + } + + public boolean isRepositoryDisableSSLValidation() { + return repositoryDisableSSLValidation; + } + + public void setRepositoryDisableSSLValidation(boolean repositoryDisableSSLValidation) { + this.repositoryDisableSSLValidation = repositoryDisableSSLValidation; + } + + public String getFrequency() { + return frequency; + } + + public void setFrequency(String frequency) { + this.frequency = frequency; + } + + public Date getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Date createdDate) { + this.createdDate = createdDate; + } + + public Date getLastImportDate() { + return lastImportDate; + } + + public void setLastImportDate(Date lastImportDate) { + this.lastImportDate = lastImportDate; + } + + public String getLastImportError() { + return lastImportError; + } + + public void setLastImportError(String lastImportError) { + this.lastImportError = lastImportError; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public String getEtag() { + return etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + public Metadata getMetadata() { + return metadata; + } + + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } + + public SecretRef getSecretRef() { + return secretRef; + } + + public void setSecretRef(SecretRef secretRef) { + this.secretRef = secretRef; + } + + public Set getServiceRefs() { + return serviceRefs; + } + + public void setServiceRefs(Set serviceRefs) { + this.serviceRefs = serviceRefs; + } + + public void addServiceRef(ServiceRef serviceRef) { + if (this.serviceRefs == null) { + this.serviceRefs = new HashSet<>(); + } + serviceRefs.add(serviceRef); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Message.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Message.java new file mode 100644 index 000000000..1837e987c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Message.java @@ -0,0 +1,89 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.util.HashSet; +import java.util.Set; + +/** + * Base class holding common attributes for Request, Response and EventMessage domain objects. + * @author laurent + */ +public abstract class Message { + + private String name; + private String content; + private String operationId; + private String testCaseId; + private String sourceArtifact; + + private Set
headers; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getOperationId() { + return operationId; + } + + public void setOperationId(String operationId) { + this.operationId = operationId; + } + + public String getTestCaseId() { + return testCaseId; + } + + public void setTestCaseId(String testCaseId) { + this.testCaseId = testCaseId; + } + + public String getSourceArtifact() { + return sourceArtifact; + } + + public void setSourceArtifact(String sourceArtifact) { + this.sourceArtifact = sourceArtifact; + } + + public Set
getHeaders() { + return headers; + } + + public void setHeaders(Set
headers) { + this.headers = headers; + } + + public void addHeader(Header header) { + if (this.headers == null) { + this.headers = new HashSet<>(); + } + headers.add(header); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Metadata.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Metadata.java new file mode 100644 index 000000000..3bb4a006e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Metadata.java @@ -0,0 +1,105 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.util.*; + +/** + * This is a commodity object for holding metadata on any entity. This object is inspired by Kubernetes metadata. + * @author laurent + */ +public class Metadata { + + private Date createdOn = new Date(); + private Date lastUpdate = new Date(); + private Map annotations; + private Map labels; + + /** Mark the related object of this metadata as updated. */ + public void objectUpdated() { + lastUpdate = new Date(); + } + + /** @return The creation date of the related object. */ + public Date getCreatedOn() { + return createdOn; + } + + /** @return The last update date of the related object. */ + public Date getLastUpdate() { + return lastUpdate; + } + + /** @return An immutable version of annotations map. */ + public Map getAnnotations() { + if (annotations == null) { + return null; + } + return Collections.unmodifiableMap(annotations); + } + + /** + * Add a new annotation or update an existing one within this metadata. + * @param key The key of the annotation to add / update + * @param value The value of the annotation to add / update. + */ + public void setAnnotation(String key, String value) { + if (key != null && value != null) { + if (annotations == null) { + annotations = new HashMap<>(); + } + annotations.put(key, value); + } + } + + /** + * Override all the metadata annotations with new ones. + * @param annotations New annotations to set + */ + public void setAnnotations(Map annotations) { + this.annotations = annotations; + } + + /** @return An immutable version of labels map. */ + public Map getLabels() { + if (labels == null) { + return null; + } + return Collections.unmodifiableMap(labels); + } + + /** + * Add a new label or update an existing one within this metadata. + * @param key The key of the label to add / update + * @param value The value of the label to add / update. + */ + public void setLabel(String key, String value) { + if (key != null && value != null) { + if (labels == null) { + labels = new HashMap<>(); + } + labels.put(key, value); + } + } + + /** + * Override all the metadata labels with new ones. + * @param labels New labels to set + */ + public void setLabels(Map labels) { + this.labels = labels; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2AuthorizedClient.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2AuthorizedClient.java new file mode 100644 index 000000000..321a42a13 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2AuthorizedClient.java @@ -0,0 +1,88 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Transient; + +/** + * Represent persisted information for an OAuth2 authorization/authentication done before launching a test. + * @author laurent + */ +public class OAuth2AuthorizedClient { + + private OAuth2GrantType grantType; + private String principalName; + private String tokenUri; + private String scopes; + @Transient + private String encodedAccessToken; + + public OAuth2AuthorizedClient() { + } + + /** + * Build an OAuth2AuthorizedClient from required information including the volatile encodedAccessToken + * @param grantType OAuth2 authorization flow/grant type applied. + * @param principalName Name of authorized principal + * @param tokenUri IDP URI used for token retrieval + * @param scopes Included scopes (separated using space) + * @param encodedAccessToken THe volatile access token, encoded in base64 + */ + public OAuth2AuthorizedClient(OAuth2GrantType grantType, String principalName, String tokenUri, String scopes, + String encodedAccessToken) { + this.grantType = grantType; + this.principalName = principalName; + this.tokenUri = tokenUri; + this.scopes = scopes; + this.encodedAccessToken = encodedAccessToken; + } + + public OAuth2GrantType getGrantType() { + return grantType; + } + + public void setGrantType(OAuth2GrantType grantType) { + this.grantType = grantType; + } + + public String getPrincipalName() { + return principalName; + } + + public void setPrincipalName(String principalName) { + this.principalName = principalName; + } + + public String getTokenUri() { + return tokenUri; + } + + public void setTokenUri(String tokenUri) { + this.tokenUri = tokenUri; + } + + public String getScopes() { + return scopes; + } + + public void setScopes(String scopes) { + this.scopes = scopes; + } + + public String getEncodedAccessToken() { + return encodedAccessToken; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2ClientContext.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2ClientContext.java new file mode 100644 index 000000000..6aaa102c4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2ClientContext.java @@ -0,0 +1,98 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.io.Serializable; + +/** + * Represents a volatile OAuth2 client context usually associated with a Test request. + * @author laurent + */ +public class OAuth2ClientContext implements Serializable { + + private String clientId; + private String clientSecret; + private String tokenUri; + private String scopes; + private String username; + private String password; + private String refreshToken; + private OAuth2GrantType grantType; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getTokenUri() { + return tokenUri; + } + + public void setTokenUri(String tokenUri) { + this.tokenUri = tokenUri; + } + + public String getScopes() { + return scopes; + } + + public void setScopes(String scopes) { + this.scopes = scopes; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public OAuth2GrantType getGrantType() { + return grantType; + } + + public void setGrantType(OAuth2GrantType grantType) { + this.grantType = grantType; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2GrantType.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2GrantType.java new file mode 100644 index 000000000..7900872f5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OAuth2GrantType.java @@ -0,0 +1,26 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Enumeration for the different supported grants/flows of OAuth2. + * @author laurent + */ +public enum OAuth2GrantType { + PASSWORD, + CLIENT_CREDENTIALS, + REFRESH_TOKEN; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Operation.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Operation.java new file mode 100644 index 000000000..a4df31da4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Operation.java @@ -0,0 +1,163 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An Operation / action of a micro service. Holds information on messages constitution (inputName, outputName, + * bindings) and how dispatch request to them. + * @author laurent + */ +public class Operation { + + private String name; + private String method; + private String action; + private String inputName; + private String outputName; + private Map bindings; + + private boolean override = false; + private String dispatcher; + private String dispatcherRules; + private Long defaultDelay; + + private Set resourcePaths; + private Set parameterConstraints; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public String getInputName() { + return inputName; + } + + public void setInputName(String inputName) { + this.inputName = inputName; + } + + public String getOutputName() { + return outputName; + } + + public void setOutputName(String outputName) { + this.outputName = outputName; + } + + public Map getBindings() { + return bindings; + } + + public void setBindings(Map bindings) { + this.bindings = bindings; + } + + public void addBinding(String name, Binding binding) { + if (this.bindings == null) { + this.bindings = new HashMap<>(); + } + bindings.put(name, binding); + } + + public boolean hasOverride() { + return this.override; + } + + public void setOverride(boolean override) { + this.override = override; + } + + public String getDispatcher() { + return dispatcher; + } + + public void setDispatcher(String dispatcher) { + this.dispatcher = dispatcher; + } + + public String getDispatcherRules() { + return dispatcherRules; + } + + public void setDispatcherRules(String dispatcherRules) { + this.dispatcherRules = dispatcherRules; + } + + public Long getDefaultDelay() { + return defaultDelay; + } + + public void setDefaultDelay(Long defaultDelay) { + this.defaultDelay = defaultDelay; + } + + public Set getResourcePaths() { + return resourcePaths; + } + + public void setResourcePaths(Set resourcePaths) { + this.resourcePaths = resourcePaths; + } + + public void addResourcePath(String resourcePath) { + if (this.resourcePaths == null) { + this.resourcePaths = new HashSet<>(); + } + if (!this.resourcePaths.contains(resourcePath)) { + this.resourcePaths.add(resourcePath); + } + } + + public Set getParameterConstraints() { + return parameterConstraints; + } + + public void setParameterConstraints(Set parameterConstraints) { + this.parameterConstraints = parameterConstraints; + } + + public void addParameterConstraint(ParameterConstraint constraint) { + if (this.parameterConstraints == null) { + this.parameterConstraints = new HashSet<>(); + } + parameterConstraints.add(constraint); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OperationsHeaders.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OperationsHeaders.java new file mode 100644 index 000000000..1ee5ea70c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/OperationsHeaders.java @@ -0,0 +1,33 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.util.HashMap; +import java.util.Set; + +/** + * Specification of additional headers for a Service/API operations. Keys are operation name or "globals" (if header + * applies to all), values are Header objects. + * @author laurent + */ +public class OperationsHeaders extends HashMap> { + + public static final String GLOBALS = "globals"; + + public Set
getGlobals() { + return this.get(GLOBALS); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Parameter.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Parameter.java new file mode 100644 index 000000000..09ce0a043 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Parameter.java @@ -0,0 +1,42 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Companion objects for Request representing query parameter. + * @author laurent + */ +public class Parameter { + + private String name; + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ParameterConstraint.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ParameterConstraint.java new file mode 100644 index 000000000..a51ee00e7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ParameterConstraint.java @@ -0,0 +1,86 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.util.Objects; + +/** + * Companion object for Operation that may be used to express constraints on request parameters. + * @author laurent + */ +public class ParameterConstraint { + + private String name; + private ParameterLocation in; + private boolean required; + private boolean recopy; + private String mustMatchRegexp; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ParameterLocation getIn() { + return in; + } + + public void setIn(ParameterLocation in) { + this.in = in; + } + + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + + public boolean isRecopy() { + return recopy; + } + + public void setRecopy(boolean recopy) { + this.recopy = recopy; + } + + public String getMustMatchRegexp() { + return mustMatchRegexp; + } + + public void setMustMatchRegexp(String mustMatchRegexp) { + this.mustMatchRegexp = mustMatchRegexp; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ParameterConstraint that = (ParameterConstraint) o; + return Objects.equals(name, that.name) && in == that.in; + } + + @Override + public int hashCode() { + return Objects.hash(name, in); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ParameterLocation.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ParameterLocation.java new file mode 100644 index 000000000..3ef76f780 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ParameterLocation.java @@ -0,0 +1,27 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Enumeration of locations for operation parameters. + * @author laurent + */ +public enum ParameterLocation { + path, + query, + header, + cookie +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Request.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Request.java new file mode 100644 index 000000000..9bb49e228 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Request.java @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +import java.util.ArrayList; +import java.util.List; + +/** + * Domain object representing a Microservice operation / action invocation request. + * @author laurent + */ +public class Request extends Message { + + @Id + private String id; + private String responseId; + + private List queryParameters; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getResponseId() { + return responseId; + } + + public void setResponseId(String responseId) { + this.responseId = responseId; + } + + public List getQueryParameters() { + return queryParameters; + } + + public void setQueryParameters(List queryParameters) { + this.queryParameters = queryParameters; + } + + public void addQueryParameter(Parameter parameter) { + if (this.queryParameters == null) { + this.queryParameters = new ArrayList<>(); + } + queryParameters.add(parameter); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/RequestResponsePair.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/RequestResponsePair.java new file mode 100644 index 000000000..c0c5c0557 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/RequestResponsePair.java @@ -0,0 +1,51 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Simple bean representing a request/response pair. + * @author laurent + */ +public class RequestResponsePair extends Exchange { + + private Request request; + private Response response; + + @JsonCreator + public RequestResponsePair(@JsonProperty("request") Request request, @JsonProperty("response") Response response) { + this.request = request; + this.response = response; + } + + public Request getRequest() { + return request; + } + + public void setRequest(Request request) { + this.request = request; + } + + public Response getResponse() { + return response; + } + + public void setResponse(Response response) { + this.response = response; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Resource.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Resource.java new file mode 100644 index 000000000..38eb40daa --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Resource.java @@ -0,0 +1,119 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +import java.util.HashSet; +import java.util.Set; + +/** + * Domain object representing a Resource: a contractualization companion to microservices Service managed with this + * application. These are typically retrieved using the serviceId. + * @author laurent + */ +public class Resource { + + @Id + private String id; + private String name; + private String path; + private String content; + private ResourceType type; + private String serviceId; + private String sourceArtifact; + private boolean mainArtifact = false; + private Set operations; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public ResourceType getType() { + return type; + } + + public void setType(ResourceType type) { + this.type = type; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getSourceArtifact() { + return sourceArtifact; + } + + public void setSourceArtifact(String sourceArtifact) { + this.sourceArtifact = sourceArtifact; + } + + public boolean isMainArtifact() { + return mainArtifact; + } + + public void setMainArtifact(boolean mainArtifact) { + this.mainArtifact = mainArtifact; + } + + public Set getOperations() { + return operations; + } + + public void setOperations(Set operations) { + this.operations = operations; + } + + public void addOperation(String operation) { + if (operations == null) { + operations = new HashSet<>(); + } + operations.add(operation); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ResourceType.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ResourceType.java new file mode 100644 index 000000000..ba34ce946 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ResourceType.java @@ -0,0 +1,39 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Enumeration of types of resources managed by microservices. + * @author laurent + */ +public enum ResourceType { + WSDL, + XSD, + JSON_SCHEMA, + SWAGGER, + RAML, + OPEN_API_SPEC, + OPEN_API_SCHEMA, + ASYNC_API_SPEC, + ASYNC_API_SCHEMA, + AVRO_SCHEMA, + PROTOBUF_SCHEMA, + PROTOBUF_DESCRIPTOR, + GRAPHQL_SCHEMA, + POSTMAN_COLLECTION, + SOAP_UI_PROJECT, + JSON_FRAGMENT +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Response.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Response.java new file mode 100644 index 000000000..547aef85b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Response.java @@ -0,0 +1,74 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +/** + * Domain object representing a microservice operation / rest action invocation response. Holds information wether it's + * a fault or not and on status code and mediaType to use for return. Responses are typically retrieved using + * dispatchCriteria extracted from a corresponding request message. + * @author laurent + */ +public class Response extends Message { + + @Id + private String id; + private String status; + private String mediaType; + private String dispatchCriteria; + private boolean isFault = false; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMediaType() { + return mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public String getDispatchCriteria() { + return dispatchCriteria; + } + + public void setDispatchCriteria(String dispatchCriteria) { + this.dispatchCriteria = dispatchCriteria; + } + + public boolean isFault() { + return isFault; + } + + public void setFault(boolean isFault) { + this.isFault = isFault; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Secret.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Secret.java new file mode 100644 index 000000000..2b40c575e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Secret.java @@ -0,0 +1,102 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +/** + * Domain class representing a Secret used for authenticating. + * @author laurent + */ +public class Secret { + + @Id + private String id; + private String name; + private String description; + + private String username; + private String password; + + private String token; + private String tokenHeader; + + private String caCertPem; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getTokenHeader() { + return tokenHeader; + } + + public void setTokenHeader(String tokenHeader) { + this.tokenHeader = tokenHeader; + } + + public String getCaCertPem() { + return caCertPem; + } + + public void setCaCertPem(String caCertPem) { + this.caCertPem = caCertPem; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/SecretRef.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/SecretRef.java new file mode 100644 index 000000000..95d32e493 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/SecretRef.java @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * A lightweight reference to a Secret object. + * @author laurent + */ +public class SecretRef { + + private String secretId; + private String name; + + public SecretRef() { + } + + public SecretRef(String secretId, String name) { + this.secretId = secretId; + this.name = name; + } + + public String getSecretId() { + return secretId; + } + + public void setSecretId(String secretId) { + this.secretId = secretId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Service.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Service.java new file mode 100644 index 000000000..87145f781 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Service.java @@ -0,0 +1,110 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +import java.util.ArrayList; +import java.util.List; + +/** + * Domain class representing a MicroService and the operations / actions it's holding. + * @author laurent + */ +public class Service { + + @Id + private String id; + private String name; + private String version; + private String xmlNS; + private ServiceType type; + private Metadata metadata; + private String sourceArtifact; + + private List operations = new ArrayList<>(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getXmlNS() { + return xmlNS; + } + + public void setXmlNS(String xmlNS) { + this.xmlNS = xmlNS; + } + + public ServiceType getType() { + return type; + } + + public void setType(ServiceType type) { + this.type = type; + } + + public String getSourceArtifact() { + return sourceArtifact; + } + + public void setSourceArtifact(String sourceArtifact) { + this.sourceArtifact = sourceArtifact; + } + + public List getOperations() { + return operations; + } + + public void setOperations(List operations) { + this.operations = operations; + } + + public void addOperation(Operation operation) { + if (this.operations == null) { + this.operations = new ArrayList<>(); + } + operations.add(operation); + } + + public Metadata getMetadata() { + return metadata; + } + + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceRef.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceRef.java new file mode 100644 index 000000000..f9ee99080 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceRef.java @@ -0,0 +1,60 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Representation of a reference to a Service. + * @author laurent + */ +public class ServiceRef { + + private String serviceId; + private String name; + private String version; + + public ServiceRef() { + } + + public ServiceRef(String serviceId, String name, String version) { + this.serviceId = serviceId; + this.name = name; + this.version = version; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceState.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceState.java new file mode 100644 index 000000000..91c82b460 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceState.java @@ -0,0 +1,93 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Date; + +/** + * An ephemeral Service state that can be used to create stateful mocks. + * @author laurent + */ +@Document +public class ServiceState { + + @Id + private String id; + private String serviceId; + private String key; + private String value; + @Indexed(expireAfterSeconds = 0) + private Date expireAt; + + + /** Build a ServiceState. */ + public ServiceState() { + } + + /** + * Build a ServiceState with required information. + * @param serviceId The unique identifier of Service this state relates to + * @param key The key a value will be stored for + */ + public ServiceState(String serviceId, String key) { + this.serviceId = serviceId; + this.key = key; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Date getExpireAt() { + return expireAt; + } + + public void setExpireAt(Date expireAt) { + this.expireAt = expireAt; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceType.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceType.java new file mode 100644 index 000000000..db8e30cd6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceType.java @@ -0,0 +1,30 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Types for managed Microservices. + * @author laurent + */ +public enum ServiceType { + SOAP_HTTP, + REST, + GENERIC_REST, + EVENT, + GENERIC_EVENT, + GRPC, + GRAPHQL +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceView.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceView.java new file mode 100644 index 000000000..e90d94a5a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/ServiceView.java @@ -0,0 +1,47 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * Aggregate bean for grouping a Service and its messages pairs. + * @author laurent + */ +public class ServiceView { + + private Service service; + private Map> messagesMap; + + @JsonCreator + public ServiceView(@JsonProperty("service") Service service, + @JsonProperty("messagesMap") Map> messagesMap) { + this.service = service; + this.messagesMap = messagesMap; + } + + public Service getService() { + return service; + } + + public Map> getMessagesMap() { + return messagesMap; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestCaseResult.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestCaseResult.java new file mode 100644 index 000000000..f8fe3cc91 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestCaseResult.java @@ -0,0 +1,66 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Companion objects for TestResult. Each TestCaseResult correspond to a particuliar service operation / action + * reference by the operationName field. TestCaseResults owns a collection of TestStepResults (one for every request + * associated to service operation / action). + * @author laurent + */ +public class TestCaseResult { + + private boolean success = false; + private long elapsedTime = -1; + private String operationName; + + private List testStepResults = new ArrayList<>(); + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public long getElapsedTime() { + return elapsedTime; + } + + public void setElapsedTime(long elapsedTime) { + this.elapsedTime = elapsedTime; + } + + public String getOperationName() { + return operationName; + } + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public List getTestStepResults() { + return testStepResults; + } + + public void setTestStepResults(List testStepResults) { + this.testStepResults = testStepResults; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestConformanceMetric.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestConformanceMetric.java new file mode 100644 index 000000000..b6ce47f04 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestConformanceMetric.java @@ -0,0 +1,105 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; + +import java.util.HashMap; +import java.util.Map; + +/** + * Domain object representing the test conformance metrics (current and configurable depth history) for a Service + * objects. If classification is allowed then aggregation can be realized using the {@code aggregationLabelValue} field. + * @author laurent + */ +public class TestConformanceMetric { + + @Id + private String id; + private String serviceId; + private String aggregationLabelValue; + + private double maxPossibleScore; + private double currentScore; + private String lastUpdateDay; + + private Trend latestTrend = Trend.STABLE; + private Map latestScores = new HashMap<>(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getAggregationLabelValue() { + return aggregationLabelValue; + } + + public void setAggregationLabelValue(String aggregationLabelValue) { + this.aggregationLabelValue = aggregationLabelValue; + } + + public double getMaxPossibleScore() { + return maxPossibleScore; + } + + public void setMaxPossibleScore(double maxPossibleScore) { + this.maxPossibleScore = maxPossibleScore; + } + + public double getCurrentScore() { + return currentScore; + } + + public void setCurrentScore(double currentScore) { + this.currentScore = currentScore; + } + + public String getLastUpdateDay() { + return lastUpdateDay; + } + + public void setLastUpdateDay(String lastUpdateDay) { + this.lastUpdateDay = lastUpdateDay; + } + + public Trend getLatestTrend() { + return latestTrend; + } + + public void setLatestTrend(Trend latestTrend) { + this.latestTrend = latestTrend; + } + + public Map getLatestScores() { + return latestScores; + } + + public void setLatestScores(Map latestScores) { + this.latestScores = latestScores; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestOptionals.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestOptionals.java new file mode 100644 index 000000000..19b6d55a1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestOptionals.java @@ -0,0 +1,84 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import java.util.List; + +/** + * Simple bean representing the optional elements of a Test requests. Some of them are made to be persisted into + * TestResult, some other are just volatile information for execution. + * @author laurent + */ +public class TestOptionals { + + private SecretRef secretRef; + private Long timeout; + private List filteredOperations; + private OperationsHeaders operationsHeaders; + private OAuth2ClientContext oAuth2Context; + + public TestOptionals() { + } + + public TestOptionals(SecretRef secretRef, Long timeout, List filteredOperations, + OperationsHeaders operationsHeaders, OAuth2ClientContext oAuth2Context) { + this.secretRef = secretRef; + this.timeout = timeout; + this.filteredOperations = filteredOperations; + this.operationsHeaders = operationsHeaders; + this.oAuth2Context = oAuth2Context; + } + + public SecretRef getSecretRef() { + return secretRef; + } + + public void setSecretRef(SecretRef secretRef) { + this.secretRef = secretRef; + } + + public Long getTimeout() { + return timeout; + } + + public void setTimeout(Long timeout) { + this.timeout = timeout; + } + + public List getFilteredOperations() { + return filteredOperations; + } + + public void setFilteredOperations(List filteredOperations) { + this.filteredOperations = filteredOperations; + } + + public OperationsHeaders getOperationsHeaders() { + return operationsHeaders; + } + + public void setOperationsHeaders(OperationsHeaders operationsHeaders) { + this.operationsHeaders = operationsHeaders; + } + + public OAuth2ClientContext getOAuth2Context() { + return oAuth2Context; + } + + public void setOAuth2Context(OAuth2ClientContext oAuth2Context) { + this.oAuth2Context = oAuth2Context; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestResult.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestResult.java new file mode 100644 index 000000000..55604057f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestResult.java @@ -0,0 +1,172 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Domain object representing the result of a microservice test run by Microcks. Test are related to a service and made + * of multiple test cases corresponding to every operations / actions composing service. Tests are run against a + * specific endpoint named testedEndpoint. It holds global markers telling if test still ran, is a success, how many + * times is has taken and so on ... + * @author laurent + */ +public class TestResult { + + @Id + private String id; + @Version + Long version; + private Long testNumber; + private Date testDate; + private String testedEndpoint; + private String serviceId; + private SecretRef secretRef; + private long timeout; + private long elapsedTime; + private boolean success = false; + private boolean inProgress = true; + private TestRunnerType runnerType; + private OperationsHeaders operationsHeaders; + private OAuth2AuthorizedClient authorizedClient; + + private List testCaseResults = new ArrayList<>(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Long getTestNumber() { + return testNumber; + } + + public void setTestNumber(Long testNumber) { + this.testNumber = testNumber; + } + + public Date getTestDate() { + return testDate; + } + + public void setTestDate(Date testDate) { + this.testDate = testDate; + } + + public String getTestedEndpoint() { + return testedEndpoint; + } + + public void setTestedEndpoint(String testedEndpoint) { + this.testedEndpoint = testedEndpoint; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public SecretRef getSecretRef() { + return secretRef; + } + + public void setSecretRef(SecretRef secretRef) { + this.secretRef = secretRef; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public long getElapsedTime() { + return elapsedTime; + } + + public void setElapsedTime(long elapsedTime) { + this.elapsedTime = elapsedTime; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public boolean isInProgress() { + return inProgress; + } + + public void setInProgress(boolean inProgress) { + this.inProgress = inProgress; + } + + public TestRunnerType getRunnerType() { + return runnerType; + } + + public void setRunnerType(TestRunnerType runnerType) { + this.runnerType = runnerType; + } + + public OperationsHeaders getOperationsHeaders() { + return operationsHeaders; + } + + public void setOperationsHeaders(OperationsHeaders operationsHeaders) { + this.operationsHeaders = operationsHeaders; + } + + public List getTestCaseResults() { + return testCaseResults; + } + + public void setTestCaseResults(List testCaseResults) { + this.testCaseResults = testCaseResults; + } + + public OAuth2AuthorizedClient getAuthorizedClient() { + return authorizedClient; + } + + public void setAuthorizedClient(OAuth2AuthorizedClient authorizedClient) { + this.authorizedClient = authorizedClient; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestReturn.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestReturn.java new file mode 100644 index 000000000..b41d8d58f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestReturn.java @@ -0,0 +1,152 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * A simple bean for wrapping a test exchange (whether request/response or async event) code, elapsed time and exchange + * content. + * @author laurent + */ +public class TestReturn { + + public static final int SUCCESS_CODE = 0; + public static final int FAILURE_CODE = 1; + + private int code; + private long elapsedTime; + private String message; + private Request request; + private Response response; + private EventMessage eventMessage; + + /** Default constructor for enabling bean serialization. */ + public TestReturn() { + } + + /** + * Build a TestReturn for event based exchange with its code. + * @param code The code (may be success of failure) + * @param elapsedTime Time taken for a test + * @param eventMessage The event message for this test + */ + public TestReturn(int code, long elapsedTime, EventMessage eventMessage) { + this.code = code; + this.elapsedTime = elapsedTime; + this.eventMessage = eventMessage; + } + + /** + * Build a TestReturn for event based exchange with its code. + * @param code The code (may be success of failure) + * @param elapsedTime Time taken for a test + * @param message The return message for this test + * @param eventMessage The event message for this test + */ + public TestReturn(int code, long elapsedTime, String message, EventMessage eventMessage) { + this.code = code; + this.elapsedTime = elapsedTime; + this.message = message; + this.eventMessage = eventMessage; + } + + /** + * Build a TestReturn with its code and response. + * @param code The code (may be success of failure) + * @param elapsedTime Time taken for a test + * @param request The request for this test + * @param response The response for this test + */ + public TestReturn(int code, long elapsedTime, Request request, Response response) { + this.code = code; + this.elapsedTime = elapsedTime; + this.request = request; + this.response = response; + } + + /** + * Build a TestReturn with its code and response. + * @param code The code (may be success of failure) + * @param elapsedTime Time taken for a test + * @param message The return message for this test + * @param request The request for this test + * @param response The response for this test + */ + public TestReturn(int code, long elapsedTime, String message, Request request, Response response) { + this.code = code; + this.elapsedTime = elapsedTime; + this.message = message; + this.request = request; + this.response = response; + } + + /** @return Return code */ + public int getCode() { + return code; + } + + /** @return Elapsed time */ + public long getElapsedTime() { + return elapsedTime; + } + + /** @return Test return message */ + public String getMessage() { + return message; + } + + /** @return Request content */ + public Request getRequest() { + return request; + } + + /** @return Response content */ + public Response getResponse() { + return response; + } + + /** @return EventMessage content */ + public EventMessage getEventMessage() { + return eventMessage; + } + + /** @return True is this test return is for a request/response exchange test */ + public boolean isRequestResponseTest() { + return request != null && response != null; + } + + /** @return True if this test return is for an asynchronous event test */ + public boolean isEventTest() { + return eventMessage != null; + } + + /** + * Build a TestStepResult from inner elements. + * @return A new TestStepResult ready to attached to a test case. + */ + public TestStepResult buildTestStepResult() { + TestStepResult result = new TestStepResult(); + result.setElapsedTime(elapsedTime); + result.setSuccess(code == SUCCESS_CODE); + result.setMessage(message); + if (request != null) { + result.setRequestName(request.getName()); + } + if (eventMessage != null) { + result.setEventMessageName(eventMessage.getName()); + } + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestRunnerType.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestRunnerType.java new file mode 100644 index 000000000..51465c7db --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestRunnerType.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Enumeration of runner types available in application. + * @author laurent + */ +public enum TestRunnerType { + HTTP, + SOAP_HTTP, + SOAP_UI, + POSTMAN, + OPEN_API_SCHEMA, + ASYNC_API_SCHEMA, + GRPC_PROTOBUF, + GRAPHQL_SCHEMA +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestStepResult.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestStepResult.java new file mode 100644 index 000000000..ed8f45bda --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/TestStepResult.java @@ -0,0 +1,70 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * TestStepResult is an entity embedded within TestCaseResult. They are created for each request or message associated + * with an operation / action of a microservice. + * @author laurent + */ +public class TestStepResult { + + private boolean success = false; + private long elapsedTime; + private String requestName; + private String eventMessageName; + private String message; + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public long getElapsedTime() { + return elapsedTime; + } + + public void setElapsedTime(long elapsedTime) { + this.elapsedTime = elapsedTime; + } + + public String getRequestName() { + return requestName; + } + + public void setRequestName(String requestName) { + this.requestName = requestName; + } + + public String getEventMessageName() { + return eventMessageName; + } + + public void setEventMessageName(String eventMessageName) { + this.eventMessageName = eventMessageName; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Trend.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Trend.java new file mode 100644 index 000000000..ae4261360 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/Trend.java @@ -0,0 +1,28 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * Enumeration type for evolution trend. + * @author laurent + */ +public enum Trend { + DOWN, + LOW_DOWN, + STABLE, + LOW_UP, + UP +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/UnidirectionalEvent.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/UnidirectionalEvent.java new file mode 100644 index 000000000..8fcafdb8b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/UnidirectionalEvent.java @@ -0,0 +1,41 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Simple bean representing an unidirectional exchange as an event message. + * @author laurent + */ +public class UnidirectionalEvent extends Exchange { + + private EventMessage eventMessage; + + @JsonCreator + public UnidirectionalEvent(@JsonProperty("eventMessage") EventMessage eventMessage) { + this.eventMessage = eventMessage; + } + + public EventMessage getEventMessage() { + return eventMessage; + } + + public void setEventMessage(EventMessage eventMessage) { + this.eventMessage = eventMessage; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/WeightedMetricValue.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/WeightedMetricValue.java new file mode 100644 index 000000000..30c5b8591 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/domain/WeightedMetricValue.java @@ -0,0 +1,51 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.domain; + +/** + * A metric value that has a weight (typically an aggregation result). + * @author laurent + */ +public class WeightedMetricValue { + + private String name; + private int weight; + private double value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public double getValue() { + return value; + } + + public void setValue(double value) { + this.value = value; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/event/ChangeType.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/event/ChangeType.java new file mode 100644 index 000000000..a782519d4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/event/ChangeType.java @@ -0,0 +1,26 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.event; + +/** + * Enumeration of types for domain objects changes. + * @author laurent + */ +public enum ChangeType { + CREATED, + UPDATED, + DELETED +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/event/ServiceViewChangeEvent.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/event/ServiceViewChangeEvent.java new file mode 100644 index 000000000..11ea045b2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/event/ServiceViewChangeEvent.java @@ -0,0 +1,59 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.event; + +import io.github.microcks.domain.ServiceView; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * This represents a domain event around ServiceView change. + * @author laurent + */ +public class ServiceViewChangeEvent { + + private String serviceId; + private ServiceView serviceView; + private ChangeType changeType; + private long timestamp; + + @JsonCreator + public ServiceViewChangeEvent(@JsonProperty("serviceId") String serviceId, + @JsonProperty("serviceView") ServiceView serviceView, @JsonProperty("changeType") ChangeType changeType, + @JsonProperty("timestamp") long timestamp) { + this.serviceId = serviceId; + this.serviceView = serviceView; + this.changeType = changeType; + this.timestamp = timestamp; + } + + public String getServiceId() { + return serviceId; + } + + public ServiceView getServiceView() { + return serviceView; + } + + public ChangeType getChangeType() { + return changeType; + } + + public long getTimestamp() { + return timestamp; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/util/IdBuilder.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/util/IdBuilder.java new file mode 100644 index 000000000..f6b94f2d7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/util/IdBuilder.java @@ -0,0 +1,87 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestResult; + +/** + * Helper class for building composite/aggregates keys or Ids. + * @author laurent + */ +public class IdBuilder { + + /** + * Private Constructor. So that the utility class cannot be instanced + */ + private IdBuilder() { + } + + /** + * Build a unique operation Id from service and operation. + * @param service The domain service holding operation + * @param operation A domain bean representing operation to build an id for + * @return A unique identifier for operation. + */ + public static String buildOperationId(Service service, Operation operation) { + return service.getId() + "-" + operation.getName(); + } + + /** + * Build a unique TestCase Id from test result and operation. + * @param testResult The domain testResult holding test case + * @param operation A domain bean representing operation matching case + * @return A unique identifier for test case. + */ + public static String buildTestCaseId(TestResult testResult, Operation operation) { + return testResult.getId() + "-" + testResult.getTestNumber() + "-" + operation.getName(); + } + + /** + * Build a unique TestCase Id from test result and operation. + * @param testResult The domain testResult holding test case + * @param operationName A string representing matching operation name case + * @return A unique identifier for test case. + */ + public static String buildTestCaseId(TestResult testResult, String operationName) { + return testResult.getId() + "-" + testResult.getTestNumber() + "-" + operationName; + } + + /** + * Build the full name of a Resource dedicated to no particular operations of a Service. Such Resource is typically a + * global Schema dependency that defines shared data types, so that you'll be able to easily retrieve it later. + * @param service The domain service owning this resource + * @param resourceName The name of resource + * @return A full name for this globally attached resource. + */ + public static String buildResourceFullName(Service service, String resourceName) { + return service.getName() + "-" + service.getVersion() + "-" + Sanitizer.urlSanitize(resourceName); + } + + /** + * Build the full name of a Resource dedicated to no particular operations of a Service. Such Resource is typically a + * global Schema dependency that defines shared data types, so that you'll be able to easily retrieve it later. + * @param service The domain service owning this resource + * @param resourceName The name of resource + * @param context The context this resource belongs to + * @return A full name for this globally attached resource. + */ + public static String buildResourceFullName(Service service, String resourceName, String context) { + return service.getName() + "-" + service.getVersion() + "-" + Sanitizer.urlSanitize(context.replace(".", "")) + + "-" + Sanitizer.urlSanitize(resourceName); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/util/Sanitizer.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/util/Sanitizer.java new file mode 100644 index 000000000..9943665e5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/main/java/io/github/microcks/util/Sanitizer.java @@ -0,0 +1,49 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Utility class for sanitizing strings to be used in a URL. + */ +public class Sanitizer { + private static final String ALLOWED_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$-_.+!*'(),"; + private static final Set ALLOWED_CHARS_SET = ALLOWED_CHARS.chars().mapToObj(c -> (char) c) + .collect(Collectors.toSet()); + private static final Character REPLACE_CHAR = '-'; + + private Sanitizer() { + // Hide the implicit constructor as it's a utility class. + } + + /** + * Sanitize a string to be used in a URL. It replaces all characters that are not in the set of allowed characters by + * a dash. + * @param string the string to sanitize + * @return the sanitized string + */ + public static String urlSanitize(String string) { + StringBuilder sanitized = new StringBuilder(); + + for (char c : string.toCharArray()) { + sanitized.append(ALLOWED_CHARS_SET.contains(c) ? c : REPLACE_CHAR); + } + + return sanitized.toString(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/test/java/io/github/microcks/util/SanitizerTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/test/java/io/github/microcks/util/SanitizerTest.java new file mode 100644 index 000000000..f7c2810c9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/model/src/test/java/io/github/microcks/util/SanitizerTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for the Sanitizer util. + * @author laurent + */ +class SanitizerTest { + + @Test + void testUrlSanitize() { + String url = Sanitizer.urlSanitize("weird|~url.json"); + assertEquals("weird--url.json", url); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/pom.xml new file mode 100644 index 000000000..9a96a4663 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/pom.xml @@ -0,0 +1,84 @@ + + + + + + microcks + io.github.microcks + 1.12.2-SNAPSHOT + ../../pom.xml + + 4.0.0 + + Microcks EL + microcks-el + + + UTF-8 + ../.. + 21 + 2.0.2 + 5.10.2 + + + + + com.fasterxml.jackson.core + jackson-databind + + + org.slf4j + slf4j-api + + + net.datafaker + datafaker + ${datafaker.version} + + + + + io.github.microcks + microcks-model + ${project.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + -Xlint:deprecation + -Xlint:unchecked + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/EvaluableRequest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/EvaluableRequest.java new file mode 100644 index 000000000..c258749ab --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/EvaluableRequest.java @@ -0,0 +1,67 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import java.util.Map; + +/** + * This is a simple bean wrapping request most common elements and adapted for evaluation within EL expressions. + * @author laurent + */ +public class EvaluableRequest { + + private String body; + private String[] path; + private Map params; + private Map headers; + + public EvaluableRequest(String body, String[] path) { + this.body = body; + this.path = path; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String[] getPath() { + return path; + } + + public void setPath(String[] path) { + this.path = path; + } + + public Map getParams() { + return params; + } + + public void setParams(Map params) { + this.params = params; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/EvaluationContext.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/EvaluationContext.java new file mode 100644 index 000000000..8d1597136 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/EvaluationContext.java @@ -0,0 +1,84 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import io.github.microcks.util.el.function.ELFunction; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A context used during evaluation of EL expressions. Can be used to register functions, store input variables or + * intermediary results. + * @author laurent + */ +public class EvaluationContext { + + private final Map variables = new ConcurrentHashMap<>(); + + /** + * Put a variable into this context + * @param name The name of variable + * @param value The variable itself + */ + public void setVariable(String name, Object value) { + variables.put(name, value); + } + + /** + * Put a set of variables into this context + * @param variables key/value pairs for variables names and their values + */ + public void setVariables(Map variables) { + variables.forEach(this::setVariable); + } + + /** + * Retrieve a registered variable by its name. + * @param name The name of variable to look for + * @return The variable having this name or null if no variable. + */ + public Object lookupVariable(String name) { + return variables.get(name); + } + + /** + * Register a function using a name and the ELFunction class. + * @param name The name of function to register + * @param function The class representing the function + * @param Any implementation of {@code ELFunction} interface + */ + public void registerFunction(String name, Class function) { + this.variables.put(name, function); + } + + /** + * Retrieve a registered function by its name. + * @param name The name of function to look for + * @param Any implementation of {@code ELFunction} interface + * @return The Function class object + */ + @SuppressWarnings("unchecked") + public Class lookupFunction(String name) { + Object function = variables.get(name); + if (function instanceof Class functionClazz) { + if (ELFunction.class.isAssignableFrom(functionClazz)) { + return (Class) functionClazz; + } + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/Expression.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/Expression.java new file mode 100644 index 000000000..176867e25 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/Expression.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +/** + * Simple interface representing an EL expression. Expression just render a value regarding a specific + * {@code EvaluationContext}. + * @author laurent + */ +public interface Expression { + + /** + * Render this expression value within this evaluation context. + * @param context The context of current evaluation + * @return This expression rendered value + */ + String getValue(EvaluationContext context); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/ExpressionParser.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/ExpressionParser.java new file mode 100644 index 000000000..c8d35688a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/ExpressionParser.java @@ -0,0 +1,235 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import io.github.microcks.util.el.function.ELFunction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Helper object for finding and parsing expressions present into a string template. For example, following template: + * {@code Hello {{ request.body/name }} it's {{ now() }}} should be decomposed into 4 expressions: + *
    + *
  • A LiteralExpression representing the "Hello " part
  • + *
  • A VariableReferenceExpression representing the "request.body/name part
  • + *
  • A LiteralExpression representing the " it's " part
  • + *
  • A ELFunctionExpression representing the "now()" part
  • + *
+ * @author laurent + */ +public class ExpressionParser { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ExpressionParser.class); + + // Private constructor to hide the implicit one. + private ExpressionParser() { + } + + /** + * Navigate the template for finding expressions that can be evaluated into this template. Expressions are returned + * into an ordered array. + * @param template The string to browse + * @param context The EvaluationContext that may contains variable or function references + * @param expressionPrefix The prefix starting new expression (ex: "{{") + * @param expressionSuffix The suffix closing expression (ex: "}}") + * @return The array of found expressions when browsing template from left to right + */ + public static Expression[] parseExpressions(String template, EvaluationContext context, String expressionPrefix, + String expressionSuffix) throws ParseException { + // Prepare an array for results. + List expressions = new ArrayList<>(); + int startIdx = 0; + + while (startIdx < template.length()) { + int prefixIndex = template.indexOf(expressionPrefix, startIdx); + if (prefixIndex >= startIdx) { + // an inner expression was found - this is a composite + if (prefixIndex > startIdx) { + log.debug("Found a literal expression starting at {}", startIdx); + expressions.add(new LiteralExpression(template.substring(startIdx, prefixIndex))); + } + int afterPrefixIndex = prefixIndex + expressionPrefix.length(); + int suffixIndex = skipToCorrectEndSuffix(expressionSuffix, template, afterPrefixIndex); + if (suffixIndex == -1) { + log.info("No ending suffix '{}' for expression starting at character {}: {}", expressionSuffix, + prefixIndex, template.substring(prefixIndex)); + throw new ParseException(template, prefixIndex, + "No ending suffix '" + expressionSuffix + "' for expression starting at character " + prefixIndex + + ": " + template.substring(prefixIndex)); + } + if (suffixIndex == afterPrefixIndex) { + log.info("No expression defined within delimiter '{}' at character {}", expressionPrefix, prefixIndex); + throw new ParseException(template, prefixIndex, "No expression defined within delimiter '" + + expressionPrefix + expressionSuffix + "' at character " + prefixIndex); + } + String expr = template.substring(prefixIndex + expressionPrefix.length(), suffixIndex); + expr = expr.trim(); + if (expr.isEmpty()) { + log.info("No expression defined within delimiter '{}' at character {}", expressionPrefix, prefixIndex); + throw new ParseException(template, prefixIndex, "No expression defined within delimiter '" + + expressionPrefix + expressionSuffix + "' at character " + prefixIndex); + } + if (expr.charAt(0) == '{') { + expressions.add(new LiteralExpression(expr.substring(0, 1))); + expressions.add(doParseExpression(expr.substring(1, expr.length() - 1), context)); + expressions.add(new LiteralExpression(expr.substring(expr.length() - 1))); + } else + expressions.add(doParseExpression(expr, context)); + startIdx = suffixIndex + expressionSuffix.length(); + log.debug("Expression accumulated. Pursuing with index {} on {}", startIdx, template.length()); + } else { + // no more expression. finalize with a literal. + expressions.add(new LiteralExpression(template.substring(startIdx, template.length()))); + break; + } + } + return expressions.toArray(new Expression[0]); + } + + /** Find for next suitable correct end suffix. Could be extended in future to manager recursivity... */ + private static int skipToCorrectEndSuffix(String expressionSuffix, String template, int afterPrefixIndex) { + int nextSuffix = template.indexOf(expressionSuffix, afterPrefixIndex); + if (nextSuffix == -1) { + return -1; // the suffix is missing + } + + // Check if there are more closing curly braces after the found "}}" + int lastIndexOfSuffix = nextSuffix + expressionSuffix.length(); + while (lastIndexOfSuffix < template.length() && template.charAt(lastIndexOfSuffix) == '}') { + lastIndexOfSuffix++; + } + + // If we found extra closing curly braces, return the position of the first two "}}" + if (lastIndexOfSuffix > nextSuffix + expressionSuffix.length()) { + return lastIndexOfSuffix - expressionSuffix.length(); + } + + return nextSuffix; + } + + /** + * Depending on expression string, try to guess if it's a Redirect, a Literal, a Function or a VariableReference + * expression. + */ + private static Expression doParseExpression(String expressionString, EvaluationContext context) { + + // Check for special exception like RedirectExpression. + boolean hasRedirect = expressionString.indexOf(RedirectExpression.REDIRECT_MARKER) != -1; + log.debug("hasRedirect:{}", hasRedirect); + + if (hasRedirect) { + String[] parts = expressionString.split(RedirectExpression.REDIRECT_MARKER_SPLIT_REGEX); + Expression[] expressions = new Expression[parts.length]; + for (int i = 0; i < parts.length; i++) { + expressions[i] = doParseSimpleExpression(parts[i].trim(), context); + } + return new RedirectExpression(expressions); + } + + // Check for special exception like FallbackExpression. + boolean hasFallback = expressionString.contains(FallbackExpression.FALLBACK_MARKER); + log.debug("hasFallback:{}", hasFallback); + + if (hasFallback) { + String[] parts = expressionString.split(FallbackExpression.FALLBACK_MARKER_SPLIT_REGEX); + Expression[] expressions = new Expression[parts.length]; + for (int i = 0; i < parts.length; i++) { + expressions[i] = doParseSimpleExpression(parts[i].trim(), context); + } + return new FallbackExpression(expressions); + } + + // Else parse simple expression. + return doParseSimpleExpression(expressionString, context); + } + + /** + * Depending on expression string, try to guess if it's a Literal, a Function or a VariableReference expression. + */ + private static Expression doParseSimpleExpression(String expressionString, EvaluationContext context) { + int argsStart = expressionString.indexOf('('); + int argsEnd = expressionString.indexOf(')'); + int variableStart = expressionString.indexOf('.'); + + boolean hasVariable = variableStart != -1; + boolean hasArgs = (argsStart != 1 && argsEnd != -1 && argsStart < argsEnd); + boolean isPostmanFunction = expressionString.startsWith("$"); + boolean varBeforeArgs = (variableStart < argsStart) && !isPostmanFunction; + + log.debug("hasVariable:{} hasArgs:{} isPostmanFunction:{} varBeforeArgs:{}", hasVariable, hasArgs, + isPostmanFunction, varBeforeArgs); + + // Check if it's a VariableReferenceExpression. + if (hasVariable && (!hasArgs || varBeforeArgs)) { + log.debug("Found a variable reference expression {}", expressionString); + String variableName = expressionString.substring(0, expressionString.indexOf('.')); + Object variable = context.lookupVariable(variableName); + String pathExpression = expressionString.substring(expressionString.indexOf('.') + 1); + + if (variable != null) { + return new VariableReferenceExpression(variable, pathExpression); + } + log.warn("Variable with name {} cannot be found into EvaluationContext. Returning empty literal expression", + variableName); + return new LiteralExpression(""); + } + + // Check if it's a ELFunctionExpression + if (hasArgs || isPostmanFunction) { + log.debug("Found a function expression {}", expressionString); + return buildFunctionExpression(expressionString, argsStart, argsEnd, context); + } + + log.info("No ELFunction or complex VariableReference expressions found... Returning simple VariableReference"); + return new VariableReferenceExpression(expressionString); + } + + private static Expression buildFunctionExpression(String expressionString, int argsStart, int argsEnd, + EvaluationContext context) { + String functionName = null; + String[] args = new String[0]; + // Checking for easier Postman compatibility notation first. + if (expressionString.startsWith("$")) { + functionName = expressionString.substring(1); + } else { + functionName = expressionString.substring(0, argsStart); + String argsString = expressionString.substring(argsStart + 1, argsEnd); + // Parse arguments if non empty string. + if (!argsString.isEmpty()) { + args = Arrays.stream(argsString.split(",")).map(String::trim).toArray(String[]::new); + } + } + + Class functionClazz = context.lookupFunction(functionName); + if (functionClazz != null) { + ELFunction function = null; + try { + function = functionClazz.getDeclaredConstructor().newInstance(); + return new FunctionExpression(function, args); + } catch (Exception e) { + log.error("Exception while instantiating the functionClazz " + functionClazz, e); + } + } + // Fallback on empty literal expression. + return new LiteralExpression(""); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/FallbackExpression.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/FallbackExpression.java new file mode 100644 index 000000000..60c2bf755 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/FallbackExpression.java @@ -0,0 +1,46 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.el; + +/** + * An implementation of {@code Expression} that returns the result of the first expression that is not null or empty. + * @author laurent + */ +public class FallbackExpression implements Expression { + + public static final String FALLBACK_MARKER = "||"; + + public static final String FALLBACK_MARKER_SPLIT_REGEX = "\\|\\|"; + + private final Expression[] expressions; + + public FallbackExpression(Expression[] expressions) { + this.expressions = expressions; + } + + @Override + public String getValue(EvaluationContext context) { + String result = null; + for (Expression expression : expressions) { + result = expression.getValue(context); + if (result != null && !result.isEmpty()) { + break; + } + } + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/FunctionExpression.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/FunctionExpression.java new file mode 100644 index 000000000..03acc7862 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/FunctionExpression.java @@ -0,0 +1,51 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import io.github.microcks.util.el.function.ELFunction; + +/** + * An implementation of {@code Expression} that invokes an {@code ELFunction} + * @author laurent + */ +public class FunctionExpression implements Expression { + + private final ELFunction function; + private final String[] functionArgs; + + /** + * Build a new function expression with a function and its invocation arguments. + * @param function The ELFunction associated to this expression + * @param functionArgs The invocation arguments of this function + */ + public FunctionExpression(ELFunction function, String[] functionArgs) { + this.function = function; + this.functionArgs = functionArgs; + } + + @Override + public String getValue(EvaluationContext context) { + return function.evaluate(context, functionArgs); + } + + public ELFunction getFunction() { + return function; + } + + public String[] getFunctionArgs() { + return functionArgs; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/LiteralExpression.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/LiteralExpression.java new file mode 100644 index 000000000..cd736bcdf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/LiteralExpression.java @@ -0,0 +1,38 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +/** + * A simple implementation of {@code Expression} that managed literals. The rendered value is the given one, unchanged. + * @author laurent + */ +public class LiteralExpression implements Expression { + + private final String value; + + /** + * Create a new Literal expression with this constant value. + * @param value The constant literal value + */ + public LiteralExpression(String value) { + this.value = value; + } + + @Override + public String getValue(EvaluationContext context) { + return this.value; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/ParseException.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/ParseException.java new file mode 100644 index 000000000..e2e7a091f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/ParseException.java @@ -0,0 +1,47 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +/** + * Represent an exception that occurs during expression parsing. + * @author laurent + */ +public class ParseException extends RuntimeException { + + protected String expressionString = null; + + protected int position = 0; + + /** + * Create a new default expression parsing exception. + * @param message description of the problem that occurred + */ + public ParseException(String message) { + super(message); + } + + /** + * Create a new expression parsing exception. + * @param expressionString the expression string that could not be parsed + * @param position the position in the expression string where the problem occurred + * @param message description of the problem that occurred + */ + public ParseException(String expressionString, int position, String message) { + super(message); + this.expressionString = expressionString; + this.position = position; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/RedirectExpression.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/RedirectExpression.java new file mode 100644 index 000000000..525461de1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/RedirectExpression.java @@ -0,0 +1,58 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import java.util.Arrays; + +/** + * An implementation of {@code Expression} that redirects the result of a first expression to other ones among a list. + * If additional expressions are {@code FunctionExpression}, result is appended to the list of arguments before + * expression invocation. + * @author laurent + */ +public class RedirectExpression implements Expression { + + public static final char REDIRECT_MARKER = '>'; + + public static final String REDIRECT_MARKER_SPLIT_REGEX = "\\>"; + + private final Expression[] expressions; + + public RedirectExpression(Expression[] expressions) { + this.expressions = expressions; + } + + @Override + public String getValue(EvaluationContext context) { + String result = null; + if (expressions.length > 0) { + // Execute first expression for getting result. + result = expressions[0].getValue(context); + for (int i = 1; i < expressions.length; i++) { + Expression exp = expressions[i]; + if (exp instanceof FunctionExpression functionExp) { + // Clone this expression, enriching args with previous result. + String[] clonedArgs = Arrays.copyOf(functionExp.getFunctionArgs(), + functionExp.getFunctionArgs().length + 1); + clonedArgs[clonedArgs.length - 1] = result; + FunctionExpression clonedExp = new FunctionExpression(functionExp.getFunction(), clonedArgs); + clonedExp.getValue(context); + } + } + } + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/TemplateEngine.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/TemplateEngine.java new file mode 100644 index 000000000..d44f4a16c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/TemplateEngine.java @@ -0,0 +1,77 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +/** + * An engine that can evaluate String templates holding some Expression Language {@code Expression}. Engine can be + * customized setting new expression delimiters (prefix and suffix). It embeds an {@code EvaluationContext} that can + * also be customized with variables and function registration before template evaluation using the {@code getValue()} + * method. + * @author laurent + */ +public class TemplateEngine { + + public static final String DEFAULT_EXPRESSION_PREFIX = "{{"; + public static final String DEFAULT_EXPRESSION_SUFFIX = "}}"; + + private String expressionPrefix = DEFAULT_EXPRESSION_PREFIX; + private String expressionSuffix = DEFAULT_EXPRESSION_SUFFIX; + + private EvaluationContext context = new EvaluationContext(); + + protected TemplateEngine() { + } + + public String getExpressionPrefix() { + return expressionPrefix; + } + + public void setExpressionPrefix(String expressionPrefix) { + this.expressionPrefix = expressionPrefix; + } + + public String getExpressionSuffix() { + return expressionSuffix; + } + + public void setExpressionSuffix(String expressionSuffix) { + this.expressionSuffix = expressionSuffix; + } + + public EvaluationContext getContext() { + return context; + } + + /** + * Evaluate the given string template, finding expressions within and evaluating them. + * @param template The string template to render. + * @return The rendered value of string template. + */ + public String getValue(String template) { + StringBuilder builder = new StringBuilder(); + + // Just delegate parsing stuffs to Expression parser to retrieve all the expressions ordered. + Expression[] expressions = ExpressionParser.parseExpressions(template, context, expressionPrefix, + expressionSuffix); + + // Now just go through expressions and evaluate them. + for (Expression expression : expressions) { + builder.append(expression.getValue(context)); + } + + return builder.toString(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/TemplateEngineFactory.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/TemplateEngineFactory.java new file mode 100644 index 000000000..058d995b9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/TemplateEngineFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import io.github.microcks.util.el.function.*; + +/** + * Helper class holding commodity methods for getting {@code TemplateEngine} instances.. + * @author laurent + */ +public class TemplateEngineFactory { + + /** + * Helper method for getting a {@code TemplateEngine} initialized with built-in functions. + * @return A new TemplateEngine instance. + */ + public static TemplateEngine getTemplateEngine() { + TemplateEngine engine = new TemplateEngine(); + + // Register some built-in functions into evaluation context. + engine.getContext().registerFunction("now", NowELFunction.class); + engine.getContext().registerFunction("timestamp", NowELFunction.class); + engine.getContext().registerFunction("uuid", UUIDELFunction.class); + engine.getContext().registerFunction("guid", UUIDELFunction.class); + engine.getContext().registerFunction("randomUUID", UUIDELFunction.class); + engine.getContext().registerFunction("randomInt", RandomIntELFunction.class); + engine.getContext().registerFunction("randomString", RandomStringELFunction.class); + engine.getContext().registerFunction("randomBoolean", RandomBooleanELFunction.class); + engine.getContext().registerFunction("randomValue", RandomValueELFunction.class); + + engine.getContext().registerFunction("randomFirstName", RandomFirstNameELFunction.class); + engine.getContext().registerFunction("randomLastName", RandomLastNameELFunction.class); + engine.getContext().registerFunction("randomFullName", RandomFullNameELFunction.class); + engine.getContext().registerFunction("randomNamePrefix", RandomNamePrefixELFunction.class); + engine.getContext().registerFunction("randomNameSuffix", RandomNameSuffixELFunction.class); + + engine.getContext().registerFunction("randomCity", RandomCityELFunction.class); + engine.getContext().registerFunction("randomCountry", RandomCountryELFunction.class); + engine.getContext().registerFunction("randomCountryCode", RandomCountryCodeELFunction.class); + engine.getContext().registerFunction("randomStreetName", RandomStreetNameELFunction.class); + engine.getContext().registerFunction("randomStreetAddress", RandomStreetAddressELFunction.class); + engine.getContext().registerFunction("randomLatitude", RandomLatitudeELFunction.class); + engine.getContext().registerFunction("randomLongitude", RandomLongitudeELFunction.class); + engine.getContext().registerFunction("randomPhoneNumber", RandomPhoneNumberELFunction.class); + + engine.getContext().registerFunction("randomEmail", RandomEmailELFunction.class); + + engine.getContext().registerFunction("put", PutInContextELFunction.class); + + return engine; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/VariableReferenceExpression.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/VariableReferenceExpression.java new file mode 100644 index 000000000..b0c7a27cc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/VariableReferenceExpression.java @@ -0,0 +1,218 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.StringReader; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.InputSource; + +/** + * An implementation of {@code Expression} that deals with variable references. Such expression is able to evaluate + * simple forms like {@code request.body} where {@code request} is provided bean. It is also able to evaluate path-like + * sub-queries when variable property value is a JSON or a XML string.
+ * For example, if {@code request.body} is a JSON string, you may use {@code request.body/books/1/author} for extracting + * the author value of first book ;-) + * @author laurent + */ +public class VariableReferenceExpression implements Expression { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(VariableReferenceExpression.class); + + private static final String ARRAY_INDEX_REGEXP = "\\[(\\d+)\\]"; + private static final String MAP_INDEX_REGEXP = "\\[([\\.\\w-]+)\\]"; + private static final Pattern ARRAY_INDEX_PATTERN = Pattern.compile(ARRAY_INDEX_REGEXP); + private static final Pattern MAP_INDEX_PATTERN = Pattern.compile(MAP_INDEX_REGEXP); + + private static final String[] PROPERTY_NAME_DELIMITERS = { "/", "[" }; + + private Object variable; + private String pathExpression; + + private String variableName; + + /** + * Create a new expression with a variable and a path (property + sub-query expression). + * @param variable Bean from whom to extract value + * @param pathExpression Path expression to get value from root object (property name + path sub-query) + */ + public VariableReferenceExpression(Object variable, String pathExpression) { + this.variable = variable; + this.pathExpression = pathExpression; + } + + /** + * Create a new expression with a variable name (to be searched later into EvaluationContext) + * @param variableName Name of a variable to get from Evaluation context. + */ + public VariableReferenceExpression(String variableName) { + this.variableName = variableName; + } + + public Object getVariable() { + return variable; + } + + public void setVariable(Object variable) { + this.variable = variable; + } + + public String getPathExpression() { + return pathExpression; + } + + public void setPathExpression(String pathExpression) { + this.pathExpression = pathExpression; + } + + @Override + public String getValue(EvaluationContext context) { + // Use variable name if we just provide this. + if (variableName != null && variable == null) { + variable = context.lookupVariable(variableName); + return (variable != null ? variable.toString() : ""); + } + + String propertyName = pathExpression; + String propertyPath = null; + int delimiterIndex = -1; + + // Search for a delimiter to isolate property name. + for (String delimiter : PROPERTY_NAME_DELIMITERS) { + delimiterIndex = pathExpression.indexOf(delimiter); + if (delimiterIndex != -1) { + propertyName = pathExpression.substring(0, delimiterIndex); + propertyPath = pathExpression.substring(delimiterIndex); + break; + } + } + Object variableValue = getProperty(variable, propertyName); + + if (log.isDebugEnabled()) { + log.debug("propertyName: {}", propertyName); + log.debug("propertyPath: {}", propertyPath); + log.debug("variableValue: {}", variableValue); + } + + if (propertyPath != null) { + if (variableValue.getClass().equals(String.class)) { + if (propertyPath.startsWith("/")) { + // This is a JSON Pointer or XPath expression to apply. + String variableString = String.valueOf(variableValue); + + if (variableString.trim().startsWith("{") || variableString.trim().startsWith("[")) { + variableValue = getJsonPointerValue(variableString, propertyPath); + } else if (variableString.trim().startsWith("<")) { + variableValue = getXPathValue(variableString, propertyPath); + } else { + log.warn("Got a path query expression but content seems not to be JSON nor XML..."); + variableValue = null; + } + } + } else if (variableValue.getClass().isArray()) { + if (propertyPath.matches(ARRAY_INDEX_REGEXP)) { + Matcher m = ARRAY_INDEX_PATTERN.matcher(propertyPath); + if (m.matches()) { + String arrayIndex = m.group(1); + Object[] variableValues = (Object[]) variableValue; + try { + variableValue = variableValues[Integer.parseInt(arrayIndex)]; + } catch (ArrayIndexOutOfBoundsException ae) { + log.warn("Expression asked for " + arrayIndex + " but array is smaller (" + variableValues.length + + "). Returning null."); + variableValue = null; + } + } + } + } else if (Map.class.isAssignableFrom(variableValue.getClass())) { + if (propertyPath.matches(MAP_INDEX_REGEXP)) { + Matcher m = MAP_INDEX_PATTERN.matcher(propertyPath); + if (m.matches()) { + String mapKey = m.group(1); + Map variableValues = (Map) variableValue; + variableValue = variableValues.get(mapKey); + } + } + } + } + + return String.valueOf(variableValue); + } + + /** + * Fetch a property from an object. For example of you wanted to get the foo property on a bar object you would + * normally call {@code bar.getFoo()}. + * @param obj The object whose property you want to fetch + * @param property The property name + * @return The value of the property or null if it does not exist. + */ + private static Object getProperty(Object obj, String property) { + Object result = null; + + try { + String methodName = "get" + property.substring(0, 1).toUpperCase() + property.substring(1); + Class clazz = obj.getClass(); + Method method = clazz.getMethod(methodName); + result = method.invoke(obj); + } catch (Exception e) { + // Do nothing, we'll return the default value + log.warn(property + " property was requested on " + obj.getClass() + " but cannot find a valid getter", e); + } + return result; + } + + /** Extract a value from JSON using a JSON Pointer expression. */ + private static String getJsonPointerValue(String jsonText, String jsonPointerExp) { + // Parse json text ang get root node. + JsonNode rootNode; + try { + ObjectMapper mapper = new ObjectMapper(); + rootNode = mapper.readTree(new StringReader(jsonText)); + // Retrieve evaluated node within JSON tree. + JsonNode evaluatedNode = rootNode.at(jsonPointerExp); + // Return serialized array if array type node is referenced by JsonPointer, text value otherwise + return evaluatedNode.isArray() || evaluatedNode.isObject() ? mapper.writeValueAsString(evaluatedNode) + : evaluatedNode.asText(); + } catch (Exception e) { + log.warn("Exception while parsing Json text", e); + return null; + } + } + + /** Extract a value from XML using a XPath expression. */ + private static String getXPathValue(String xmlText, String xPathExp) { + XPath xpath = XPathFactory.newInstance().newXPath(); + try { + XPathExpression expression = xpath.compile(xPathExp); + return expression.evaluate(new InputSource(new StringReader(xmlText))); + } catch (XPathExpressionException e) { + log.warn("Exception while compiling/evaluating XPath", e); + return null; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/AbstractRandomELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/AbstractRandomELFunction.java new file mode 100644 index 000000000..680972ce6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/AbstractRandomELFunction.java @@ -0,0 +1,37 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * This is a base class for random data generator that holds a secure random generator. + * @author laurent + */ +public abstract class AbstractRandomELFunction implements ELFunction { + + private static Random random = new SecureRandom(); + + /** Default protected constructor to hide the implicit one. */ + protected AbstractRandomELFunction() { + } + + /** Get a random generator. */ + protected static Random getRandom() { + return random; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/ELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/ELFunction.java new file mode 100644 index 000000000..04bde8031 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/ELFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; + +/** + * This is a Expression Language function definition. It just can be evaluted. + * @author laurent + */ +public interface ELFunction { + + /** + * Evaluate the function represented by the EL notation. + * @param evaluationContext The context of evaluation (contextualized info, cached one, ...) + * @param args The arguments of the function evaluation + * @return The result as a String fo this function evaluation. + */ + String evaluate(EvaluationContext evaluationContext, String... args); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/FakerELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/FakerELFunction.java new file mode 100644 index 000000000..374e19711 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/FakerELFunction.java @@ -0,0 +1,48 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * This is a base class for function that are using Datafaker (see Datafaker). + * This base class provides a convenient method for retrieving or lazy loading a Faker that will be put into + * {@code EvaluationContext}. + * @author laurent + */ +public abstract class FakerELFunction implements ELFunction { + + protected static final String FAKER_VARIABLE_NAME = "faker"; + + /** + * Retrieve a Faker from evaluation content. Lazy load it if not already present. + * @param evaluationContext The context to retrieve from or store within + * @return A Faker implementation ready to use. + */ + protected Faker retrieveFaker(EvaluationContext evaluationContext) { + Faker faker; + Object fakerObject = evaluationContext.lookupVariable(FAKER_VARIABLE_NAME); + if (fakerObject instanceof Faker) { + faker = (Faker) fakerObject; + } else { + // Build a new Faker and store it into context for later fakers. + faker = new Faker(); + evaluationContext.setVariable(FAKER_VARIABLE_NAME, faker); + } + return faker; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/NowELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/NowELFunction.java new file mode 100644 index 000000000..48a34f252 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/NowELFunction.java @@ -0,0 +1,88 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; + +import java.text.SimpleDateFormat; +import java.util.Calendar; + +/** + * Implementation of ELFunction that generates a date representation corresponding to current time + an optional amount + * of time. String format and added time unit are specified using Java Date Format patterns. + * @author laurent + */ +public class NowELFunction implements ELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + SimpleDateFormat dateFormat = null; + + if (args != null) { + switch (args.length) { + case 1: + dateFormat = new SimpleDateFormat(args[0]); + return dateFormat.format(Calendar.getInstance().getTime()); + case 2: + Calendar now = Calendar.getInstance(); + // 2nd argument is a delta with unit. + String unit = args[1].substring(args[1].length() - 1); + String amountStr = args[1].substring(0, args[1].length() - 1); + + if (isInteger(amountStr)) { + int amount = Integer.parseInt(amountStr); + switch (unit) { + case "m": + now.add(Calendar.MINUTE, amount); + break; + case "H": + now.add(Calendar.HOUR, amount); + break; + case "d": + now.add(Calendar.DATE, amount); + break; + case "M": + now.add(Calendar.MONTH, amount); + break; + case "y": + now.add(Calendar.YEAR, amount); + break; + default: + break; + } + } + dateFormat = new SimpleDateFormat(args[0]); + return dateFormat.format(now.getTime()); + default: + return String.valueOf(System.currentTimeMillis()); + } + } + return String.valueOf(System.currentTimeMillis()); + } + + /** Check if given string is parsable in Integer without throwing exception. */ + private static boolean isInteger(String strNum) { + if (strNum == null) { + return false; + } + try { + int d = Integer.parseInt(strNum); + } catch (NumberFormatException nfe) { + return false; + } + return true; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/PutInContextELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/PutInContextELFunction.java new file mode 100644 index 000000000..6868d0189 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/PutInContextELFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that puts a previous result into an evaluation context variable. + * @author laurent + */ +public class PutInContextELFunction implements ELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + if (args != null && args.length > 1) { + evaluationContext.setVariable(args[0], args[1]); + } + return ""; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomBooleanELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomBooleanELFunction.java new file mode 100644 index 000000000..ada42e319 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomBooleanELFunction.java @@ -0,0 +1,30 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random boolean. + * @author laurent + */ +public class RandomBooleanELFunction extends AbstractRandomELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + return String.valueOf(getRandom().nextBoolean()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCityELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCityELFunction.java new file mode 100644 index 000000000..328e753aa --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCityELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random city. + * @author laurent + */ +public class RandomCityELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.address().cityName(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCountryCodeELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCountryCodeELFunction.java new file mode 100644 index 000000000..897729f3e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCountryCodeELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random country code. + * @author laurent + */ +public class RandomCountryCodeELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.address().countryCode(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCountryELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCountryELFunction.java new file mode 100644 index 000000000..42ec53512 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomCountryELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random country. + * @author laurent + */ +public class RandomCountryELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.address().country(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomEmailELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomEmailELFunction.java new file mode 100644 index 000000000..e94813674 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomEmailELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random email. + * @author laurent + */ +public class RandomEmailELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.internet().safeEmailAddress(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomFirstNameELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomFirstNameELFunction.java new file mode 100644 index 000000000..2cc863a59 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomFirstNameELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random firstname. + * @author laurent + */ +public class RandomFirstNameELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.name().firstName(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomFullNameELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomFullNameELFunction.java new file mode 100644 index 000000000..fdd8cfa69 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomFullNameELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random fullname. + * @author laurent + */ +public class RandomFullNameELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.name().fullName(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomIntELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomIntELFunction.java new file mode 100644 index 000000000..e21bc30c5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomIntELFunction.java @@ -0,0 +1,57 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; + +/** + * This is an implementation of ELFunction that generates random integer values. When invoked with no arg a integer + * between Integer.MIN_VALUE and Integer.MAX_VALUE is generated. When invoked with one + * argument, a positive integer bounded by arg value is generated. When invoked with 2 arguments, an integer in + * designated intervfal is generated. + * @author laurent + */ +public class RandomIntELFunction extends AbstractRandomELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + if (args != null) { + switch (args.length) { + case 1: + int maxValue = Integer.MAX_VALUE; + try { + maxValue = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + // Ignore, we'll stick to integer max value. + } + return String.valueOf(getRandom().nextInt(maxValue)); + case 2: + int minValue = 0; + maxValue = Integer.MAX_VALUE; + try { + minValue = Integer.parseInt(args[0]); + maxValue = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + // Ignore, we'll stick to the defaults. + } + return String.valueOf(getRandom().nextInt(maxValue - minValue) + minValue); + default: + return String.valueOf(getRandom().nextInt()); + } + } + return String.valueOf(getRandom().nextInt()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLastNameELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLastNameELFunction.java new file mode 100644 index 000000000..c20cfe8db --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLastNameELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random lastname. + * @author laurent + */ +public class RandomLastNameELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.name().lastName(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLatitudeELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLatitudeELFunction.java new file mode 100644 index 000000000..0cd3613a3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLatitudeELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random latitude. + * @author laurent + */ +public class RandomLatitudeELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.address().latitude(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLongitudeELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLongitudeELFunction.java new file mode 100644 index 000000000..cf6ac60c3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomLongitudeELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random latitude. + * @author laurent + */ +public class RandomLongitudeELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.address().longitude(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomNamePrefixELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomNamePrefixELFunction.java new file mode 100644 index 000000000..7eb939372 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomNamePrefixELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random name prefix. + * @author laurent + */ +public class RandomNamePrefixELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.name().prefix(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomNameSuffixELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomNameSuffixELFunction.java new file mode 100644 index 000000000..2381dee81 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomNameSuffixELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random name suffix. + * @author laurent + */ +public class RandomNameSuffixELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.name().suffix(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomPhoneNumberELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomPhoneNumberELFunction.java new file mode 100644 index 000000000..102ca5076 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomPhoneNumberELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random phone number. + * @author laurent + */ +public class RandomPhoneNumberELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.phoneNumber().phoneNumber(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStreetAddressELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStreetAddressELFunction.java new file mode 100644 index 000000000..a742f88b8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStreetAddressELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random street address. + * @author laurent + */ +public class RandomStreetAddressELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.address().streetAddress(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStreetNameELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStreetNameELFunction.java new file mode 100644 index 000000000..616a704f5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStreetNameELFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import net.datafaker.Faker; +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that generates a random street name. + * @author laurent + */ +public class RandomStreetNameELFunction extends FakerELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + Faker faker = retrieveFaker(evaluationContext); + return faker.address().streetName(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStringELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStringELFunction.java new file mode 100644 index 000000000..4edd58f3b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomStringELFunction.java @@ -0,0 +1,54 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; + +import java.util.Random; + +/** + * This is an implementation of ELFunction that generates random Alphanumeric string. You may specify string length as + * first argument. Default length is 32. + * @author laurent + */ +public class RandomStringELFunction extends AbstractRandomELFunction { + + public static final int DEFAULT_LENGTH = 32; + + private static final int LEFT_LIMIT = 48; // numeral '0' + private static final int RIGHT_LIMIT = 122; // letter 'z' + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + if (args != null && args.length == 1) { + int maxLength = DEFAULT_LENGTH; + try { + maxLength = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + // Ignore, we'll stick to the default. + } + return generateString(getRandom(), maxLength); + } + return generateString(getRandom(), DEFAULT_LENGTH); + } + + private String generateString(Random random, int length) { + // See https://www.baeldung.com/java-random-string for reference. + return random.ints(LEFT_LIMIT, RIGHT_LIMIT + 1).filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(length).collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomValueELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomValueELFunction.java new file mode 100644 index 000000000..81d804f83 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/RandomValueELFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; + +/** + * Implementation of ELFunction that pick a value among random ones. + * @author laurent + */ +public class RandomValueELFunction extends AbstractRandomELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + if (args == null || args.length == 0) { + return ""; + } + return args[getRandom().nextInt(args.length)]; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/UUIDELFunction.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/UUIDELFunction.java new file mode 100644 index 000000000..fa6eea795 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/java/io/github/microcks/util/el/function/UUIDELFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; + +import java.util.UUID; + +/** + * Implementation of ELFunction that generates a UUID compliant with RFC 4122 (see + * https://www.cryptosys.net/pki/uuid-rfc4122.html). + * @author laurent + */ +public class UUIDELFunction implements ELFunction { + + @Override + public String evaluate(EvaluationContext evaluationContext, String... args) { + return UUID.randomUUID().toString(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/resources/META-INF/native-image/io.github.microcks/microcks-el/reflect-config.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/resources/META-INF/native-image/io.github.microcks/microcks-el/reflect-config.json new file mode 100644 index 000000000..942202aa5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/main/resources/META-INF/native-image/io.github.microcks/microcks-el/reflect-config.json @@ -0,0 +1,136 @@ +[ + { + "name": "io.github.microcks.util.el.EvaluableRequest", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.NowELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.PutInContextELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomBooleanELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomCityELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomCountryCodeELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomCountryELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomEmailELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomFirstNameELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + + { + "name": "io.github.microcks.util.el.function.RandomFullNameELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + + { + "name": "io.github.microcks.util.el.function.RandomIntELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomLastNameELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomLatitudeELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomLongitudeELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomNamePrefixELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomNameSuffixELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomPhoneNumberELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomStreetAddressELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomStreetNameELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomStringELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.RandomValueELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true, "allDeclaredMethods": true + }, + { + "name": "io.github.microcks.util.el.function.UUIDELFunction", + "queryAllDeclaredMethods": true, "queryAllDeclaredConstructors": true, "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, "queryAllPublicConstructors": true, "queryAllPublicFields": true, + "allDeclaredClasses": true, "allDeclaredConstructors": true + } +] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/EvaluationContextTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/EvaluationContextTest.java new file mode 100644 index 000000000..0b19db604 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/EvaluationContextTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import io.github.microcks.domain.Request; +import io.github.microcks.util.el.function.ELFunction; +import io.github.microcks.util.el.function.NowELFunction; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for EvaluationContext class. + * @author laurent + */ +class EvaluationContextTest { + + @Test + void testVariableRegistrationAndLookup() { + // Create a context, register and retrieve a variable. + EvaluationContext context = new EvaluationContext(); + context.setVariable("request", new Request()); + + Object requestObj = context.lookupVariable("request"); + assertTrue(requestObj instanceof Request); + } + + @Test + void testFunctionRegistrationAndLookup() throws Exception { + // Create a context, register and retrieve a function. + EvaluationContext context = new EvaluationContext(); + context.registerFunction("now", NowELFunction.class); + + Class functionClazz = context.lookupFunction("now"); + ELFunction function = functionClazz.newInstance(); + + assertEquals(NowELFunction.class, function.getClass()); + + // Test failure case. + functionClazz = context.lookupFunction("never"); + assertNull(functionClazz); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/ExpressionParserTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/ExpressionParserTest.java new file mode 100644 index 000000000..cd26476e3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/ExpressionParserTest.java @@ -0,0 +1,170 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import io.github.microcks.util.el.function.NowELFunction; +import io.github.microcks.util.el.function.PutInContextELFunction; +import io.github.microcks.util.el.function.RandomBooleanELFunction; +import io.github.microcks.util.el.function.UUIDELFunction; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * A TestCase for ExpressionParser class. + * @author laurent + */ +class ExpressionParserTest { + + @Test + void testParseExpressions() { + String template = "Hello {{ request.body/name}} it's {{now() }}"; + + // Build a suitable context. + EvaluationContext context = new EvaluationContext(); + context.registerFunction("now", NowELFunction.class); + context.setVariable("request", new EvaluableRequest("{'name': 'Laurent'}", null)); + + Expression[] expressions = ExpressionParser.parseExpressions(template, context, "{{", "}}"); + + assertEquals(4, expressions.length); + assertInstanceOf(LiteralExpression.class, expressions[0]); + assertInstanceOf(VariableReferenceExpression.class, expressions[1]); + assertInstanceOf(LiteralExpression.class, expressions[2]); + assertInstanceOf(FunctionExpression.class, expressions[3]); + + assertEquals("Hello ", ((LiteralExpression) expressions[0]).getValue(context)); + assertEquals(" it's ", ((LiteralExpression) expressions[2]).getValue(context)); + } + + @Test + void testRedirectParseExpressions() { + String template = "Hello {{ guid() > put(id) }} world! This is my {{ id }}"; + + // Build a suitable context. + EvaluationContext context = new EvaluationContext(); + context.registerFunction("guid", UUIDELFunction.class); + context.registerFunction("put", PutInContextELFunction.class); + + Expression[] expressions = ExpressionParser.parseExpressions(template, context, "{{", "}}"); + + assertEquals(4, expressions.length); + assertInstanceOf(LiteralExpression.class, expressions[0]); + assertInstanceOf(RedirectExpression.class, expressions[1]); + assertInstanceOf(LiteralExpression.class, expressions[2]); + assertInstanceOf(VariableReferenceExpression.class, expressions[3]); + + String guidValue = expressions[1].getValue(context); + String contextValue = expressions[3].getValue(context); + assertEquals(guidValue, contextValue); + } + + @Test + void testFallbackParseExpression() { + String template = "Bar value: {{ request.body/bar || randomBoolean() }}"; + + // Build a suitable context. + EvaluationContext context = new EvaluationContext(); + context.registerFunction("randomBoolean", RandomBooleanELFunction.class); + context.setVariable("request", new EvaluableRequest("{\"foo\": \"Foo\"}", null)); + + Expression[] expressions = ExpressionParser.parseExpressions(template, context, "{{", "}}"); + + assertEquals(2, expressions.length); + assertInstanceOf(LiteralExpression.class, expressions[0]); + assertInstanceOf(FallbackExpression.class, expressions[1]); + + assertEquals("Bar value: ", expressions[0].getValue(context)); + String barValue = expressions[1].getValue(context); + assertTrue("true".equals(barValue) || "false".equals(barValue)); + } + + @Test + void testXpathExpressionWithNestedFunction() { + String template = "Hello {{ request.body//*[local-name() = 'name'] }} it's {{ now() }}"; + + // Build a suitable context. + EvaluationContext context = new EvaluationContext(); + context.registerFunction("now", NowELFunction.class); + context.setVariable("request", new EvaluableRequest( + "Laurent", null)); + + Expression[] expressions = ExpressionParser.parseExpressions(template, context, "{{", "}}"); + + assertEquals(4, expressions.length); + assertTrue(expressions[0] instanceof LiteralExpression); + assertTrue(expressions[1] instanceof VariableReferenceExpression); + assertTrue(expressions[2] instanceof LiteralExpression); + assertTrue(expressions[3] instanceof FunctionExpression); + + assertEquals("Laurent", ((VariableReferenceExpression) expressions[1]).getValue(context)); + } + + @Test + void testXpathAttributeExpressionWithNestedFunction() { + String template = "Hello {{ request.body/request/name/@firstname }} it's {{ now() }}"; + + // Build a suitable context. + EvaluationContext context = new EvaluationContext(); + context.registerFunction("now", NowELFunction.class); + context.setVariable("request", new EvaluableRequest("", null)); + + Expression[] expressions = ExpressionParser.parseExpressions(template, context, "{{", "}}"); + + assertEquals(4, expressions.length); + assertTrue(expressions[0] instanceof LiteralExpression); + assertTrue(expressions[1] instanceof VariableReferenceExpression); + assertTrue(expressions[2] instanceof LiteralExpression); + assertTrue(expressions[3] instanceof FunctionExpression); + + assertEquals("Laurent", ((VariableReferenceExpression) expressions[1]).getValue(context)); + } + + @Test + void testParseExpressionWithEscapeCharacter() { + String template1 = "id : {{{uuid()}}}"; + String template2 = "name : #{{request.body/name}}#"; + + // Build a suitable context. + EvaluationContext context = new EvaluationContext(); + context.registerFunction("uuid", UUIDELFunction.class); + context.setVariable("request", new EvaluableRequest("{\"name\": \"Laurent\"}", null)); + Expression[] expressions; + + //Test for template1 + expressions = ExpressionParser.parseExpressions(template1, context, "{{", "}}"); + assertEquals(4, expressions.length); + assertInstanceOf(LiteralExpression.class, expressions[0]); + assertInstanceOf(LiteralExpression.class, expressions[1]); + assertInstanceOf(FunctionExpression.class, expressions[2]); + assertInstanceOf(LiteralExpression.class, expressions[3]); + + assertEquals("{", ((LiteralExpression) expressions[1]).getValue(context)); + assertEquals("}", ((LiteralExpression) expressions[3]).getValue(context)); + + //Test for template2 + expressions = ExpressionParser.parseExpressions(template2, context, "{{", "}}"); + assertEquals(3, expressions.length); + assertInstanceOf(LiteralExpression.class, expressions[0]); + assertInstanceOf(VariableReferenceExpression.class, expressions[1]); + assertInstanceOf(LiteralExpression.class, expressions[2]); + + assertEquals("name : #", ((LiteralExpression) expressions[0]).getValue(context)); + assertEquals("#", ((LiteralExpression) expressions[2]).getValue(context)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/FallbackExpressionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/FallbackExpressionTest.java new file mode 100644 index 000000000..86d0c4947 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/FallbackExpressionTest.java @@ -0,0 +1,74 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.el; + +import io.github.microcks.util.el.function.RandomBooleanELFunction; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for FallbackExpression class. + * @author laurent + */ +class FallbackExpressionTest { + + @Test + void testLiteralFallback() { + Expression[] expressions = new Expression[] { new LiteralExpression("hello"), new LiteralExpression("world") }; + + FallbackExpression exp = new FallbackExpression(expressions); + String result = exp.getValue(new EvaluationContext()); + assertEquals("hello", result); + } + + @Test + void testVariableReferenceFallbackToRandom() { + // First case: the first expression is not null or empty. + String jsonString = """ + { + "foo": "Foo", + "bar": "Bar" + } + """; + EvaluableRequest request = new EvaluableRequest(jsonString, null); + + Expression[] expressions = new Expression[] { new VariableReferenceExpression(request, "body/bar"), + new FunctionExpression(new RandomBooleanELFunction(), new String[] {}) }; + + FallbackExpression exp = new FallbackExpression(expressions); + String result = exp.getValue(new EvaluationContext()); + assertEquals("Bar", result); + + // Second case: the first expression is null or empty (eg: no 'bar' field in JSON). + jsonString = """ + { + "foo": "Foo" + } + """; + request = new EvaluableRequest(jsonString, null); + + expressions = new Expression[] { new VariableReferenceExpression(request, "body/bar"), + new FunctionExpression(new RandomBooleanELFunction(), new String[] {}) }; + + exp = new FallbackExpression(expressions); + result = exp.getValue(new EvaluationContext()); + assertTrue(result.equals("true") || result.equals("false")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/RedirectExpressionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/RedirectExpressionTest.java new file mode 100644 index 000000000..e7509080c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/RedirectExpressionTest.java @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import io.github.microcks.util.el.function.PutInContextELFunction; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for RedirectExpression class. + * @author laurent + */ +class RedirectExpressionTest { + + @Test + void testLiteralRedirect() { + Expression[] expressions = new Expression[] { new LiteralExpression("hello"), new LiteralExpression("world") }; + + RedirectExpression exp = new RedirectExpression(expressions); + String result = exp.getValue(new EvaluationContext()); + assertEquals("hello", result); + } + + @Test + void testLiteralRedirectToContext() { + EvaluationContext context = new EvaluationContext(); + + Expression[] expressions = new Expression[] { new LiteralExpression("hello"), + new FunctionExpression(new PutInContextELFunction(), new String[] { "greeting" }) }; + + RedirectExpression exp = new RedirectExpression(expressions); + String result = exp.getValue(context); + assertEquals("hello", result); + assertEquals("hello", context.lookupVariable("greeting")); + } + + @Test + void testLiteralRedirectToMultiContext() { + EvaluationContext context = new EvaluationContext(); + + Expression[] expressions = new Expression[] { new LiteralExpression("hello"), + new FunctionExpression(new PutInContextELFunction(), new String[] { "greeting1" }), + new FunctionExpression(new PutInContextELFunction(), new String[] { "greeting2" }) }; + + RedirectExpression exp = new RedirectExpression(expressions); + String result = exp.getValue(context); + assertEquals("hello", result); + assertEquals("hello", context.lookupVariable("greeting1")); + assertEquals("hello", context.lookupVariable("greeting2")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/TemplateEngineTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/TemplateEngineTest.java new file mode 100644 index 000000000..3ee32109c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/TemplateEngineTest.java @@ -0,0 +1,128 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import java.util.Calendar; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for TemplateEngine class. + * @author laurent + */ +class TemplateEngineTest { + + @Test + void testSimpleTemplate() { + // Prepare a string representing now(). + Calendar currentDate = Calendar.getInstance();// Assert formatting. + int day = currentDate.get(Calendar.DAY_OF_MONTH); + int month = currentDate.get(Calendar.MONTH); + int year = currentDate.get(Calendar.YEAR); + String dateString = (day < 10 ? "0" + day : day) + "/" + (++month < 10 ? "0" + month : month) + "/" + year; + + // Execute simple template calling now() and request.body function. + EvaluableRequest request = new EvaluableRequest("hello world!", new String[] { "name", "Laurent" }); + + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + engine.getContext().setVariable("request", request); + + String result = engine.getValue("Today is {{ now(dd/MM/yyyy) }} and {{ request.body }}"); + + assertEquals("Today is " + dateString + " and hello world!", result); + } + + @Test + void testContextlessTemplate() { + String template = "{\"signedAt\": \"{{ now() }}\", \"fullName\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": {{ randomInt(20, 99) }}} \n"; + + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + + String content = null; + try { + content = engine.getValue(template); + } catch (Throwable t) { + fail("Contextless template should not fail."); + } + assertTrue(content.startsWith("{\"signedAt\": \"1")); + } + + @Test + void testPostmanNotationCompatibility() { + String template = "{\"signedAt\": \"{{ now() }}\", \"fullName\": \"{{ randomFullName() }}\", \"email\": \"{{ randomEmail() }}\", \"age\": {{ randomInt(20, 99) }} } \n"; + String postmanTemplate = "{\"signedAt\": \"{{ $timestamp }}\", \"fullName\": \"{{ $randomFullName }}\", \"email\": \"{{ $randomEmail }}\", \"age\": {{ $randomInt }} } \n"; + + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + + String content = null; + String postmanContent = null; + try { + content = engine.getValue(template); + postmanContent = engine.getValue(postmanTemplate); + } catch (Throwable t) { + fail("Contextless template should not fail."); + } + assertTrue(content.startsWith("{\"signedAt\": \"1")); + assertTrue(postmanContent.startsWith("{\"signedAt\": \"1")); + } + + @Test + void testXMLWithAttributeTemplate() { + // Execute simple template calling now() and request.body function. + EvaluableRequest request = new EvaluableRequest("", null); + + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + engine.getContext().setVariable("request", request); + + String result = engine.getValue("Hello {{request.body/request/name/@firstname}}"); + + assertEquals("Hello Laurent", result); + } + + @Test + void testXMLWithNSAndAttributeTemplate() { + // Execute simple template calling now() and request.body function. + EvaluableRequest request = new EvaluableRequest( + "", + null); + + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + engine.getContext().setVariable("request", request); + + String result = engine + .getValue("Hello {{request.body//*[local-name() = 'name']/firstname/@value}}"); + + assertEquals("Hello Laurent", result); + } + + @Test + void testRequestParams() { + EvaluableRequest request = new EvaluableRequest("", null); + Map params = Map.of("id", "8", "account-name", "test"); + request.setParams(params); + + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + engine.getContext().setVariable("request", request); + + String result = engine + .getValue("{ \"id\": \"{{request.params[id]}}\", \"accountName\": \"{{request.params[account-name]}}\" }"); + + assertEquals("{ \"id\": \"8\", \"accountName\": \"test\" }", result); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/VariableReferenceExpressionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/VariableReferenceExpressionTest.java new file mode 100644 index 000000000..bd74bd3dc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/VariableReferenceExpressionTest.java @@ -0,0 +1,159 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for VariableReferenceExpression class. + * @author laurent + */ +class VariableReferenceExpressionTest { + + @Test + void testStringValue() { + EvaluableRequest request = new EvaluableRequest("hello world", null); + + // Create new expression evaluating simple string value. + VariableReferenceExpression exp = new VariableReferenceExpression(request, "body"); + String result = exp.getValue(new EvaluationContext()); + assertEquals("hello world", result); + } + + @Test + void testJSONPointerValue() { + String jsonString = "{\n" + " \"library\": \"My Personal Library\",\n" + " \"books\": [\n" + + " { \"title\":\"Title 1\", \"author\":\"Jane Doe\" },\n" + + " { \"title\":\"Title 2\", \"author\":\"John Doe\" }\n" + " ]\n" + "}"; + EvaluableRequest request = new EvaluableRequest(jsonString, null); + + // Create new expression evaluating JSON Pointer path. + VariableReferenceExpression exp = new VariableReferenceExpression(request, "body/books/1/author"); + String result = exp.getValue(new EvaluationContext()); + assertEquals("John Doe", result); + + // Test extraction of Array by JSON Pointer path + VariableReferenceExpression expArray = new VariableReferenceExpression(request, "body/books"); + String resultArray = expArray.getValue(new EvaluationContext()); + assertEquals("[{\"title\":\"Title 1\",\"author\":\"Jane Doe\"},{\"title\":\"Title 2\",\"author\":\"John Doe\"}]", + resultArray); + + // Test extraction of Object by JSON Pointer path + VariableReferenceExpression expObj = new VariableReferenceExpression(request, "body/books/1"); + String resultObj = expObj.getValue(new EvaluationContext()); + assertEquals("{\"title\":\"Title 2\",\"author\":\"John Doe\"}", resultObj); + } + + @Test + void testJSONPointerValueInArray() { + String jsonString = "[{\"foo\":{\"bar\":111222},\"quantity\":1}]"; + EvaluableRequest request = new EvaluableRequest(jsonString, null); + + // Create new expression evaluating JSON Pointer path. + VariableReferenceExpression exp = new VariableReferenceExpression(request, "body/0/quantity"); + String result = exp.getValue(new EvaluationContext()); + assertEquals("1", result); + + // Test with a nested expression + exp = new VariableReferenceExpression(request, "body/0/foo/bar"); + result = exp.getValue(new EvaluationContext()); + assertEquals("111222", result); + } + + @Test + void testXPathValue() { + String xmlString = "\n" + " My Personal Library\n" + " \n" + + " Title 1Jane Doe\n" + + " Title 2John Doe\n" + " \n" + ""; + EvaluableRequest request = new EvaluableRequest(xmlString, null); + + // Create new expression evaluating XML XPath. + VariableReferenceExpression exp = new VariableReferenceExpression(request, "body/library/books/book[1]/author"); + String result = exp.getValue(new EvaluationContext()); + assertEquals("Jane Doe", result); + } + + @Test + void testXPathWithNamespaceValue() { + String xmlString = "\n" + + " My Personal Library\n" + " \n" + + " Title 1Jane Doe\n" + + " Title 2John Doe\n" + " \n" + + ""; + EvaluableRequest request = new EvaluableRequest(xmlString, null); + + // Create new expression evaluating XML XPath. + VariableReferenceExpression exp = new VariableReferenceExpression(request, "body//*[local-name() = 'name']"); + String result = exp.getValue(new EvaluationContext()); + assertEquals("My Personal Library", result); + } + + @Test + void testArrayValues() { + EvaluableRequest request = new EvaluableRequest(null, new String[] { "one", "two" }); + + // Create new expression evaluating array value. + VariableReferenceExpression exp = new VariableReferenceExpression(request, "path[0]"); + String result = exp.getValue(new EvaluationContext()); + assertEquals("one", result); + + // Change array index. + exp.setPathExpression("path[1]"); + result = exp.getValue(new EvaluationContext()); + assertEquals("two", result); + + // Test with incorrect index. + exp.setPathExpression("path[2]"); + result = exp.getValue(new EvaluationContext()); + assertEquals("null", result); + } + + @Test + void testMapValues() { + EvaluableRequest request = new EvaluableRequest(null, null); + Map headers = new HashMap<>(); + headers.put("key", "value"); + headers.put("hello", "world"); + headers.put("account_-name", "test"); + headers.put("account.name", "test"); + request.setHeaders(headers); + + // Create new expression evaluating map value. + VariableReferenceExpression exp = new VariableReferenceExpression(request, "headers[hello]"); + String result = exp.getValue(new EvaluationContext()); + assertEquals("world", result); + + // Test with key having special characters. + exp.setPathExpression("headers[account_-name]"); + result = exp.getValue(new EvaluationContext()); + assertEquals("test", result); + + // Test with incorrect key. + exp.setPathExpression("headers[microcks]"); + result = exp.getValue(new EvaluationContext()); + assertEquals("null", result); + + // Test with key having dot character. + exp.setPathExpression("headers[account.name]"); + result = exp.getValue(new EvaluationContext()); + assertEquals("test", result); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/NowELFunctionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/NowELFunctionTest.java new file mode 100644 index 000000000..ce581b2bf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/NowELFunctionTest.java @@ -0,0 +1,85 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import java.text.SimpleDateFormat; +import java.util.Calendar; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for NowELFunction class. + * @author laurent + */ +class NowELFunctionTest { + + @Test + void testSimpleEvaluation() { + long before = System.currentTimeMillis(); + + // Compute evaluation. + NowELFunction function = new NowELFunction(); + String result = function.evaluate(null); + long resultLong = Long.parseLong(result); + + // Get new timestamp and compare. + long after = System.currentTimeMillis(); + assertTrue(before <= resultLong); + assertTrue(after >= resultLong); + } + + @Test + void testPatternEvaluation() { + Calendar currentDate = Calendar.getInstance(); + + // Compute evaluation. + NowELFunction function = new NowELFunction(); + String result = function.evaluate(null, "dd/MM/yyyy HH:mm:ss"); + + // Assert formatting. + int day = currentDate.get(Calendar.DAY_OF_MONTH); + int month = currentDate.get(Calendar.MONTH); + int year = currentDate.get(Calendar.YEAR); + String dateString = (day < 10 ? "0" + day : day) + "/" + (++month < 10 ? "0" + month : month) + "/" + year; + + assertTrue(result.startsWith(dateString)); + } + + @Test + void testPatternDeltaEvaluation() { + Calendar currentDate = Calendar.getInstance(); + + // Compute evaluation. + NowELFunction function = new NowELFunction(); + String result = function.evaluate(null, "dd/MM/yyyy HH:mm:ss", "1d"); + + // Assert formatting. + currentDate.add(Calendar.DAY_OF_YEAR, 1); + SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + String dateString = dateFormat.format(currentDate.getTime()); + + assertTrue(result.startsWith(dateString.split(" ")[0])); + + // Now add 1 month. + result = function.evaluate(null, "dd/MM/yyyy HH:mm:ss", "1M"); + currentDate.add(Calendar.DAY_OF_YEAR, -1); + currentDate.add(Calendar.MONTH, 1); + dateString = dateFormat.format(currentDate.getTime()); + assertTrue(result.startsWith(dateString.split(" ")[0])); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomFakerELFunctionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomFakerELFunctionTest.java new file mode 100644 index 000000000..1dba69e5b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomFakerELFunctionTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import io.github.microcks.util.el.EvaluationContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * This is a Test case for Random functions based on Faker. + * @author laurent + */ +class RandomFakerELFunctionTest { + + @Test + void testNameEvaluations() { + EvaluationContext context = new EvaluationContext(); + // Test simple evaluations. + RandomFirstNameELFunction function = new RandomFirstNameELFunction(); + assertNotNull(function.evaluate(context)); + + RandomLastNameELFunction lFunction = new RandomLastNameELFunction(); + assertNotNull(lFunction.evaluate(context)); + + RandomFullNameELFunction fnFunction = new RandomFullNameELFunction(); + assertNotNull(fnFunction.evaluate(context)); + + RandomNamePrefixELFunction npFunction = new RandomNamePrefixELFunction(); + assertNotNull(npFunction.evaluate(context)); + + RandomNameSuffixELFunction nsFunction = new RandomNameSuffixELFunction(); + assertNotNull(nsFunction.evaluate(context)); + } + + @Test + void testAddressEvaluations() { + EvaluationContext context = new EvaluationContext(); + // Test simple evaluations. + RandomCityELFunction cFunction = new RandomCityELFunction(); + assertNotNull(cFunction.evaluate(context)); + + RandomCountryELFunction coFunction = new RandomCountryELFunction(); + assertNotNull(coFunction.evaluate(context)); + + RandomStreetNameELFunction snFunction = new RandomStreetNameELFunction(); + assertNotNull(snFunction.evaluate(context)); + + RandomStreetAddressELFunction saFunction = new RandomStreetAddressELFunction(); + assertNotNull(saFunction.evaluate(context)); + + RandomPhoneNumberELFunction pFunction = new RandomPhoneNumberELFunction(); + assertNotNull(pFunction.evaluate(context)); + + RandomLatitudeELFunction laFunction = new RandomLatitudeELFunction(); + assertNotNull(laFunction.evaluate(context)); + + RandomLongitudeELFunction loFunction = new RandomLongitudeELFunction(); + assertNotNull(loFunction.evaluate(context)); + } + + @Test + void testInternetEvaluations() { + EvaluationContext context = new EvaluationContext(); + // Test simple evaluations. + RandomEmailELFunction eFunction = new RandomEmailELFunction(); + assertNotNull(eFunction.evaluate(context)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomIntELFunctionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomIntELFunctionTest.java new file mode 100644 index 000000000..3d5544c2b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomIntELFunctionTest.java @@ -0,0 +1,60 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for RandomIntELFunction class. + * @author laurent + */ +class RandomIntELFunctionTest { + + @Test + void testSimpleEvaluation() { + // Compute evaluation. + RandomIntELFunction function = new RandomIntELFunction(); + String randomIntString = function.evaluate(null); + + int randomInt = Integer.parseInt(randomIntString); + assertTrue(randomInt >= Integer.MIN_VALUE); + assertTrue(randomInt <= Integer.MAX_VALUE); + } + + @Test + void testBoundedEvaluation() { + // Compute evaluation. + RandomIntELFunction function = new RandomIntELFunction(); + String randomIntString = function.evaluate(null, "50"); + + int randomInt = Integer.parseInt(randomIntString); + assertTrue(randomInt >= 0); + assertTrue(randomInt <= 50); + } + + @Test + void testIntervalEvaluation() { + // Compute evaluation. + RandomIntELFunction function = new RandomIntELFunction(); + String randomIntString = function.evaluate(null, "25", "50"); + + int randomInt = Integer.parseInt(randomIntString); + assertTrue(randomInt >= 25); + assertTrue(randomInt <= 50); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomStringELFunctionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomStringELFunctionTest.java new file mode 100644 index 000000000..0b3c8d755 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomStringELFunctionTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for RandomStringELFunction class. + * @author laurent + */ +class RandomStringELFunctionTest { + + @Test + void testSimpleEvaluation() { + // Compute evaluation. + RandomStringELFunction function = new RandomStringELFunction(); + String result = function.evaluate(null); + + assertEquals(RandomStringELFunction.DEFAULT_LENGTH, result.length()); + } + + @Test + void testCustomSizeEvaluation() { + // Compute evaluation. + RandomStringELFunction function = new RandomStringELFunction(); + String result = function.evaluate(null, "64"); + + assertEquals(64, result.length()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomValueELFunctionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomValueELFunctionTest.java new file mode 100644 index 000000000..f373a51f4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/RandomValueELFunctionTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for RandomValueEFFunction class. + * @author laurent + */ +class RandomValueELFunctionTest { + + @Test + void testSimpleEvaluation() { + List values = List.of("one", "two", "three"); + + // Compute evaluation. + RandomValueELFunction function = new RandomValueELFunction(); + String result = function.evaluate(null); + assertEquals("", result); + + result = function.evaluate(null, "one", "two", "three"); + assertTrue(values.contains(result)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/UUIDELFunctionTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/UUIDELFunctionTest.java new file mode 100644 index 000000000..ee37c9919 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util-el/src/test/java/io/github/microcks/util/el/function/UUIDELFunctionTest.java @@ -0,0 +1,42 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.el.function; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for UUIDELFunction class. + * @author laurent + */ +class UUIDELFunctionTest { + + @Test + void testSimpleEvaluation() { + // Compute uuid. + UUIDELFunction function = new UUIDELFunction(); + String uuid = function.evaluate(null); + + // Check its RFC 4122 compliant. + assertEquals(36, uuid.length()); + assertEquals(8, uuid.indexOf('-')); + assertEquals(23, uuid.lastIndexOf('-')); + assertEquals("-", uuid.substring(13, 14)); + assertEquals("4", uuid.substring(14, 15)); + assertEquals("-", uuid.substring(18, 19)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/pom.xml new file mode 100644 index 000000000..da94382e6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/pom.xml @@ -0,0 +1,128 @@ + + + + + + microcks + io.github.microcks + 1.12.2-SNAPSHOT + ../../pom.xml + + 4.0.0 + + Microcks Util + microcks-util + + + UTF-8 + ../.. + 21 + 5.10.2 + 2.17.3 + 1.5.5 + 2.2 + 1.12.0 + 5.7.0 + + + + + org.slf4j + slf4j-api + + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + + org.yaml + snakeyaml + + + + + + com.networknt + json-schema-validator + ${json-schema-validator.version} + + + + org.apache.avro + avro + ${avro.version} + + + + com.graphql-java + graphql-java + + + + + io.github.microcks + microcks-model + ${project.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + commons-io + commons-io + 2.16.1 + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + -Xlint:deprecation + -Xlint:unchecked + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/AvroUtil.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/AvroUtil.java new file mode 100644 index 000000000..69ad1d16b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/AvroUtil.java @@ -0,0 +1,332 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.apache.avro.AvroTypeException; +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericDatumReader; +import org.apache.avro.generic.GenericDatumWriter; +import org.apache.avro.generic.GenericRecord; +import org.apache.avro.io.DatumReader; +import org.apache.avro.io.Decoder; +import org.apache.avro.io.DecoderFactory; +import org.apache.avro.io.Encoder; +import org.apache.avro.io.EncoderFactory; +import org.apache.avro.io.JsonDecoder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Helper class using utility methods for converting Avro format from and to JSON. + * @author laurent + */ +public class AvroUtil { + + private AvroUtil() { + // Private constructor to hide implicit public one. + } + + /** + * Convert a Avro schema string into a Schema object. + * @param avroSchema String representation of an Avro Schema to use for conversion + * @return The Avro Schema to use + * @throws org.apache.avro.SchemaParseException if the schema is not valid + */ + public static Schema getSchema(String avroSchema) { + return new Schema.Parser().parse(avroSchema); + } + + /** + * Convert a JSON string into an Avro binary representation using specified schema. + * @param json A JSON string to convert to Avro + * @param avroSchema String representation of an Avro Schema to use for conversion + * @return The Avro binary representation of JSON + * @throws AvroTypeException if there's a mismatch between JSON string and Avro Schema + * @throws IOException if something goes wrong during conversion + */ + public static byte[] jsonToAvro(String json, String avroSchema) throws AvroTypeException, IOException { + return jsonToAvro(json, getSchema(avroSchema)); + } + + /** + * Convert a JSON string into an Avro binary representation using specified schema. + * @param json A JSON string to convert to Avro + * @param avroSchema The Avro Schema to use for conversion + * @return The Avro binary representation of JSON + * @throws AvroTypeException if there's a mismatch between JSON string and Avro Schema + * @throws IOException if something goes wrong during conversion + */ + public static byte[] jsonToAvro(String json, Schema avroSchema) throws AvroTypeException, IOException { + if (avroSchema.isUnion()) { + // If the schema is a union, we need to find the right schema to use. + for (Schema schema : avroSchema.getTypes()) { + try { + return jsonToAvro(json, schema); + } catch (AvroTypeException e) { + // Ignore and try next schema. + } + } + throw new AvroTypeException("No schema in union matches JSON data"); + } + // Prepare reader an input stream from Json string. + GenericDatumReader reader = new GenericDatumReader<>(avroSchema); + InputStream input = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + JsonDecoder jsonDecoder = DecoderFactory.get().jsonDecoder(avroSchema, input); + + // Prepare write and output stream to produce binary encoding. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GenericDatumWriter writer = new GenericDatumWriter<>(avroSchema); + Encoder e = EncoderFactory.get().binaryEncoder(baos, null); + + // Read the data into a GenericRecord. + GenericRecord datum = reader.read(null, jsonDecoder); + + // Write the GenericRecord to the Avro binary. + writer.write(datum, e); + e.flush(); + + return baos.toByteArray(); + } + + /** + * Convert a JSON string into an Avro GenericRecord object using specified schema. + * @param json A JSON string to convert to Avro + * @param avroSchema String representation of an Avro Schema to use for conversion + * @return The GenericRecord representation of JSON + * @throws AvroTypeException if there's a mismatch between JSON string and Avro Schema + * @throws IOException if something goes wrong during conversion + */ + public static GenericRecord jsonToAvroRecord(String json, String avroSchema) throws AvroTypeException, IOException { + return jsonToAvroRecord(json, getSchema(avroSchema)); + } + + /** + * Convert a JSON string into an Avro GenericRecord object using specified schema. + * @param json A JSON string to convert to Avro + * @param avroSchema The Avro Schema to use for conversion + * @return The GenericRecord representation of JSON + * @throws AvroTypeException if there's a mismatch between JSON string and Avro Schema + * @throws IOException if something goes wrong during conversion + */ + public static GenericRecord jsonToAvroRecord(String json, Schema avroSchema) throws AvroTypeException, IOException { + if (avroSchema.isUnion()) { + // If the schema is a union, we need to find the right schema to use. + for (Schema schema : avroSchema.getTypes()) { + try { + return jsonToAvroRecord(json, schema); + } catch (AvroTypeException e) { + // Ignore and try next schema. + } + } + throw new AvroTypeException("No schema in union matches JSON data"); + } + + // Prepare reader an input stream from Json string. + GenericDatumReader reader = new GenericDatumReader<>(avroSchema); + InputStream input = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + JsonDecoder jsonDecoder = DecoderFactory.get().jsonDecoder(avroSchema, input); + + return reader.read(null, jsonDecoder); + } + + /** + * Convert an Avro binary representation into a JSON string using specified schema. + * @param avroBinary An Avro binary representation to convert in JSON string + * @param avroSchema The Avro Schema to use for conversion + * @return The JSON string representing Avro binary + * @throws AvroTypeException if there's a mismatch between Avro binary and Schema + * @throws IOException if something goes wrong during conversion + */ + public static String avroToJson(byte[] avroBinary, String avroSchema) throws AvroTypeException, IOException { + return avroToJson(avroBinary, getSchema(avroSchema)); + } + + /** + * Convert an Avro binary representation into a JSON string using specified schema. + * @param avroBinary An Avro binary representation to convert in JSON string + * @param avroSchema The Avro Schema to use for conversion + * @return The JSON string representing Avro binary + * @throws AvroTypeException if there's a mismatch between Avro binary and Schema + * @throws IOException if something goes wrong during conversion + */ + public static String avroToJson(byte[] avroBinary, Schema avroSchema) throws AvroTypeException, IOException { + if (avroSchema.isUnion()) { + // If the schema is a union, we need to find the right schema to use. + for (Schema schema : avroSchema.getTypes()) { + try { + return avroToJson(avroBinary, schema); + } catch (AvroTypeException e) { + // Ignore and try next schema. + } + } + throw new AvroTypeException("No schema in union matches Avro binary data"); + } + DatumReader datumReader = new GenericDatumReader<>(avroSchema); + Decoder decoder = DecoderFactory.get().binaryDecoder(avroBinary, null); + + GenericRecord genRecord = datumReader.read(null, decoder); + return genRecord.toString(); + } + + /** + * Convert an Avro binary representation into an Avro GenericRecord object using specified schema. + * @param avroBinary An Avro binary representation to convert in record + * @param avroSchema The Avro Schema to use for conversion + * @return The JSON string representing Avro binary + * @throws AvroTypeException if there's a mismatch between Avro binary and Schema + * @throws IOException if something goes wrong during conversion + */ + public static GenericRecord avroToAvroRecord(byte[] avroBinary, String avroSchema) + throws AvroTypeException, IOException { + return avroToAvroRecord(avroBinary, getSchema(avroSchema)); + } + + /** + * Convert an Avro binary representation into an Avro GenericRecord object using specified schema. + * @param avroBinary An Avro binary representation to convert in record + * @param avroSchema The Avro Schema to use for conversion + * @return The JSON string representing Avro binary + * @throws AvroTypeException if there's a mismatch between Avro binary and Schema + * @throws IOException if something goes wrong during conversion + */ + public static GenericRecord avroToAvroRecord(byte[] avroBinary, Schema avroSchema) + throws AvroTypeException, IOException { + if (avroSchema.isUnion()) { + // If the schema is a union, we need to find the right schema to use. + for (Schema schema : avroSchema.getTypes()) { + try { + return avroToAvroRecord(avroBinary, schema); + } catch (AvroTypeException e) { + // Ignore and try next schema. + } + } + throw new AvroTypeException("No schema in union matches Avro binary data"); + } + DatumReader datumReader = new GenericDatumReader<>(avroSchema); + Decoder decoder = DecoderFactory.get().binaryDecoder(avroBinary, null); + + return datumReader.read(null, decoder); + } + + /** + * Validate that a datum object (typically a GenericRecord read somewhere but the method signature is loosely coupled + * to make it recursive friendly) is compliant with an Avro schema. + * @param schema The Avro Schema to validate datum against + * @param datum The Object datum to validate + * @return True if the object is compliant with supplied schema, false otherwise. + */ + public static boolean validate(Schema schema, Object datum) { + switch (schema.getType()) { + case RECORD: + if (datum instanceof GenericRecord genericRecord) { + for (Schema.Field f : schema.getFields()) { + if (!genericRecord.hasField(f.name())) + return false; + if (!validate(f.schema(), genericRecord.get(f.pos()))) + return false; + } + return true; + } + default: + return GenericData.get().validate(schema, datum); + } + } + + /** + * Get validation errors of a datum object regarding Avro schema. + * @param schema The Schema to check datum object against + * @param datum The datum object to validate + * @param fieldName The name of the field we're currently validating + * @return A list of String representing validation errors. List may be empty if no error found. + */ + public static List getValidationErrors(Schema schema, Object datum, String... fieldName) { + List errors = new ArrayList<>(); + + switch (schema.getType()) { + case RECORD: + if (datum instanceof GenericRecord genericRecord) { + for (Schema.Field f : schema.getFields()) { + // Check for defined and required field. + if (!genericRecord.hasField(f.name()) && !f.hasDefaultValue()) { + errors.add("Required field " + f.name() + " cannot be found in record"); + } else if (genericRecord.hasField(f.name())) { + // Now add errors for each field if defined at the record level. + errors.addAll(getValidationErrors(f.schema(), genericRecord.get(f.pos()), f.name())); + } + } + } + break; + case ENUM: + if (!schema.hasEnumSymbol(datum.toString())) + errors.add(datum + " enum value is not defined in schema"); + break; + case ARRAY: + if (!(datum instanceof Collection collection)) { + errors.add(fieldName[0] + " is not a valid array"); + } else { + // Now add errors for each element. + for (Object element : collection) { + errors.addAll(getValidationErrors(schema.getElementType(), element)); + } + } + break; + case STRING: + if (!(datum instanceof CharSequence)) + errors.add(fieldName[0] + " is not a string"); + break; + case BYTES: + if (!(datum instanceof ByteBuffer)) + errors.add(fieldName[0] + " is not bytes"); + break; + case INT: + if (!(datum instanceof Integer)) + errors.add(fieldName[0] + " is not an integer"); + break; + case LONG: + if (!(datum instanceof Long)) + errors.add(fieldName[0] + " is not a long"); + break; + case FLOAT: + if (!(datum instanceof Float)) + errors.add(fieldName[0] + " is not a float"); + break; + case DOUBLE: + if (!(datum instanceof Double)) + errors.add(fieldName[0] + " is not a double"); + break; + case BOOLEAN: + if (!(datum instanceof Boolean)) + errors.add(fieldName[0] + " is not a boolean"); + break; + case UNION: + // Get validation errors for each type in union. + for (Schema unionSchema : schema.getTypes()) { + errors.addAll(getValidationErrors(unionSchema, datum)); + } + break; + } + return errors; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/JsonSchemaValidator.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/JsonSchemaValidator.java new file mode 100644 index 000000000..5ef428f5a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/JsonSchemaValidator.java @@ -0,0 +1,202 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.networknt.schema.JsonMetaSchema; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.ValidationMessage; +import com.networknt.schema.SpecVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Helper class for validating Json objects against their Json schema. Supported version of Json schema is + * http://json-schema.org/draft-07/schema. + * @author laurent + */ +public class JsonSchemaValidator { + + /** A commons logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(JsonSchemaValidator.class); + + public static final String JSON_V4_SCHEMA_IDENTIFIER = "http://json-schema.org/draft-04/schema#"; + public static final String JSON_V7_SCHEMA_IDENTIFIER = "http://json-schema.org/draft-07/schema#"; + public static final String JSON_V12_SCHEMA_IDENTIFIER = "http://json-schema.org/draft/2020-12/schema#"; + public static final String JSON_SCHEMA_IDENTIFIER_ELEMENT = "$schema"; + + public static final String JSON_SCHEMA_COMPONENTS_ELEMENT = "components"; + public static final String JSON_SCHEMA_PROPERTIES_ELEMENT = "properties"; + public static final String JSON_SCHEMA_REQUIRED_ELEMENT = "required"; + public static final String JSON_SCHEMA_ITEMS_ELEMENT = "items"; + public static final String JSON_SCHEMA_ADD_PROPERTIES_ELEMENT = "additionalProperties"; + + private static final ObjectMapper mapper = new ObjectMapper() + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + .enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN).enable(SerializationFeature.INDENT_OUTPUT); + + private JsonSchemaValidator() { + // Private constructor to hide the implicit public one. + } + + /** + * Check if a Json object is valid against the given Json schema specification. + * @param schemaText The Json schema specification as a string + * @param jsonText The Json object as a string + * @return True if Json object is valid, false otherwise + * @throws IOException if json string representations cannot be parsed + */ + public static boolean isJsonValid(String schemaText, String jsonText) throws IOException { + return isJsonValid(schemaText, jsonText, null); + } + + /** + * Check if a Json object is valid against the given Json schema specification. + * @param schemaText The Json schema specification as a string + * @param jsonText The Json object as a string + * @param namespace Namespace definition to resolve relative dependencies in Json schema + * @return True if Json object is valid, false otherwise + * @throws IOException if json string representations cannot be parsed + */ + public static boolean isJsonValid(String schemaText, String jsonText, String namespace) throws IOException { + List errors = validateJson(schemaText, jsonText, namespace); + if (!errors.isEmpty()) { + log.debug("Get validation errors, returning false"); + return false; + } + return true; + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of Json schema spec + * semantics regarding additional or unknown attributes: schema must explicitly set additionalProperties + * to false if you want to consider unknown attributes as validation errors. It returns a list of validation error + * messages. + * @param schemaText The Json schema specification as a string + * @param jsonText The Json object as a string + * @return The list of validation failure messages. If empty, json object is valid ! + * @throws IOException if json string representations cannot be parsed + */ + public static List validateJson(String schemaText, String jsonText) throws IOException { + return validateJson(getJsonNode(schemaText), getJsonNode(jsonText), null); + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of Json schema spec + * semantics regarding additional or unknown attributes: schema must explicitly set additionalProperties + * to false if you want to consider unknown attributes as validation errors. It returns a list of validation error + * messages. + * @param schemaText The Json schema specification as a string + * @param jsonText The Json object as a string + * @param namespace Namespace definition to resolve relative dependencies in Json schema + * @return The list of validation failure messages. If empty, json object is valid ! + * @throws IOException if json string representations cannot be parsed + */ + public static List validateJson(String schemaText, String jsonText, String namespace) throws IOException { + return validateJson(getJsonNode(schemaText), getJsonNode(jsonText), namespace); + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of Json schema spec + * semantics regarding additional or unknown attributes: schema must explicitly set additionalProperties + * to false if you want to consider unknown attributes as validation errors. It returns a list of validation error + * messages. + * @param schemaNode The Json schema specification as a Jackson node + * @param jsonNode The Json object as a Jackson node + * @return The list of validation failures messages. If empty, json object is valid ! + */ + public static List validateJson(JsonNode schemaNode, JsonNode jsonNode) { + return validateJson(schemaNode, jsonNode, null); + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of Json schema spec + * semantics regarding additional or unknown attributes: schema must explicitly set additionalProperties + * to false if you want to consider unknown attributes as validation errors. It returns a list of validation error + * messages. + * @param schemaNode The Json schema specification as a Jackson node + * @param jsonNode The Json object as a Jackson node + * @param namespace Namespace definition to resolve relative dependencies in Json schema + * @return The list of validation failure messages. If empty, json object is valid ! + */ + public static List validateJson(JsonNode schemaNode, JsonNode jsonNode, String namespace) { + List errors = new ArrayList<>(); + + final JsonSchema jsonSchemaNode = extractJsonSchemaNode(schemaNode, namespace); + + Set messages = jsonSchemaNode.validate(jsonNode, executionContext -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + executionContext.getExecutionConfig().setLocale(Locale.US); + }); + + if (!messages.isEmpty()) { + for (ValidationMessage message : messages) { + errors.add(message.getError()); + } + } + return errors; + } + + /** + * Get a Jackson JsonNode representation for Json object. + * @param jsonText The Json object as a string + * @return The Jackson JsonNode corresponding to json object string + * @throws IOException if json string representation cannot be parsed + */ + public static JsonNode getJsonNode(String jsonText) throws IOException { + return mapper.readTree(jsonText); + } + + /** + * Get a Jackson JsonNode representation for Json schema. + * @param schemaText The Json schema specification as a string + * @return The Jackson JsonSchema corresponding to json schema string + * @throws IOException if json string representation cannot be parsed + */ + public static JsonSchema getSchemaNode(String schemaText) throws IOException { + final JsonNode schemaNode = getJsonNode(schemaText); + return extractJsonSchemaNode(schemaNode, null); + } + + private static JsonSchema extractJsonSchemaNode(JsonNode jsonNode, String namespace) { + JsonMetaSchema jsonMetaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012()).build(); + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012, + builder -> builder.metaSchema(jsonMetaSchema)); + + if (namespace != null) { + URI baseUri = URI.create(namespace); + return jsonSchemaFactory.getSchema(baseUri, jsonNode); + } + + return jsonSchemaFactory.getSchema(jsonNode); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/MalformedXmlException.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/MalformedXmlException.java new file mode 100644 index 000000000..8528f7810 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/MalformedXmlException.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +/** + * A typed exception for malformed Xml document that can be launched during parsing or analysis. + * @author laurent + */ +public class MalformedXmlException extends Exception { + + /** + * Build a new MalformedXmlException. + * @param message Message that explains this exception. + */ + public MalformedXmlException(String message) { + super(message); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/SchemaMap.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/SchemaMap.java new file mode 100644 index 000000000..848aaaa4c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/SchemaMap.java @@ -0,0 +1,70 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * This is a lightweight structure representing a Schema registry snapshot dedicated for being used by message + * validators. + * @author laurent + */ +public class SchemaMap { + + /** The internal map backing schemas storage. This is an in-memory map. */ + private Map schemaEntries; + + /** Build a new schema map with empty content. */ + public SchemaMap() { + schemaEntries = new HashMap<>(); + } + + /** + * Initialize a new schema map with provided content. + * @param schemaEntries Map of schema entries. Keys are schema paths, Values are schema string representation. + */ + public SchemaMap(Map schemaEntries) { + this.schemaEntries = schemaEntries; + } + + /** + * Put a new schema entry in map. + * @param schemaPath The path of this new entry. + * @param content The string representation of schema content + */ + public void putSchemaEntry(String schemaPath, String content) { + schemaEntries.put(schemaPath, content); + } + + /** + * Check if we've got an entry for this schema path. + * @param schemaPath The path of searched entry. + * @return True if we've got a corresponding entry, false otherwise. + */ + public boolean hasSchemaEntry(String schemaPath) { + return schemaEntries.containsKey(schemaPath); + } + + /** + * Get schema entry string content. + * @param schemaPath The path of searched entry. + * @return The string representation of schema content + */ + public String getSchemaEntry(String schemaPath) { + return schemaEntries.get(schemaPath); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlErrorHandler.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlErrorHandler.java new file mode 100644 index 000000000..dfb023e25 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlErrorHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXParseException; + +import java.util.ArrayList; +import java.util.List; + +/** + * A simple SAX error handler that accumulates exception within an exceptions list. + * @author laurent + */ +public class XmlErrorHandler implements ErrorHandler { + + private List exceptions; + + public XmlErrorHandler() { + this.exceptions = new ArrayList<>(); + } + + /** + * Get attached Xml validation errors. + * @return A list of SAX parsing exception while line numbers and explanation of validation failures. + */ + public List getExceptions() { + return exceptions; + } + + @Override + public void warning(SAXParseException exception) { + exceptions.add(exception); + } + + @Override + public void error(SAXParseException exception) { + exceptions.add(exception); + } + + @Override + public void fatalError(SAXParseException exception) { + exceptions.add(exception); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlSchemaURLResolver.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlSchemaURLResolver.java new file mode 100644 index 000000000..002439f9a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlSchemaURLResolver.java @@ -0,0 +1,176 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.ls.LSInput; +import org.w3c.dom.ls.LSResourceResolver; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.util.Map; + +/** + * An implementation of LSResourceResolver that may use local cache for standard schemas (xml.xsd) of an URL resolver + * for relative references to XSD that may be embedded into other XSD. + * @author laurent + */ +public class XmlSchemaURLResolver implements LSResourceResolver { + + /** A commons logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(XmlSchemaURLResolver.class); + + private static final Map LOCAL_RESOLUTIONS = Map.of("http://www.w3.org/2001/xml.xsd", "xml.xsd"); + + private String baseResourceURL; + + /** + * Build a XmlSchemaURLResolver with base URL for resolving relative dependencies. + * @param baseResourceURL The base URL to consider for relative dependencies + */ + public XmlSchemaURLResolver(String baseResourceURL) { + this.baseResourceURL = baseResourceURL; + } + + @Override + public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) { + log.debug("Resolving resource with systemId: {}", systemId); + + InputStream resourceStream = null; + if (LOCAL_RESOLUTIONS.containsKey(systemId)) { + log.debug("Got a local resolution, loading it from classloader"); + resourceStream = this.getClass().getClassLoader().getResourceAsStream(LOCAL_RESOLUTIONS.get(systemId)); + } else if (baseResourceURL != null && (!systemId.startsWith("http://") || !systemId.startsWith("https://"))) { + log.debug("Got a baseResourceURL defined and relative reference, loading it from URL"); + String sanitizedSystemId = systemId; + if (systemId.startsWith("./")) { + sanitizedSystemId = systemId.substring(2); + } + try { + URL resourceURL = new URL(baseResourceURL + (baseResourceURL.endsWith("/") ? "" : "/") + sanitizedSystemId); + resourceStream = resourceURL.openStream(); + } catch (Exception e) { + log.error("Failed to open stream on {}/{}", baseResourceURL, sanitizedSystemId, e); + } + } + + if (resourceStream != null) { + Input input = new Input(); + input.setSystemId(systemId); + input.setPublicId(publicId); + input.setBaseURI(baseURI); + input.setCharacterStream(new InputStreamReader(resourceStream)); + return input; + } + // Let default behaviour happen! + return null; + } + + public class Input implements LSInput { + + private Reader characterStream; + private InputStream byteStream; + private String stringData; + private String systemId; + private String publicId; + private String baseURI; + private String encoding; + private boolean certifiedText; + + @Override + public Reader getCharacterStream() { + return characterStream; + } + + @Override + public void setCharacterStream(Reader characterStream) { + this.characterStream = characterStream; + } + + @Override + public InputStream getByteStream() { + return byteStream; + } + + @Override + public void setByteStream(InputStream byteStream) { + this.byteStream = byteStream; + } + + @Override + public String getStringData() { + return stringData; + } + + @Override + public void setStringData(String stringData) { + this.stringData = stringData; + } + + @Override + public String getSystemId() { + return systemId; + } + + @Override + public void setSystemId(String systemId) { + this.systemId = systemId; + } + + @Override + public String getPublicId() { + return publicId; + } + + @Override + public void setPublicId(String publicId) { + this.publicId = publicId; + } + + @Override + public String getBaseURI() { + return baseURI; + } + + @Override + public void setBaseURI(String baseURI) { + this.baseURI = baseURI; + } + + @Override + public String getEncoding() { + return encoding; + } + + @Override + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + @Override + public boolean getCertifiedText() { + return false; + } + + @Override + public void setCertifiedText(boolean certifiedText) { + this.certifiedText = certifiedText; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlSchemaValidator.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlSchemaValidator.java new file mode 100644 index 000000000..c662e620f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlSchemaValidator.java @@ -0,0 +1,75 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import java.io.InputStream; +import java.io.StringReader; +import java.util.List; + +/** + * Helper class for easy access to a Xml Schema validation. Validators embed an error handler and use a specific + * LSResourceResolver allowing for resolving relative dependencies from provided XmlSchemas. + * @author laurent + */ +public class XmlSchemaValidator { + + private XmlSchemaValidator() { + // Hide the implicit default constructor. + } + + /** + * Validation the Xml string with provided stream on its Xml schema. + * @param schemaStream A stream on reference Xml Schema + * @param xmlString String representation of Xml to validate + * @return A list of validation errors that mey be empty if validation is ok + * @throws Exception if validator cannot be initialized (in case of malformed schema stream) + */ + public static List validateXml(InputStream schemaStream, String xmlString) throws Exception { + return validateXml(schemaStream, xmlString, null); + } + + /** + * Validation the Xml string with provided stream on its Xml schema. A resource URL can be provided to resolved + * relative includes found into Xml Schema stream. + * @param schemaStream A stream on reference Xml Schema + * @param xmlString String representation of Xml to validate + * @param baseResourceUrl Base resource URL for resolving relative dependencies + * @return A list of validation errors that mey be empty if validation is ok + * @throws Exception if validator cannot be initialized (in case of malformed schema stream) + */ + public static List validateXml(InputStream schemaStream, String xmlString, String baseResourceUrl) + throws Exception { + SchemaFactory factory = SchemaFactory.newDefaultInstance(); + factory.setResourceResolver(new XmlSchemaURLResolver(baseResourceUrl)); + + Source schemaFile = new StreamSource(schemaStream); + Schema schema = factory.newSchema(schemaFile); + Validator validator = schema.newValidator(); + + XmlErrorHandler errorHandler = new XmlErrorHandler(); + validator.setErrorHandler(errorHandler); + + validator.validate(new StreamSource(new StringReader(xmlString))); + + return errorHandler.getExceptions().stream().map(e -> "line " + e.getLineNumber() + ": " + e.getMessage()) + .toList(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlUtil.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlUtil.java new file mode 100644 index 000000000..35e77c576 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/XmlUtil.java @@ -0,0 +1,101 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper methods for parsing/navigating an Xml DOM. + * @author laurent + */ +public class XmlUtil { + + /** XML Schema public namespace; */ + public static final String XML_SCHEMA_NS = "http://www.w3.org/2001/XMLSchema"; + /** WSDL public namespace. */ + public static final String WSDL_NS = "http://schemas.xmlsoap.org/wsdl/"; + + private XmlUtil() { + // Hide the implicit default constructor. + } + + /** + * Retrieve direct children elements of a parent having specified namespace and tag. Only includes level 1 children. + * @param parent The parent of children to find + * @param namespace The namespace of children + * @param tag The tag of children + * @return Children Elements as a list + */ + public static List getDirectChildren(Element parent, String namespace, String tag) { + List directChildren = new ArrayList<>(); + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE && namespace.equals(child.getNamespaceURI()) + && tag.equals(child.getLocalName())) { + directChildren.add((Element) child); + } + } + return directChildren; + } + + /** + * Retrieve a direct child that is expected to be unique under the parent. Throws a MalformedXmlException if no child + * or more than one child present. + * @param parent The parent of child to find + * @param namespace The namespace of child + * @param tag The tag of child + * @return The child Element + * @throws MalformedXmlException if no child or more than one child present. + */ + public static Element getUniqueDirectChild(Element parent, String namespace, String tag) + throws MalformedXmlException { + NodeList children = parent.getElementsByTagNameNS(namespace, tag); + if (children.getLength() == 0) { + throw new MalformedXmlException("Element " + tag + " is missing under " + parent.getTagName()); + } + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE && child.getParentNode() == parent) { + return (Element) child; + } + } + throw new MalformedXmlException("Element " + tag + " was expected directly under " + parent.getTagName()); + } + + /** + * Check if parent has at least one direct child having namespace and tag. + * @param parent The parent of children to find + * @param namespace The namespace of children + * @param tag The tag of children + * @return true if at least one child is present, false otherwise + */ + public static boolean hasDirectChild(Element parent, String namespace, String tag) { + NodeList children = parent.getElementsByTagNameNS(namespace, tag); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE && child.getParentNode() == parent) { + return true; + } + } + return false; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/asyncapi/AsyncAPISchemaUtil.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/asyncapi/AsyncAPISchemaUtil.java new file mode 100644 index 000000000..ce6d97960 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/asyncapi/AsyncAPISchemaUtil.java @@ -0,0 +1,165 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.asyncapi; + +import io.github.microcks.util.AvroUtil; +import io.github.microcks.util.SchemaMap; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.apache.avro.Schema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class for extracting information from AsyncAPI schema. Supported version of AsyncAPI schema are + * https://www.asyncapi.com/docs/reference/specification/v3.0.0 and + * https://www.asyncapi.com/docs/reference/specification/v2.x. + * @author laurent + */ +public class AsyncAPISchemaUtil { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(AsyncAPISchemaUtil.class); + + public static final String ASYNC_SCHEMA_PAYLOAD_ELEMENT = "payload"; + private static final String ONE_OF_STRUCT = "oneOf"; + + + /** Private constructor to hide the implicit one. */ + private AsyncAPISchemaUtil() { + } + + /** Define the JSON pointer expression to access the operation messages. */ + public static String findMessagePathPointer(JsonNode specificationNode, String operationName) { + String messagePathPointer = ""; + String[] operationElements = operationName.split(" "); + + String asyncApi = specificationNode.path("asyncapi").asText("2"); + if (asyncApi.startsWith("3")) { + // Assume we have an AsyncAPI v3 document. + String operationNamePtr = "/operations/" + operationElements[1].replace("/", "~1"); + messagePathPointer = operationNamePtr + "/messages"; + } else { + // Assume we have an AsyncAPI v2 document. + String operationNamePtr = "/channels/" + operationElements[1].replace("/", "~1"); + if ("SUBSCRIBE".equals(operationElements[0])) { + operationNamePtr += "/subscribe"; + } else { + operationNamePtr += "/publish"; + } + messagePathPointer = operationNamePtr + "/message"; + } + return messagePathPointer; + } + + /** + * Retrieve the Avro schema corresponding to a message using its JSON pointer in Spec. Complete the {@code schemaMap} + * if provided. Raise a simple exception with message if problem while navigating the spec. + */ + public static Schema retrieveMessageAvroSchema(JsonNode specificationNode, String messagePathPointer, + SchemaMap schemaMap) throws Exception { + // Extract Json node for message pointer. + JsonNode messageNode = specificationNode.at(messagePathPointer); + if (messageNode == null || messageNode.isMissingNode()) { + log.debug("messagePathPointer {} is not a valid JSON Pointer", messagePathPointer); + throw new Exception("messagePathPointer does not represent a valid JSON Pointer in AsyncAPI specification"); + } + // Message node can be just a reference. + messageNode = followRefIfAny(messageNode, specificationNode); + + if (messageNode.isArray()) { + // In the case of AsyncAPI v3, we always got messages even if there's just one element. + // Wrapping them in oneOf, make us lose details on validation errors so just do it if necessary. + ArrayNode messagesNode = (ArrayNode) messageNode; + if (messagesNode.size() > 1) { + return buildOneOfMessagesAvroSchemas(specificationNode, (ArrayNode) messageNode, schemaMap); + } + return buildSingleMessageAvroSchema(followRefIfAny(messagesNode.get(0), specificationNode), schemaMap); + } else if (messageNode.has(ONE_OF_STRUCT)) { + ArrayNode oneOfMessageNode = (ArrayNode) messageNode.get(ONE_OF_STRUCT); + return buildOneOfMessagesAvroSchemas(specificationNode, oneOfMessageNode, schemaMap); + } else { + return buildSingleMessageAvroSchema(messageNode, schemaMap); + } + } + + /** Build an array of Avro schemas for messages expressed as a direct oneOf structure. */ + private static Schema buildOneOfMessagesAvroSchemas(JsonNode specificationNode, ArrayNode oneOfMessageNode, + SchemaMap schemaMap) throws Exception { + // Initialize a oneOf schema with array. + Schema[] schemas = new Schema[oneOfMessageNode.size()]; + + // Extract the schema payload for each alternative of the message. + for (int i = 0; i < oneOfMessageNode.size(); i++) { + JsonNode altMessageNode = oneOfMessageNode.get(i); + // Extract a schema for each messages + altMessageNode = followRefIfAny(altMessageNode, specificationNode); + schemas[i] = buildSingleMessageAvroSchema(altMessageNode, schemaMap); + } + + return Schema.createUnion(schemas); + } + + /** Build an Avro schema for spring message definition. */ + private static Schema buildSingleMessageAvroSchema(JsonNode messageNode, SchemaMap schemaMap) throws Exception { + // Check that message node has a payload attribute. + if (!messageNode.has(ASYNC_SCHEMA_PAYLOAD_ELEMENT)) { + log.debug("messageNode {} has no 'payload' attribute", messageNode); + throw new Exception("message definition has no valid payload in AsyncAPI specification"); + } + + // Navigate to payload definition. + messageNode = messageNode.path(ASYNC_SCHEMA_PAYLOAD_ELEMENT); + + // Payload node can be just a reference to another schema... But in the case of Avro, this is an external schema + // as #/components/schemas can only hold JSON schemas. So we have to use a registry for resolving and accessing + // this Avro schema. We'll have to build an Avro Schema either from payload content or registry content. + String schemaContent = null; + + if (messageNode.has("$ref")) { + // Remove trailing anchor marker if any. + // './user-signedup.avsc#/User' => './user-signedup.avsc' + String ref = messageNode.path("$ref").asText(); + log.debug("Looking for an external Avro schema in registry: {}", ref); + if (ref.contains("#")) { + ref = ref.substring(0, ref.indexOf("#")); + } + if (schemaMap != null) { + schemaContent = schemaMap.getSchemaEntry(ref); + } + if (schemaContent == null) { + log.info("No schema content found in SchemaMap. {} is not found", ref); + throw new Exception("no schema content found for " + ref + " in used SchemaMap."); + } + } else { + // Schema is specified within the payload definition. + schemaContent = messageNode.toString(); + } + + // Now build and return the schema. + return AvroUtil.getSchema(schemaContent); + } + + /** Check if a node has a reference and follow it to target node in the document. */ + public static JsonNode followRefIfAny(JsonNode referencableNode, JsonNode documentRoot) { + if (referencableNode.has("$ref")) { + String ref = referencableNode.path("$ref").asText(); + return followRefIfAny(documentRoot.at(ref.substring(1)), documentRoot); + } + return referencableNode; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/asyncapi/AsyncAPISchemaValidator.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/asyncapi/AsyncAPISchemaValidator.java new file mode 100644 index 000000000..fddd4956c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/asyncapi/AsyncAPISchemaValidator.java @@ -0,0 +1,404 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.asyncapi; + +import io.github.microcks.util.AvroUtil; +import io.github.microcks.util.JsonSchemaValidator; +import io.github.microcks.util.SchemaMap; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.apache.avro.AvroTypeException; +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static io.github.microcks.util.JsonSchemaValidator.*; +import static io.github.microcks.util.asyncapi.AsyncAPISchemaUtil.*; + +/** + * Helper class for validating Json objects against their AsyncAPI schema. Supported version of AsyncAPI schema are + * https://www.asyncapi.com/docs/reference/specification/v3.0.0 and + * https://www.asyncapi.com/docs/reference/specification/v2.x. + * @author laurent + */ +public class AsyncAPISchemaValidator { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(AsyncAPISchemaValidator.class); + + public static final String ASYNC_SCHEMA_PAYLOAD_ELEMENT = "payload"; + + private static final String ONE_OF_STRUCT = "oneOf"; + private static final String[] STRUCTURES = { "allOf", "anyOf", ONE_OF_STRUCT, "not", "items", + "additionalProperties" }; + private static final String[] NOT_SUPPORTED_ATTRIBUTES = { "discriminator", "externalDocs", "deprecated" }; + + + /** Private constructor to hide the implicit one. */ + private AsyncAPISchemaValidator() { + } + + /** + * Check if a Json object is valid against the given AsyncAPI schema specification. + * @param schemaText The AsyncAPI schema specification as a string + * @param jsonText The Json object as a string + * @return True if Json object is valid, false otherwise + * @throws IOException if string representations cannot be parsed + */ + public static boolean isJsonValid(String schemaText, String jsonText) throws IOException { + List errors = validateJson(schemaText, jsonText); + if (!errors.isEmpty()) { + log.debug("Get validation errors, returning false"); + return false; + } + return true; + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of AsyncAPI schema + * spec semantics regarding additional or unknown attributes: schema must explicitely set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param schemaText The AsyncAPI schema specification as a string + * @param jsonText The Json object as a string + * @return The list of validation failures. If empty, json object is valid ! + * @throws IOException if json string representations cannot be parsed + */ + public static List validateJson(String schemaText, String jsonText) throws IOException { + return validateJson(getJsonNodeForSchema(schemaText), JsonSchemaValidator.getJsonNode(jsonText)); + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of AsyncAPI schema + * spec semantics regarding additional or unknown attributes: schema must explicitely set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param schemaNode The AsyncAPI schema specification as a Jackson node + * @param jsonNode The Json object as a Jackson node + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJson(JsonNode schemaNode, JsonNode jsonNode) { + return validateJson(schemaNode, jsonNode, null); + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of AsyncAPI schema + * spec semantics regarding additional or unknown attributes: schema must explicitely set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param schemaNode The AsyncAPI schema specification as a Jackson node + * @param jsonNode The Json object as a Jackson node + * @param namespace Namespace definition to resolve relative dependencies in Json schema + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJson(JsonNode schemaNode, JsonNode jsonNode, String namespace) { + schemaNode = convertAsyncAPISchemaToJsonSchema(schemaNode); + return JsonSchemaValidator.validateJson(schemaNode, jsonNode, namespace); + } + + /** + * Validate a Json object representing an AsyncAPI message against a node representing a full AsyncAPI specification + * (and not just a schema node). Specify the message by providing a valid JSON pointer for {@code messagePathPointer} + * to allow finding the correct schema information. Validation is a deep one: its pursue checking children nodes on a + * failed parent. Validation is respectful of AsyncAPI schema spec semantics regarding additional or unknown + * attributes: schema must explicitly set {@code additionalProperties} to false if you want to consider unknown + * attributes as validation errors. It returns a list of validation error messages. + * @param specificationNode The AsyncAPI full specification as a Jackson node + * @param jsonNode The Json object representing actual message as a Jackson node + * @param messagePathPointer A JSON Pointer for accessing expected message definition within spec + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJsonMessage(JsonNode specificationNode, JsonNode jsonNode, + String messagePathPointer) { + return validateJsonMessage(specificationNode, jsonNode, messagePathPointer, null); + } + + /** + * Validate a Json object representing an AsyncAPI message against a node representing a full AsyncAPI specification + * (and not just a schema node). Specify the message by providing a valid JSON pointer for {@code messagePathPointer} + * to allow finding the correct schema information. Validation is a deep one: its pursue checking children nodes on a + * failed parent. Validation is respectful of AsyncAPI schema spec semantics regarding additional or unknown + * attributes: schema must explicitly set {@code additionalProperties} to false if you want to consider unknown + * attributes as validation errors. It returns a list of validation error messages. + * @param specificationNode The AsyncAPI full specification as a Jackson node + * @param jsonNode The Json object representing actual message as a Jackson node + * @param messagePathPointer A JSON Pointer for accessing expected message definition within spec + * @param namespace Namespace definition to resolve relative dependencies in OpenAPI schema + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJsonMessage(JsonNode specificationNode, JsonNode jsonNode, + String messagePathPointer, String namespace) { + // Extract specific content type node for message node. + JsonNode messageNode = specificationNode.at(messagePathPointer); + if (messageNode == null || messageNode.isMissingNode()) { + log.debug("messagePathPointer {} is not a valid JSON Pointer", messagePathPointer); + return List.of("messagePathPointer does not represent a valid JSON Pointer in AsyncAPI specification"); + } + // Message node can be just a reference. + messageNode = followRefIfAny(messageNode, specificationNode); + + // We need to build a node holding schema information. + JsonNode schemaNode = null; + try { + if (messageNode.isArray()) { + // In the case of AsyncAPI v3, we always got messages even if there's just one element. + // Wrapping them in oneOf, make us lose details on validation errors so just do it if necessary. + ArrayNode messagesNode = (ArrayNode) messageNode; + if (messagesNode.size() > 1) { + schemaNode = buildOneOfMessageSchemaNode(specificationNode, (ArrayNode) messageNode); + } else { + schemaNode = retrieveSingleMessageSchemaNode(specificationNode, + followRefIfAny(messagesNode.get(0), specificationNode)); + } + } else if (messageNode.has(ONE_OF_STRUCT)) { + ArrayNode oneOfMessageNode = (ArrayNode) messageNode.get(ONE_OF_STRUCT); + schemaNode = buildOneOfMessageSchemaNode(specificationNode, oneOfMessageNode); + } else { + schemaNode = retrieveSingleMessageSchemaNode(specificationNode, messageNode); + } + } catch (Exception e) { + // Just return exception message as validation message. + return Collections.singletonList(e.getMessage()); + } + // Import all the common parts that may be referenced by references. + ((ObjectNode) schemaNode).set(JSON_SCHEMA_COMPONENTS_ELEMENT, + specificationNode.path(JSON_SCHEMA_COMPONENTS_ELEMENT).deepCopy()); + + return validateJson(schemaNode, jsonNode, namespace); + } + + /** + * Validate an Avro binary representing an AsyncAPI message against a node representing a full AsyncAPI specification + * (and not just a schema node). Specify the message by providing a valid JSON pointer for {@code messagePathPointer} + * within specification to allow finding the correct schema information. Validation with avro binary is a shallow + * one: because we do not have the schema used for writing the bytes, we can only check the given bytes are fitting + * into the read schema from AsyncAPI document. It returns a list of validation error messages. + * @param specificationNode The AsyncAPI full specification as a Jackson node + * @param avroBinary The avro binary representing actual message + * @param messagePathPointer A JSON Pointer for accessing expected message definition within spec + * @param schemaMap An optional local Schema registry snapshot for resolving Avro schemas + * @return The list of validation failures. If empty, avro binary is valid ! + */ + public static List validateAvroMessage(JsonNode specificationNode, byte[] avroBinary, + String messagePathPointer, SchemaMap schemaMap) { + // Retrieve the schema to validate binary against. + Schema avroSchema; + try { + avroSchema = retrieveMessageAvroSchema(specificationNode, messagePathPointer, schemaMap); + } catch (Exception e) { + return List.of(e.getMessage()); + } + + List errors = new ArrayList<>(); + try { + // Validation is shallow: we cannot detect schema incompatibilities as we do not + // have the schema used for writing. Just checking we can read with given schema. + AvroUtil.avroToAvroRecord(avroBinary, avroSchema); + // We're satisfying at least one schema. Exit here. + return List.of(); + } catch (AvroTypeException ate) { + errors.add("Avro schema cannot be used to read message: " + ate.getMessage()); + } catch (IOException ioe) { + errors.add("IOException while trying to validate message: " + ioe.getMessage()); + } + return errors; + } + + /** + * Validate an Avro binary representing an AsyncAPI message against a node representing a full AsyncAPI specification + * (and not just a schema node). Specify the message by providing a valid JSON pointer for {@code messagePathPointer} + * within specification to allow finding the correct schema information. Validation with avro binary is a deep one: + * each element of the reading schema from AsyncAPI spec is checked in terms of type compatibility, name and + * required/optional property. It returns a list of validation error messages. + * @param specificationNode The AsyncAPI full specification as a Jackson node + * @param avroRecord The avro record representing actual message + * @param messagePathPointer A JSON Pointer for accessing expected message definition within spec + * @param schemaMap An optional local Schema registry snapshot for resolving Avro schemas + * @return The list of validation failures. If empty, avro record is valid ! + */ + public static List validateAvroMessage(JsonNode specificationNode, GenericRecord avroRecord, + String messagePathPointer, SchemaMap schemaMap) { + // Retrieve the schema to validate record against. + Schema avroSchema = null; + try { + avroSchema = retrieveMessageAvroSchema(specificationNode, messagePathPointer, schemaMap); + } catch (Exception e) { + return List.of(e.getMessage()); + } + + // Validation is a deep one. Each element should be checked. + if (AvroUtil.validate(avroSchema, avroRecord)) { + return List.of(); + } + // Produce some insights on what's going wrong. We'll accumulate the errors on different schemas. + return AvroUtil.getValidationErrors(avroSchema, avroRecord); + } + + /** + * Get a Jackson JsonNode representation for Json object. + * @param jsonText The Json object as a string + * @return The Jackson JsonNode corresponding to json object string + * @throws IOException if json string representation cannot be parsed + */ + public static JsonNode getJsonNode(String jsonText) throws IOException { + return JsonSchemaValidator.getJsonNode(jsonText); + } + + /** + * Get a Jackson JsonNode representation for AsyncAPI schema text. This handles the fact that AsyncAPI spec may be + * formatted in YAML. In that case, it handles the conversion. + * @param schemaText The JSON or YAML string for AsyncAPI schema + * @return The Jackson JsonNode corresponding to AsyncAPI schema string + * @throws IOException if schema string representation cannot be parsed + */ + public static JsonNode getJsonNodeForSchema(String schemaText) throws IOException { + boolean isYaml = true; + + // Analyse first lines of content to guess content format. + String line = null; + BufferedReader reader = new BufferedReader(new StringReader(schemaText)); + while ((line = reader.readLine()) != null) { + line = line.trim(); + // Check is we start with json object or array definition. + if (line.startsWith("{") || line.startsWith("[")) { + isYaml = false; + break; + } + if (line.startsWith("---") || line.startsWith("-") || line.startsWith("asyncapi: ")) { + break; + } + } + reader.close(); + + // Convert them to Node using Jackson object mapper. + ObjectMapper mapper = null; + if (isYaml) { + log.debug("Guessing AsyncAPI spec format is YAML"); + mapper = new ObjectMapper(new YAMLFactory()); + } else { + log.debug("Guessing AsyncAPI spec format is JSON"); + mapper = new ObjectMapper(); + } + return mapper.readTree(schemaText); + } + + /** Entry point method for converting an AsyncAPI schema node to Json schema. */ + private static JsonNode convertAsyncAPISchemaToJsonSchema(JsonNode jsonNode) { + // Convert components. + if (jsonNode.has("components")) { + convertProperties(jsonNode.path("components").path("schemas").elements()); + } + // Convert schema for all structures. + for (String structure : STRUCTURES) { + if (jsonNode.has(structure) && jsonNode.path(structure).isArray()) { + ArrayNode arrayNode = (ArrayNode) jsonNode.path(structure); + for (int i = 0; i < arrayNode.size(); i++) { + JsonNode structureNode = arrayNode.get(i); + structureNode = convertAsyncAPISchemaToJsonSchema(structureNode); + } + } + } + + // Convert properties and types. + if (jsonNode.has("type") && jsonNode.path("type").asText().equals("object")) { + convertProperties(jsonNode.path(JSON_SCHEMA_PROPERTIES_ELEMENT).elements()); + } else { + convertType(jsonNode); + } + + // Remove all unsupported attributes. + for (String notSupported : NOT_SUPPORTED_ATTRIBUTES) { + if (jsonNode.has(notSupported)) { + ((ObjectNode) jsonNode).remove(notSupported); + } + } + + return jsonNode; + } + + /** Deal with converting properties of a Json node object. */ + private static void convertProperties(Iterator properties) { + while (properties.hasNext()) { + JsonNode property = properties.next(); + property = convertAsyncAPISchemaToJsonSchema(property); + } + } + + /** Deal with converting type of a Json node object. */ + private static void convertType(JsonNode node) { + // Convert nullable in additional type and remove node. + if (node.has("type") && !node.path("type").asText().equals("object") && node.path("nullable").asBoolean()) { + String type = node.path("type").asText(); + ArrayNode typeArray = ((ObjectNode) node).putArray("type"); + typeArray.add(type).add("null"); + } + } + + /** Build a Json schema node for messages expressed as a direct oneOf structure. */ + private static JsonNode buildOneOfMessageSchemaNode(JsonNode specificationNode, ArrayNode oneOfMessageNode) + throws Exception { + // Initialize a oneOf schema with array. + ObjectMapper mapper = new ObjectMapper(); + ObjectNode schemaNode = mapper.createObjectNode(); + ArrayNode oneOfNode = mapper.createArrayNode(); + schemaNode.set(ONE_OF_STRUCT, oneOfNode); + + // Extract the schema payload for each alternative of the message. + for (int i = 0; i < oneOfMessageNode.size(); i++) { + JsonNode altMessageNode = oneOfMessageNode.get(i); + // Extract the alternative message node. + altMessageNode = followRefIfAny(altMessageNode, specificationNode); + oneOfNode.add(retrieveSingleMessageSchemaNode(specificationNode, altMessageNode)); + } + // Common parts that may be referenced by references should be imported at a upper level/ + return schemaNode; + } + + /** Retrieve the Json schema node corresponding to a single message definition. */ + private static JsonNode retrieveSingleMessageSchemaNode(JsonNode specificationNode, JsonNode messageNode) + throws Exception { + // Check that message node has a payload attribute. + if (!messageNode.has(ASYNC_SCHEMA_PAYLOAD_ELEMENT)) { + log.debug("messageNode {} has no 'payload' attribute", messageNode); + throw new Exception("message definition has no valid payload in AsyncAPI specification"); + } + // Navigate to payload definition. + messageNode = messageNode.path(ASYNC_SCHEMA_PAYLOAD_ELEMENT); + + // Payload node can be just a reference to another schema... + messageNode = followRefIfAny(messageNode, specificationNode); + + // Build a schema object with messageNode as root. + // Common parts that may be referenced by references should be imported at a upper level + return messageNode.deepCopy(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/graphql/GraphQLSchemaValidator.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/graphql/GraphQLSchemaValidator.java new file mode 100644 index 000000000..292bf021b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/graphql/GraphQLSchemaValidator.java @@ -0,0 +1,107 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import graphql.analysis.QueryTraverser; +import graphql.analysis.QueryVisitor; +import graphql.language.Document; +import graphql.parser.Parser; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import io.github.microcks.util.JsonSchemaValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Helper class for validating Json objects against their GraphQL schema. Supported version of GraphQL schema is + * https://spec.graphql.org/October2021/. + * @author laurent + */ +public class GraphQLSchemaValidator { + + /** A commons logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(GraphQLSchemaValidator.class); + + public static final String GRAPHQL_RESPONSE_DATA = "data"; + + /** Have a static mapper to avoid initialization cost. */ + private static final ObjectMapper mapper = new ObjectMapper(); + + /** + * Build a JSON Schema that should apply to a GraphQL response giving the API GraphQL SDL and the query + * specification. Query specification allows to get structure and mandatory fields from selection ; Schema allows to + * get type definition (scalar, arrays, objects). + * @param schemaText The text representation of a GraphQL Schema + * @param query The text representation of a GraphQL query + * @return The Jackson JsonNode representing the Json schema for response + * @throws IOException if parsing of either schema of query fails + */ + public static JsonNode buildResponseJsonSchema(String schemaText, String query) throws IOException { + TypeDefinitionRegistry registry = new SchemaParser().parse(schemaText); + GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(registry, RuntimeWiring.MOCKED_WIRING); + + Document graphqlRequest = new Parser().parseDocument(query); + QueryTraverser queryTraversal = QueryTraverser.newQueryTraverser().schema(schema).document(graphqlRequest) + .variables(new HashMap<>()).build(); + + ObjectNode jsonSchema = initResponseJsonSchema(); + QueryVisitor visitor = new JsonSchemaBuilderQueryVisitor( + (ObjectNode) jsonSchema.get("properties").get(GRAPHQL_RESPONSE_DATA)); + + queryTraversal.visitPreOrder(visitor); + return jsonSchema; + } + + + /** + * Commodity method: just a shortcut to JsonSchemaValidator.validateJson(schemaNode, jsonNode) + * @param schemaNode The Json schema specification as a Jackson node + * @param jsonNode The Json object as a Jackson node + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJson(JsonNode schemaNode, JsonNode jsonNode) { + return JsonSchemaValidator.validateJson(schemaNode, jsonNode); + } + + /** Initialize the schema structure of a GraphQL Json response. */ + private static ObjectNode initResponseJsonSchema() { + ObjectNode jsonSchema = mapper.createObjectNode(); + + jsonSchema.put(JsonSchemaValidator.JSON_SCHEMA_IDENTIFIER_ELEMENT, + JsonSchemaValidator.JSON_V12_SCHEMA_IDENTIFIER); + jsonSchema.put("type", "object"); + jsonSchema.put("additionalProperties", false); + jsonSchema.putArray("required").add(GRAPHQL_RESPONSE_DATA); + + ObjectNode properties = jsonSchema.putObject("properties"); + ObjectNode data = properties.putObject(GRAPHQL_RESPONSE_DATA); + data.put("type", "object"); + data.putObject("properties"); + + return jsonSchema; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/graphql/JsonSchemaBuilderQueryVisitor.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/graphql/JsonSchemaBuilderQueryVisitor.java new file mode 100644 index 000000000..c44876a39 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/graphql/JsonSchemaBuilderQueryVisitor.java @@ -0,0 +1,151 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import graphql.analysis.QueryVisitor; +import graphql.analysis.QueryVisitorFieldEnvironment; +import graphql.analysis.QueryVisitorFragmentSpreadEnvironment; +import graphql.analysis.QueryVisitorInlineFragmentEnvironment; +import graphql.language.Type; +import graphql.language.TypeName; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.idl.ScalarInfo; +import graphql.schema.idl.TypeInfo; +import graphql.schema.idl.TypeUtil; + +/** + * This is an implementation of GraphQL-Java QueryVisitor that takes care of building a Json Schema that could be + * applied to the response of a GraphQL query. + * @author laurent + */ +public class JsonSchemaBuilderQueryVisitor implements QueryVisitor { + + public static final String JSON_SCHEMA_TYPE = "type"; + public static final String JSON_SCHEMA_ENUM = "enum"; + public static final String JSON_SCHEMA_ITEMS = "items"; + public static final String JSON_SCHEMA_OBJECT_TYPE = "object"; + public static final String JSON_SCHEMA_ARRAY_TYPE = "array"; + public static final String JSON_SCHEMA_STRING_TYPE = "string"; + + public static final String JSON_SCHEMA_REQUIRED = "required"; + public static final String JSON_SCHEMA_PROPERTIES = "properties"; + public static final String JSON_SCHEMA_ADDITIONAL_PROPERTIES = "additionalProperties"; + + private ObjectNode parentNode; + private ObjectNode currentNode; + + /** + * Build a new JsonSchemaBuilderQueryVisitor. + * @param jsonSchemaData The Json Schema to complete. This node must be the /properties/data path of schema object. + */ + public JsonSchemaBuilderQueryVisitor(ObjectNode jsonSchemaData) { + this.parentNode = jsonSchemaData; + this.currentNode = (ObjectNode) jsonSchemaData.path(JSON_SCHEMA_PROPERTIES); + } + + @Override + public void visitField(QueryVisitorFieldEnvironment queryVisitorFieldEnvironment) { + + // Each new property should put as required and we should not allow additional properties. + ArrayNode required = getRequiredArrayNode(); + required.add(queryVisitorFieldEnvironment.getFieldDefinition().getName()); + + // Even if type is marked as optional in the GraphQL Schema, it must be present and + // serialized as null into the Json response. We have to unwrap it first. + GraphQLOutputType outputType = queryVisitorFieldEnvironment.getFieldDefinition().getType(); + Type definitionType = queryVisitorFieldEnvironment.getFieldDefinition().getDefinition().getType(); + if (TypeUtil.isNonNull(definitionType)) { + definitionType = TypeUtil.unwrapOne(definitionType); + } + + // Add this field to current node. + ObjectNode fieldNode = currentNode.putObject(queryVisitorFieldEnvironment.getFieldDefinition().getName()); + + TypeInfo definitionTypeInfo = TypeInfo.typeInfo(definitionType); + + // Treat most common case first: we've got a scalar property. + if (ScalarInfo.isGraphqlSpecifiedScalar(definitionTypeInfo.getName())) { + fieldNode.put(JSON_SCHEMA_TYPE, getJsonScalarType(definitionTypeInfo.getName())); + } else if (outputType instanceof GraphQLObjectType) { + // Then we deal with objects. + fieldNode.put(JSON_SCHEMA_TYPE, JSON_SCHEMA_OBJECT_TYPE); + ObjectNode properties = fieldNode.putObject(JSON_SCHEMA_PROPERTIES); + parentNode.put(JSON_SCHEMA_ADDITIONAL_PROPERTIES, false); + fieldNode.put(JSON_SCHEMA_ADDITIONAL_PROPERTIES, false); + parentNode = fieldNode; + currentNode = properties; + } else if (TypeUtil.isList(definitionType)) { + // Then we deal with lists. + fieldNode.put(JSON_SCHEMA_TYPE, JSON_SCHEMA_ARRAY_TYPE); + ObjectNode items = fieldNode.putObject(JSON_SCHEMA_ITEMS); + + // Depending on item type, we should initialize an object structure. + TypeName itemTypeInfo = TypeUtil.unwrapAll(definitionType); + if (!ScalarInfo.isGraphqlSpecifiedScalar(itemTypeInfo.getName())) { + items.put(JSON_SCHEMA_TYPE, JSON_SCHEMA_OBJECT_TYPE); + ObjectNode properties = items.putObject(JSON_SCHEMA_PROPERTIES); + items.put(JSON_SCHEMA_ADDITIONAL_PROPERTIES, false); + parentNode = items; + currentNode = properties; + } + } else if (outputType instanceof GraphQLEnumType enumType) { + // Then we deal with enumerations. + fieldNode.put(JSON_SCHEMA_TYPE, JSON_SCHEMA_STRING_TYPE); + ArrayNode enumNode = fieldNode.putArray(JSON_SCHEMA_ENUM); + + for (GraphQLEnumValueDefinition valDef : enumType.getValues()) { + enumNode.add(valDef.getName()); + } + } + } + + @Override + public void visitInlineFragment(QueryVisitorInlineFragmentEnvironment queryVisitorInlineFragmentEnvironment) { + // Nothing to do when visiting a fragment. + } + + @Override + public void visitFragmentSpread(QueryVisitorFragmentSpreadEnvironment queryVisitorFragmentSpreadEnvironment) { + // Nothing to do when visiting a spread. + } + + private ArrayNode getRequiredArrayNode() { + JsonNode required = parentNode.get(JSON_SCHEMA_REQUIRED); + if (required == null) { + required = parentNode.putArray(JSON_SCHEMA_REQUIRED); + } + return (ArrayNode) required; + } + + private String getJsonScalarType(String graphqlScalarType) { + switch (graphqlScalarType) { + case "Int": + return "integer"; + case "Float": + return "number"; + case "ID": + return JSON_SCHEMA_STRING_TYPE; + default: + return graphqlScalarType.toLowerCase(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/OpenAPISchemaBuilder.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/OpenAPISchemaBuilder.java new file mode 100644 index 000000000..4e10342df --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/OpenAPISchemaBuilder.java @@ -0,0 +1,108 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.github.microcks.util.JsonSchemaValidator; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; + +/** + * Helper class for building/inferring a Json Schema from Json node object. + * @author laurent + */ +public class OpenAPISchemaBuilder { + + public static final String JSON_SCHEMA_TYPE = "type"; + public static final String JSON_SCHEMA_ITEMS = "items"; + public static final String JSON_SCHEMA_OBJECT_TYPE = "object"; + public static final String JSON_SCHEMA_ARRAY_TYPE = "array"; + public static final String JSON_SCHEMA_PROPERTIES = "properties"; + + + private OpenAPISchemaBuilder() { + // Hide the implicit default constructor. + } + + /** + * Build the Json schema for the type corresponding to given Json node provided as string. + * @param jsonText The String representing Json node to introspect for building a Json schema + * @return A JsonNode representing the Json Schema fragment for this type + * @throws IOException Tf given Json string cannot be parsed as Json + */ + public static JsonNode buildTypeSchemaFromJson(String jsonText) throws IOException { + return buildTypeSchemaFromJson(JsonSchemaValidator.getJsonNode(jsonText)); + } + + /** + * Build the Json schema for the type corresponding to given Json node. + * @param node The Jackson Json node to introspect for building a Json schema + * @return A JsonNode representing the Json Schema fragment for this type + */ + public static JsonNode buildTypeSchemaFromJson(JsonNode node) { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode schemaNode = mapper.createObjectNode(); + + traverseNode(node, schemaNode, mapper); + return schemaNode; + } + + /** Travers a node and complete its schema from schema parent node. */ + private static void traverseNode(JsonNode currentNode, ObjectNode parentNodeSchemaNode, ObjectMapper mapper) { + switch (currentNode.getNodeType()) { + case OBJECT: + parentNodeSchemaNode.put(JSON_SCHEMA_TYPE, JSON_SCHEMA_OBJECT_TYPE); + ObjectNode propertiesNodeSchemaNode = parentNodeSchemaNode.putObject(JSON_SCHEMA_PROPERTIES); + + Iterator> fieldsNodes = currentNode.fields(); + while (fieldsNodes.hasNext()) { + Map.Entry fieldsNode = fieldsNodes.next(); + ObjectNode fieldNodeSchemaNode = propertiesNodeSchemaNode.putObject(fieldsNode.getKey()); + traverseNode(fieldsNode.getValue(), fieldNodeSchemaNode, mapper); + } + break; + case ARRAY: + parentNodeSchemaNode.put(JSON_SCHEMA_TYPE, JSON_SCHEMA_ARRAY_TYPE); + ObjectNode itemsNodeSchemaNode = parentNodeSchemaNode.putObject(JSON_SCHEMA_ITEMS); + + JsonNode firstChild = ((ArrayNode) currentNode).elements().next(); + traverseNode(firstChild, itemsNodeSchemaNode, mapper); + break; + default: + parentNodeSchemaNode.put(JSON_SCHEMA_TYPE, getJsonSchemaScalarType(currentNode.getNodeType())); + } + } + + /** Return the actual Json schema type for given scalar type from Jackson internals. */ + private static String getJsonSchemaScalarType(JsonNodeType nodeType) { + switch (nodeType) { + case STRING: + return "string"; + case BOOLEAN: + return "boolean"; + case NUMBER: + return "number"; + default: + return nodeType.toString().toLowerCase(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/OpenAPISchemaValidator.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/OpenAPISchemaValidator.java new file mode 100644 index 000000000..03a00cdb1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/OpenAPISchemaValidator.java @@ -0,0 +1,403 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.github.microcks.util.JsonSchemaValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.LoaderOptions; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.github.microcks.util.JsonSchemaValidator.*; + +/** + * Helper class for validating Json objects against their OpenAPI schema. Supported version of OpenAPI schema is + * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md. + * @author laurent + */ +public class OpenAPISchemaValidator { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(OpenAPISchemaValidator.class); + + private static final String ONE_OF = "oneOf"; + private static final String NULLABLE = "nullable"; + private static final String[] STRUCTURES = { "allOf", "anyOf", ONE_OF, "not", "items", "additionalProperties" }; + private static final String[] COMPOSITION_STRUCTURES = { "allOf", "anyOf", ONE_OF }; + + private static final String[] NOT_SUPPORTED_ATTRIBUTES = { NULLABLE, "discriminator", "readOnly", "writeOnly", "xml", + "externalDocs", "example", "deprecated" }; + + + /** Private constructor to hide the implicit one. */ + private OpenAPISchemaValidator() { + } + + /** + * Check if a Json object is valid against the given OpenAPI schema specification. + * @param schemaText The OpenAPI schema specification as a string + * @param jsonText The Json object as a string + * @return True if Json object is valid, false otherwise + * @throws IOException if string representations cannot be parsed + */ + public static boolean isJsonValid(String schemaText, String jsonText) throws IOException { + return isJsonValid(schemaText, jsonText, null); + } + + + /** + * Check if a Json object is valid against the given OpenAPI schema specification. + * @param schemaText The OpenAPI schema specification as a string + * @param jsonText The Json object as a string + * @param namespace Namespace definition to resolve relative dependencies in Json schema + * @return True if Json object is valid, false otherwise + * @throws IOException if string representations cannot be parsed + */ + public static boolean isJsonValid(String schemaText, String jsonText, String namespace) throws IOException { + List errors = validateJson(schemaText, jsonText, namespace); + if (!errors.isEmpty()) { + log.debug("Get validation errors, returning false"); + return false; + } + return true; + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of OpenAPI schema + * spec semantics regarding additional or unknown attributes: schema must explicitly set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param schemaText The OpenAPI schema specification as a string + * @param jsonText The Json object as a string + * @return The list of validation failures. If empty, json object is valid ! + * @throws IOException if json string representations cannot be parsed + */ + public static List validateJson(String schemaText, String jsonText) throws IOException { + return validateJson(schemaText, jsonText, null); + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of OpenAPI schema + * spec semantics regarding additional or unknown attributes: schema must explicitly set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param schemaText The OpenAPI schema specification as a string + * @param jsonText The Json object as a string + * @param namespace Namespace definition to resolve relative dependencies in Json schema + * @return The list of validation failures. If empty, json object is valid ! + * @throws IOException if json string representations cannot be parsed + */ + public static List validateJson(String schemaText, String jsonText, String namespace) throws IOException { + return validateJson(getJsonNodeForSchema(schemaText), JsonSchemaValidator.getJsonNode(jsonText), namespace); + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of OpenAPI schema + * spec semantics regarding additional or unknown attributes: schema must explicitly set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param schemaNode The OpenAPI schema specification as a Jackson node + * @param jsonNode The Json object as a Jackson node + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJson(JsonNode schemaNode, JsonNode jsonNode) { + return validateJson(schemaNode, jsonNode, null); + } + + /** + * Validate a Json object representing by its text against a schema object representing byt its text too. Validation + * is a deep one: its pursue checking children nodes on a failed parent. Validation is respectful of OpenAPI schema + * spec semantics regarding additional or unknown attributes: schema must explicitly set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param schemaNode The OpenAPI schema specification as a Jackson node + * @param jsonNode The Json object as a Jackson node + * @param namespace Namespace definition to resolve relative dependencies in Json schema + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJson(JsonNode schemaNode, JsonNode jsonNode, String namespace) { + schemaNode = convertOpenAPISchemaToJsonSchema(schemaNode); + return JsonSchemaValidator.validateJson(schemaNode, jsonNode, namespace); + } + + /** + * Validate a Json object representing an OpenAPI message (response or request) against a node representing a full + * OpenAPI specification (and not just a schema node). Specify the message by providing a valid JSON pointer for + * messagePathPointer within specification and a contentType to allow finding the correct + * schema information. Validation is a deep one: its pursue checking children nodes on a failed parent. Validation is + * respectful of OpenAPI schema spec semantics regarding additional or unknown attributes: schema must explicitly set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param specificationNode The OpenAPI full specification as a Jackson node + * @param jsonNode The Json object representing actual message as a Jackson node + * @param messagePathPointer A JSON Pointer for accessing expected message definition within spec + * @param contentType The Content-Type of the message to valid + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJsonMessage(JsonNode specificationNode, JsonNode jsonNode, + String messagePathPointer, String contentType) { + return validateJsonMessage(specificationNode, jsonNode, messagePathPointer, contentType, null); + } + + /** + * Validate a Json object representing an OpenAPI message (response or request) against a node representing a full + * OpenAPI specification (and not just a schema node). Specify the message by providing a valid JSON pointer for + * messagePathPointer within specification and a contentType to allow finding the correct + * schema information. Validation is a deep one: its pursue checking children nodes on a failed parent. Validation is + * respectful of OpenAPI schema spec semantics regarding additional or unknown attributes: schema must explicitly set + * additionalProperties to false if you want to consider unknown attributes as validation errors. It + * returns a list of validation error messages. + * @param specificationNode The OpenAPI full specification as a Jackson node + * @param jsonNode The Json object representing actual message as a Jackson node + * @param messagePathPointer A JSON Pointer for accessing expected message definition within spec + * @param contentType The Content-Type of the message to valid + * @param namespace Namespace definition to resolve relative dependencies in OpenAPI schema + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJsonMessage(JsonNode specificationNode, JsonNode jsonNode, + String messagePathPointer, String contentType, String namespace) { + // Extract specific content type node for message node. + JsonNode messageNode = specificationNode.at(messagePathPointer); + if (messageNode == null || messageNode.isMissingNode()) { + log.debug("messagePathPointer {} is not a valid JSON Pointer", messagePathPointer); + return List.of("messagePathPointer does not represent a valid JSON Pointer in OpenAPI specification"); + } + // Message node can be just a reference. + if (messageNode.has("$ref")) { + String ref = messageNode.path("$ref").asText(); + messageNode = specificationNode.at(ref.substring(1)); + } + // Extract message corresponding to contentType. + messageNode = getMessageContentNode(messageNode, contentType); + if (messageNode == null || messageNode.isMissingNode()) { + log.debug("content for {} cannot be found into OpenAPI specification", contentType); + return List.of("messagePathPointer does not represent an existing JSON Pointer in OpenAPI specification"); + } + + // Build a schema object with responseNode schema as root and by importing + // all the common parts that may be referenced by references. + JsonNode schemaNode = messageNode.path("schema").deepCopy(); + if (schemaNode == null || schemaNode.isMissingNode()) { + log.debug("schema for {} cannot be found into OpenAPI specification", messageNode); + return List.of("schemaPathPointer does not represent an existing JSON Pointer in OpenAPI specification"); + } + ((ObjectNode) schemaNode).set(JSON_SCHEMA_COMPONENTS_ELEMENT, + specificationNode.path(JSON_SCHEMA_COMPONENTS_ELEMENT).deepCopy()); + + return validateJson(schemaNode, jsonNode, namespace); + } + + /** + * Get a Jackson JsonNode representation for Json object. + * @param jsonText The Json object as a string + * @return The Jackson JsonNode corresponding to json object string + * @throws IOException if json string representation cannot be parsed + */ + public static JsonNode getJsonNode(String jsonText) throws IOException { + return JsonSchemaValidator.getJsonNode(jsonText); + } + + /** + * Get a Jackson JsonNode representation for OpenAPI schema text. This handles the fact that OpenAPI spec may be + * formatted in YAML. In that case, it handles the conversion. + * @param schemaText The JSON or YAML string for OpenAPI schema + * @return The Jackson JsonNode corresponding to OpenAPI schema string + * @throws IOException if schema string representation cannot be parsed + */ + public static JsonNode getJsonNodeForSchema(String schemaText) throws IOException { + boolean isYaml = true; + + // Analyse first lines of content to guess content format. + String line = null; + int lineNumber = 0; + BufferedReader reader = new BufferedReader(new StringReader(schemaText)); + while ((line = reader.readLine()) != null) { + line = line.trim(); + // Check is we start with json object or array definition. + if (lineNumber == 0 && (line.startsWith("{") || line.startsWith("["))) { + isYaml = false; + break; + } + if (line.startsWith("---") || line.startsWith("-") || line.startsWith("openapi: ")) { + break; + } + lineNumber++; + } + reader.close(); + + // Convert them to Node using Jackson object mapper. + ObjectMapper mapper = null; + if (isYaml) { + log.debug("Guessing OpenAPI spec format is YAML"); + LoaderOptions options = new LoaderOptions(); + // If schema is too big, increase the code point limit to avoid exception. + // Default is 3MB hard coded in Snake Yaml, we set it to bytes length + 256. + if (schemaText.getBytes(StandardCharsets.UTF_8).length > 3 * 1024 * 1024) { + log.warn("OpenAPI schema is too big, increasing code point limit to 9MB"); + options.setCodePointLimit(schemaText.getBytes(StandardCharsets.UTF_8).length + 256); + } + mapper = new ObjectMapper(YAMLFactory.builder().loaderOptions(options).build()); + } else { + log.debug("Guessing OpenAPI spec format is JSON"); + mapper = new ObjectMapper(); + } + return mapper.readTree(schemaText); + } + + protected static JsonNode getMessageContentNode(JsonNode responseCodeNode, String contentType) { + JsonNode contentNode = responseCodeNode.at("/content/" + contentType.replace("/", "~1")); + if (contentNode == null || contentNode.isMissingNode()) { + // If no exact matching, try loose matching browsing the content types. + // We may have 'application/json; charset=utf-8' on one side and 'application/json;charset=UTF-8' on the other. + Iterator> contents = responseCodeNode.path("content").fields(); + while (contents.hasNext()) { + Map.Entry contentTypeNode = contents.next(); + if (contentTypeNode.getKey().replace(" ", "").equalsIgnoreCase(contentType.replace(" ", ""))) { + return contentTypeNode.getValue(); + } + } + + // If no match here, it's maybe contentType contains charset but not the spec. + // Remove charset information and try again. + if (contentType.contains("charset=") && contentType.indexOf(";") > 0) { + return getMessageContentNode(responseCodeNode, contentType.substring(0, contentType.indexOf(";"))); + } + } + return contentNode; + } + + /** Entry point method for converting an OpenAPI schema node to Json schema. */ + private static JsonNode convertOpenAPISchemaToJsonSchema(JsonNode jsonNode) { + // Convert components. + if (jsonNode.has(JSON_SCHEMA_COMPONENTS_ELEMENT)) { + convertProperties(jsonNode.path(JSON_SCHEMA_COMPONENTS_ELEMENT).path("schemas").elements()); + } + // Convert schema for all structures. + for (String structure : STRUCTURES) { + if (jsonNode.has(structure) && jsonNode.path(structure).isArray()) { + ArrayNode arrayNode = (ArrayNode) jsonNode.path(structure); + for (int i = 0; i < arrayNode.size(); i++) { + JsonNode structureNode = arrayNode.get(i); + structureNode = convertOpenAPISchemaToJsonSchema(structureNode); + } + } + } + + // Convert properties and types. + if (jsonNode.has("type") && jsonNode.path("type").asText().equals("object")) { + convertProperties(jsonNode.path(JSON_SCHEMA_PROPERTIES_ELEMENT).elements()); + } else { + convertType(jsonNode); + } + + // Remove all unsupported attributes. + for (String notSupported : NOT_SUPPORTED_ATTRIBUTES) { + if (jsonNode.has(notSupported)) { + ((ObjectNode) jsonNode).remove(notSupported); + } + } + + return jsonNode; + } + + /** Deal with converting properties of a Json node object. */ + private static void convertProperties(Iterator properties) { + while (properties.hasNext()) { + JsonNode property = properties.next(); + property = convertOpenAPISchemaToJsonSchema(property); + } + } + + private static Optional getCompositionStructureType(JsonNode node) { + for (var current : COMPOSITION_STRUCTURES) { + if (node.has(current)) { + return Optional.of(current); + } + } + return Optional.empty(); + } + + private static boolean isOneOfNullable(ArrayNode oneOf) { + for (Iterator it = oneOf.iterator(); it.hasNext();) { + JsonNode current = it.next(); + if (current.isObject() && ((ObjectNode) current).has("type") + && ((ObjectNode) current).get("type").asText().equals("null")) { + return true; + } + } + return false; + } + + /** Deal with converting type of a Json node object. */ + private static void convertType(JsonNode node) { + if (node.has("type") && !node.path("type").asText().equals("object")) { + + // Convert nullable in additional type and remove node. + if (node.path(NULLABLE).asBoolean()) { + String type = node.path("type").asText(); + ArrayNode typeArray = ((ObjectNode) node).putArray("type"); + typeArray.add(type).add("null"); + } + } + + // Handle OneOf, AnyOf & AllOf. + if (node.path(NULLABLE).asBoolean()) { + Optional maybeStructure = getCompositionStructureType(node); + if (maybeStructure.isPresent()) { + String structure = maybeStructure.get(); + if (structure.equals(ONE_OF)) { + // Append null type to oneOf if it's not already there. + var oneOf = ((ArrayNode) node.path(ONE_OF)); + if (!isOneOfNullable(oneOf)) { + ObjectNode nullNode = new ObjectMapper().createObjectNode(); + nullNode.put("type", "null"); + ((ArrayNode) node.path(ONE_OF)).add(nullNode); + } + } else { + // Nesting current structure inside a OneOf. + ObjectNode allOfChildObject = new ObjectMapper().createObjectNode(); + allOfChildObject.set(structure, node.path(structure)); + ((ObjectNode) node).remove(structure); + + ((ObjectNode) node).putArray(ONE_OF); + ((ArrayNode) node.path(ONE_OF)).add(allOfChildObject); + + //Adding null type to oneOf structure + ObjectNode nullNode = new ObjectMapper().createObjectNode(); + nullNode.put("type", "null"); + ((ArrayNode) node.path(ONE_OF)).add(nullNode); + } + } + } + + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/SwaggerSchemaValidator.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/SwaggerSchemaValidator.java new file mode 100644 index 000000000..2b46ae19f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/openapi/SwaggerSchemaValidator.java @@ -0,0 +1,95 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Helper class for validating Json objects against their Swagger schema. Supported version of Swagger schema is + * https://swagger.io/specification/v2/. + * @author laurent + */ +public class SwaggerSchemaValidator { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(SwaggerSchemaValidator.class); + + + /** Private constructor to hide the implicit one. */ + private SwaggerSchemaValidator() { + } + + /** + * Validate a Json object representing a Swagger message (response or request) against a node representing a full + * OpenAPI specification (and not just a schema node). Specify the message by providing a valid JSON pointer for + * messagePathPointer within specification. Validation is a deep one: its pursue checking children nodes + * on a failed parent. Validation is respectful of Swagger schema spec semantics regarding additional or unknown + * attributes: schema must explicitly set additionalProperties to false if you want to consider unknown + * attributes as validation errors. It returns a list of validation error messages. + * @param specificationNode The Swagger full specification as a Jackson node + * @param jsonNode The Json object representing actual message as a Jackson node + * @param messagePathPointer A JSON Pointer for accessing expected message definition within spec + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJsonMessage(JsonNode specificationNode, JsonNode jsonNode, + String messagePathPointer) { + return validateJsonMessage(specificationNode, jsonNode, messagePathPointer, null); + } + + /** + * Validate a Json object representing a Swagger message (response or request) against a node representing a full + * Swagger specification (and not just a schema node). Specify the message by providing a valid JSON pointer for + * messagePathPointer within specification. Validation is a deep one: its pursue checking children nodes + * on a failed parent. Validation is respectful of Swagger schema spec semantics regarding additional or unknown + * attributes: schema must explicitly set additionalProperties to false if you want to consider unknown + * attributes as validation errors. It returns a list of validation error messages. + * @param specificationNode The Swagger full specification as a Jackson node + * @param jsonNode The Json object representing actual message as a Jackson node + * @param messagePathPointer A JSON Pointer for accessing expected message definition within spec + * @param namespace Namespace definition to resolve relative dependencies in Swagger schema + * @return The list of validation failures. If empty, json object is valid ! + */ + public static List validateJsonMessage(JsonNode specificationNode, JsonNode jsonNode, + String messagePathPointer, String namespace) { + // Extract specific content type node for message node. + JsonNode messageNode = specificationNode.at(messagePathPointer); + if (messageNode == null || messageNode.isMissingNode()) { + log.debug("messagePathPointer {} is not a valid JSON Pointer", messagePathPointer); + return List.of("messagePathPointer does not represent a valid JSON Pointer in OpenAPI specification"); + } + // Message node can be just a reference. + if (messageNode.has("$ref")) { + String ref = messageNode.path("$ref").asText(); + messageNode = specificationNode.at(ref.substring(1)); + } + if (messageNode == null || messageNode.isMissingNode()) { + log.debug("Schema node for message cannot be found into Swagger specification"); + return List.of("messagePathPointer does not represent an existing JSON Pointer in OpenAPI specification"); + } + + // Build a schema object with responseNode schema as root and by importing + // all the definitions parts that may be referenced by references. + JsonNode schemaNode = messageNode.path("schema").deepCopy(); + ((ObjectNode) schemaNode).set("definitions", specificationNode.path("definitions").deepCopy()); + + return OpenAPISchemaValidator.validateJson(schemaNode, jsonNode, namespace); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/soap/SoapMessageValidator.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/soap/SoapMessageValidator.java new file mode 100644 index 000000000..5d3884d0b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/java/io/github/microcks/util/soap/SoapMessageValidator.java @@ -0,0 +1,198 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soap; + +import io.github.microcks.util.XmlSchemaValidator; +import io.github.microcks.util.XmlUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper class for validating Soap messages objects against their WSDL/XSD schemas. Supported versions of Soap are Soap + * 1.1 and Soap 1.2. + * @author laurent + */ +public class SoapMessageValidator { + + + /** A commons logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SoapMessageValidator.class); + + private static final String SOAP_ENVELOPE_SCHEMA = "soap-envelope.xsd"; + + private static final String SOAP_ENVELOPE_12_SCHEMA = "soap-envelope-12.xsd"; + + /** Soap 1.1 envelope public namespace. */ + public static final String SOAP_ENVELOPE_NS = "http://schemas.xmlsoap.org/soap/envelope/"; + /** Soap 1.2 envelope public namespace. */ + public static final String SOAP_ENVELOPE_12_NS = "http://www.w3.org/2003/05/soap-envelope"; + + private static final Pattern XML_NS_CAPTURE_PATTERN = Pattern.compile("xmlns:(\\w+)=\"([^\"]*)\"", Pattern.DOTALL); + + + private SoapMessageValidator() { + // Hide the implicit default constructor. + } + + /** + * Validate that given message has a well-formed Soap Envelope. This could Soap 1.1 or 1.2 envelope. + * @param message The Soap message to validate. + * @return A list of validation error that's empty if everything's ok. + */ + public static List validateSoapEnvelope(String message) { + List errors = new ArrayList<>(); + + InputStream schemaStream; + if (message.contains(SOAP_ENVELOPE_12_NS)) { + schemaStream = SoapMessageValidator.class.getClassLoader().getResourceAsStream(SOAP_ENVELOPE_12_SCHEMA); + } else if (message.contains(SOAP_ENVELOPE_NS)) { + schemaStream = SoapMessageValidator.class.getClassLoader().getResourceAsStream(SOAP_ENVELOPE_SCHEMA); + } else { + errors.add("Soap envelope does not appear to be valid: unrecognized namespace"); + return errors; + } + + try { + errors.addAll(XmlSchemaValidator.validateXml(schemaStream, message)); + } catch (Exception e) { + log.error("Exception while validating Soap envelope for message: {}", e.getMessage()); + errors.add("Exception while validating Soap envelope for message: " + e.getMessage()); + } + + log.debug("SoapEnvelope validation errors: {}", errors.size()); + return errors; + } + + /** + * Validate a complete Soap message: Soap envelope + payload included into Soap body against a WSDL content. + * @param wsdlContent The reference WSDL as a String + * @param partQName The qualified name of the element that is expected into Soap body. + * @param message The Soap message as a String + * @param resourceUrl AN optional resource url where includes in WSDL may be resolved + * @return A list of validation error that's empty if everything's ok. + */ + public static List validateSoapMessage(String wsdlContent, QName partQName, String message, + String resourceUrl) { + List errors = new ArrayList<>(); + + // Start validating envelope. + errors.addAll(validateSoapEnvelope(message)); + + try { + // First thing is to parse WSDL to extract types schema. + DocumentBuilderFactory factory = DocumentBuilderFactory.newDefaultInstance(); + factory.setNamespaceAware(true); + DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + + Element wsdlElement = documentBuilder.parse(new InputSource(new StringReader(wsdlContent))) + .getDocumentElement(); + Element wsdlTypes = XmlUtil.getUniqueDirectChild(wsdlElement, XmlUtil.WSDL_NS, "types"); + Element xsSchema = XmlUtil.getUniqueDirectChild(wsdlTypes, XmlUtil.XML_SCHEMA_NS, "schema"); + + // Schema may not contain all the needed xmlns declaration found at the definitions level. + // We have to report it before extracting a standalone schema. + NamedNodeMap definitionsAttributes = wsdlElement.getAttributes(); + for (int i = 0; i < definitionsAttributes.getLength(); i++) { + Node attribute = definitionsAttributes.item(i); + String name = attribute.getNodeName(); + if (name.startsWith("xmlns:") && !xsSchema.hasAttribute(name)) { + xsSchema.setAttribute(name, attribute.getNodeValue()); + } + } + + TransformerFactory transformerFactory = TransformerFactory.newDefaultInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + + Writer out = new StringWriter(); + transformer.transform(new DOMSource(xsSchema), new StreamResult(out)); + + String schemaContent = out.toString(); + + // Before extracting and validating body, we should capture all namespaces declaration to keep record + // for later reinject them into body. + Matcher nsMatcher = XML_NS_CAPTURE_PATTERN.matcher(message); + Map nsToPrefix = new HashMap<>(); + StringBuilder nsBuilder = new StringBuilder(); + while (nsMatcher.find()) { + nsBuilder.append(" ").append(nsMatcher.group(0)); + nsToPrefix.put(nsMatcher.group(2), nsMatcher.group(1)); + } + + // Then we have to extract body payload from soap envelope. We'll use no regexp for that. + String body = message; + int bodyStart = body.indexOf(":Body"); + int bodyEnd = body.lastIndexOf(":Body>"); + body = body.substring(bodyStart + 5, bodyEnd); + int firstClosingTag = body.indexOf(">"); + int lastClosingTag = body.lastIndexOf("')) + nsBuilder + body.substring(body.indexOf('>')); + + log.debug("Soap message body to validate: {}", body); + + // Check soap message if the expected part. + String expectedStartTag = "<" + nsToPrefix.get(partQName.getNamespaceURI()) + ":" + partQName.getLocalPart(); + String expectedEndTag = ""; + if (!body.startsWith(expectedStartTag) || !body.endsWith(expectedEndTag)) { + errors.add("Expecting a " + partQName + " element but got another one"); + } + + // And finally validate everything using that body ;-) + errors.addAll(XmlSchemaValidator.validateXml( + new ByteArrayInputStream(schemaContent.getBytes(StandardCharsets.UTF_8)), body, resourceUrl)); + } catch (Exception e) { + log.error("Exception while validating Soap message: {}", e.getMessage()); + errors.add("Exception while validating Soap message: " + e.getMessage()); + } + + log.debug("SoapMessage validation errors: {}", errors.size()); + return errors; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/soap-envelope-12.xsd b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/soap-envelope-12.xsd new file mode 100644 index 000000000..25d8d309a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/soap-envelope-12.xsd @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + Elements replacing the wildcard MUST be namespace qualified, but can be in the targetNamespace + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fault reporting structure + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/soap-envelope.xsd b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/soap-envelope.xsd new file mode 100644 index 000000000..72bf21a53 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/soap-envelope.xsd @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prose in the spec does not specify that attributes are allowed on the Body element + + + + + + + + + + + + + + + + + + + + + + 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing + element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern + described in SOAP specification + + + + + + + + + + + + + + + + Fault reporting structure + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/xml.xsd b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/xml.xsd new file mode 100644 index 000000000..c7be791a7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/main/resources/xml.xsd @@ -0,0 +1,286 @@ + + + + + + +
+

About the XML namespace

+ +
+

+ This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

+

+ See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +

+

+ Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

+

+ See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +

+
+
+
+
+ + + + +
+ +

lang (as an attribute name)

+

+ denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

+ +
+
+

Notes

+

+ Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

+

+ See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +

+

+ The union allows for the 'un-declaration' of xml:lang with + the empty string. +

+
+
+
+ + + + + + + + + +
+ + + + +
+ +

space (as an attribute name)

+

+ denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

+ +
+
+
+ + + + + + +
+ + + +
+ +

base (as an attribute name)

+

+ denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

+ +

+ See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

+
+
+
+
+ + + + +
+ +

id (as an attribute name)

+

+ denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

+ +

+ See http://www.w3.org/TR/xml-id/ + for information about this attribute. +

+
+
+
+
+ + + + + + + + + + +
+ +

Father (in any context at all)

+ +
+

+ denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

+
+

+ In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

+
+
+
+
+
+ + + +
+

About this schema document

+ +
+

+ This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow xml:base, + xml:lang, xml:space or + xml:id attributes on elements they define. +

+

+ To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

+
+            <schema . . .>
+            . . .
+            <import namespace="http://www.w3.org/XML/1998/namespace"
+            schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+          
+

+ or +

+
+            <import namespace="http://www.w3.org/XML/1998/namespace"
+            schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+          
+

+ Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

+
+            <type . . .>
+            . . .
+            <attributeGroup ref="xml:specialAttrs"/>
+          
+

+ will define a type which will schema-validate an instance element + with any of those attributes. +

+
+
+
+
+ + + +
+

Versioning policy for this schema document

+
+

+ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

+

+ At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

+

+ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

+

+ Previous dated (and unchanging) versions of this schema + document are at: +

+ +
+
+
+
+ +
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/AvroUtilTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/AvroUtilTest.java new file mode 100644 index 000000000..3111e1dab --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/AvroUtilTest.java @@ -0,0 +1,301 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.apache.avro.Schema; +import org.apache.avro.SchemaBuilder; +import org.apache.avro.SchemaCompatibility; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericDatumReader; +import org.apache.avro.generic.GenericDatumWriter; +import org.apache.avro.generic.GenericRecord; +import org.apache.avro.io.DatumReader; +import org.apache.avro.io.DatumWriter; +import org.apache.avro.io.Decoder; +import org.apache.avro.io.DecoderFactory; +import org.apache.avro.io.Encoder; +import org.apache.avro.io.EncoderFactory; +import org.junit.jupiter.api.Test; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case method for AvroUtil class. + * @author laurent + */ +class AvroUtilTest { + + @Test + void testAvroBasics() { + Schema schema = null; + + try { + // Load schema from file. + schema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/user-signedup-bad.avsc")); + + GenericRecord user1 = new GenericData.Record(schema); + user1.put("name", "Laurent"); + user1.put("email", "laurent@microcks.io"); + user1.put("age", 41); + + GenericRecord user2 = new GenericData.Record(schema); + user2.put("name", "John"); + user2.put("email", "john@microcks.io"); + user2.put("age", 23); + + // Serialize using Json encoding. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(new BufferedOutputStream(baos)); + + DatumWriter datumWriter = new GenericDatumWriter(schema); + Encoder encoder = EncoderFactory.get().jsonEncoder(schema, out, false); + datumWriter.write(user1, encoder); + datumWriter.write(user2, encoder); + encoder.flush(); + String jsonEncoding = baos.toString("UTF-8"); + System.err.println("jsonEncoding: \n" + jsonEncoding); + + // Serialize using binary encoding. + baos = new ByteArrayOutputStream(); + out = new PrintStream(new BufferedOutputStream(baos)); + encoder = EncoderFactory.get().binaryEncoder(out, null); + datumWriter.write(user1, encoder); + datumWriter.write(user2, encoder); + encoder.flush(); + byte[] binaryRepresentation = baos.toByteArray(); + String binaryEncoding = new String(binaryRepresentation, "UTF-8"); + System.err.println("\nbinaryEncoding: \n" + binaryEncoding); + + // Deserialize from binary encoding representation. + DatumReader datumReader = new GenericDatumReader(schema); + GenericRecord user = null; + Decoder decoder = DecoderFactory.get().binaryDecoder(binaryRepresentation, null); + + try { + while (true) { + user = datumReader.read(user, decoder); + System.err.println("User from binary representation: " + user); + } + } catch (EOFException eofException) { + // Nothing to do here, just exit the while loop. + } + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testJsonToAvro() { + String jsonText = "{\"name\":\"Laurent Broudoux\", \"email\":\"laurent@microcks.io\", \"age\":41}"; + + try { + // Load schema from file. + Schema schema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/user-signedup-bad.avsc")); + + // Convert back and forth to and from JSON. + byte[] avroBinary = AvroUtil.jsonToAvro(jsonText, schema); + System.err.println("binaryEncoding: \n" + new String(avroBinary, "UTF-8")); + String jsonRepresentation = AvroUtil.avroToJson(avroBinary, schema); + System.err.println("\njsonRepresentation: \n" + jsonRepresentation); + + assertTrue(jsonRepresentation.contains("\"Laurent Broudoux\"")); + assertTrue(jsonRepresentation.contains("\"laurent@microcks.io\"")); + assertTrue(jsonRepresentation.contains("41")); + + // Deserialize from binary encoding representation. + DatumReader datumReader = new GenericDatumReader(schema); + GenericRecord user = null; + Decoder decoder = DecoderFactory.get().binaryDecoder(avroBinary, null); + + try { + while (true) { + user = datumReader.read(user, decoder); + System.err.println("\nUser from binary representation: \n" + user.toString()); + } + } catch (EOFException eofException) { + // Nothing to do here, just exit the while loop. + } + assertEquals("Laurent Broudoux", user.get("name").toString()); + assertEquals("laurent@microcks.io", user.get("email").toString()); + assertEquals(Integer.valueOf(41), (Integer) user.get("age")); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testJsonToAvroUnionSchema() { + String jsonText = "{\"name\":\"Tigresse\"}"; + + Schema cat = SchemaBuilder.record("Cat").fields().requiredString("name").endRecord(); + Schema chat = SchemaBuilder.record("Chat").fields().requiredString("nom").endRecord(); + Schema union = SchemaBuilder.unionOf().type(cat).and().type(chat).endUnion(); + + try { + byte[] avroBinary = AvroUtil.jsonToAvro(jsonText, union); + System.err.println("binaryEncoding: \n" + new String(avroBinary, StandardCharsets.UTF_8)); + String jsonRepresentation = AvroUtil.avroToJson(avroBinary, union); + System.err.println("\njsonRepresentation: \n" + jsonRepresentation); + + assertTrue(jsonRepresentation.contains("\"Tigresse\"")); + + // Deserialize from binary encoding representation. + GenericRecord catRecord = AvroUtil.avroToAvroRecord(avroBinary, union); + System.err.println("\nCat from binary representation: \n" + catRecord.toString()); + + assertEquals("Tigresse", catRecord.get("name").toString()); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testJsonToAvroRecord() { + String jsonText = "{\"name\":\"Laurent Broudoux\", \"email\":\"laurent@microcks.io\", \"age\":42}"; + + try { + // Load schema from file. + Schema schema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/user-signedup-bad.avsc")); + + GenericRecord genericRecord = AvroUtil.jsonToAvroRecord(jsonText, schema); + assertEquals("Laurent Broudoux", genericRecord.get("name").toString()); + assertEquals("laurent@microcks.io", genericRecord.get("email").toString()); + assertEquals(Integer.valueOf(42), Integer.valueOf(genericRecord.get("age").toString())); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testAvroBinaryReadingFailure() { + String jsonText = "{\"name\":\"Laurent Broudoux\", \"email\":\"laurent@microcks.io\", \"age\":41}"; + + try { + // Load schema from file. + Schema writeSchema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/user-signedup-bad.avsc")); + + // Convert back and forth to and from JSON. + byte[] avroBinary = AvroUtil.jsonToAvro(jsonText, writeSchema); + System.err.println("binaryEncoding: \n" + new String(avroBinary, "UTF-8")); + + Schema readSchema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/user-signedup.avsc")); + String jsonRepresentation = AvroUtil.avroToJson(avroBinary, readSchema); + System.err.println("\njsonRepresentation: \n" + jsonRepresentation); + + GenericRecord genericRecord = AvroUtil.avroToAvroRecord(avroBinary, readSchema); + System.err.println(AvroUtil.validate(readSchema, genericRecord)); + + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testValidate() { + Schema v1Schema = SchemaBuilder.record("User").fields().requiredString("name").requiredInt("age").endRecord(); + Schema v2Schema = SchemaBuilder.record("User").fields().requiredString("fullName").requiredInt("age") + .optionalString("email").endRecord(); + + GenericRecord userv1 = new GenericData.Record(v1Schema); + userv1.put("name", "Laurent"); + userv1.put("age", 42); + + assertFalse(AvroUtil.validate(v2Schema, userv1)); + // The Avro validate method fails because it does not validate the field name + // just the position. This make it not usable in our context. + //assertFalse(GenericData.get().validate(v2Schema, userv1)); + + List errors = AvroUtil.getValidationErrors(v2Schema, userv1); + assertEquals(1, errors.size()); + assertEquals("Required field fullName cannot be found in record", errors.get(0)); + + GenericRecord userv2 = new GenericData.Record(v2Schema); + userv2.put("fullName", "Laurent Broudoux"); + userv2.put("age", 42); + + assertTrue(AvroUtil.validate(v2Schema, userv2)); + } + + @Test + void testAvroSchemaCompatibility() { + Schema v1Schema = SchemaBuilder.record("User").fields().requiredString("name").requiredInt("age").endRecord(); + Schema v2Schema = SchemaBuilder.record("User").fields().requiredString("fullName").requiredInt("age") + .optionalString("email").endRecord(); + + GenericRecord userv1 = new GenericData.Record(v1Schema); + userv1.put("name", "Laurent"); + userv1.put("age", 42); + + SchemaCompatibility.SchemaPairCompatibility compatibility = SchemaCompatibility + .checkReaderWriterCompatibility(userv1.getSchema(), v2Schema); + SchemaCompatibility.checkReaderWriterCompatibility(userv1.getSchema(), v2Schema).getResult() + .getIncompatibilities().stream() + .forEach(incompatibility -> System.err.println(incompatibility.getMessage())); + assertEquals(SchemaCompatibility.SchemaCompatibilityType.INCOMPATIBLE, compatibility.getType()); + } + + @Test + void testValidUnionSchema() { + Schema cat = SchemaBuilder.record("Cat").fields().requiredString("name").endRecord(); + Schema chat = SchemaBuilder.record("Chat").fields().requiredString("nom").endRecord(); + + Schema union = SchemaBuilder.unionOf().type(cat).and().type(chat).endUnion(); + + GenericRecord tigresse = new GenericData.Record(cat); + tigresse.put("name", "Tigresse"); + + // Assert that the record is valid against the union schema. + assertTrue(AvroUtil.validate(union, tigresse)); + List errors = AvroUtil.getValidationErrors(union, tigresse); + // There should still be one error regarding the Chat schema conformance. + assertEquals(1, errors.size()); + assertEquals("Required field nom cannot be found in record", errors.getFirst()); + } + + @Test + void testInvalidUnionSchema() { + Schema cat = SchemaBuilder.record("Cat").fields().requiredString("name").requiredInt("age").endRecord(); + Schema dog = SchemaBuilder.record("Dog").fields().requiredString("name").requiredInt("age") + .optionalString("fluff").endRecord(); + + Schema union = SchemaBuilder.unionOf().type(cat).and().type(dog).endUnion(); + + GenericRecord tigresse = new GenericData.Record(cat); + tigresse.put("name", "Tigresse"); + tigresse.put("age", "12"); + + // Assert that the record is not valid against the union schema. + assertFalse(AvroUtil.validate(union, tigresse)); + List errors = AvroUtil.getValidationErrors(union, tigresse); + assertEquals(2, errors.size()); + for (String error : errors) { + assertEquals("age is not an integer", error); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/JsonSchemaValidatorTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/JsonSchemaValidatorTest.java new file mode 100644 index 000000000..6597e67b6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/JsonSchemaValidatorTest.java @@ -0,0 +1,111 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for JsonSchemaValidation utility. + * @author laurent + */ +class JsonSchemaValidatorTest { + @Test + void testValidateJsonSuccess() { + boolean valid = false; + String schemaText = null; + String jsonText = "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}"; + + try { + // Load schema from file. + schemaText = FileUtils + .readFileToString(new File("target/test-classes/io/github/microcks/util/car-schema.json")); + // Validate Json according schema. + valid = JsonSchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is valid. + assertTrue(valid); + } + + @Test + void testValidateJsonFailure() { + boolean valid = true; + String schemaText = null; + String jsonText = "{\"id\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}"; + + try { + // Load schema from file. + schemaText = FileUtils + .readFileToString(new File("target/test-classes/io/github/microcks/util/car-schema.json")); + // Validate Json according schema. + valid = JsonSchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is not valid. + assertFalse(valid); + + // Now revalidate and check validation messages. + List errors = null; + try { + errors = JsonSchemaValidator.validateJson(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + assertEquals(1, errors.size()); + assertEquals("required property 'name' not found", errors.get(0)); + } + + @Test + void testValidateJsonUnknownNodeFailure() { + boolean valid = true; + String schemaText = null; + String jsonText = "{\"name\": \"307\", " + "\"model\": \"Peugeot 307\", \"year\": 2003, \"energy\": \"GO\"}"; + + try { + // Load schema from file. + schemaText = FileUtils + .readFileToString(new File("target/test-classes/io/github/microcks/util/car-schema-no-addon.json")); + // Validate Json according schema. + valid = JsonSchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is not valid. + assertFalse(valid); + + // Now revalidate and check validation messages. + List errors = null; + try { + errors = JsonSchemaValidator.validateJson(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + assertEquals(1, errors.size()); + assertEquals("property 'energy' is not defined in the schema and the schema does not allow additional properties", + errors.get(0)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/SchemaMapTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/SchemaMapTest.java new file mode 100644 index 000000000..73db5ae34 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/SchemaMapTest.java @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SchemaMapTest { + + @Test + void putSchemaEntry() { + SchemaMap schemaMap = new SchemaMap(); + schemaMap.putSchemaEntry("path1", "schema1"); + + assertTrue(schemaMap.hasSchemaEntry("path1")); + assertEquals("schema1", schemaMap.getSchemaEntry("path1")); + } + + @Test + void hasSchemaEntry() { + SchemaMap schemaMap = new SchemaMap(); + schemaMap.putSchemaEntry("path1", "schema1"); + + assertTrue(schemaMap.hasSchemaEntry("path1")); + assertFalse(schemaMap.hasSchemaEntry("path2")); + } + + @Test + void getSchemaEntry() { + SchemaMap schemaMap = new SchemaMap(); + schemaMap.putSchemaEntry("path1", "schema1"); + + assertEquals("schema1", schemaMap.getSchemaEntry("path1")); + assertNull(schemaMap.getSchemaEntry("path2")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlErrorHandlerTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlErrorHandlerTest.java new file mode 100644 index 000000000..f7d685a6e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlErrorHandlerTest.java @@ -0,0 +1,88 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xml.sax.SAXParseException; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class XmlErrorHandlerTest { + + private XmlErrorHandler errorHandler; + + @BeforeEach + public void setUp() { + errorHandler = new XmlErrorHandler(); + } + + @Test + public void testInitialExceptionsListIsEmpty() { + List exceptions = errorHandler.getExceptions(); + assertTrue(exceptions.isEmpty(), "Exceptions list should be empty initially"); + } + + @Test + public void testWarningAddsException() throws SAXParseException { + SAXParseException warningException = new SAXParseException("Warning message", null); + errorHandler.warning(warningException); + + List exceptions = errorHandler.getExceptions(); + assertEquals(1, exceptions.size(), "Exceptions list should contain one warning"); + assertEquals(warningException, exceptions.get(0), "Stored exception should match the warning exception"); + } + + @Test + public void testErrorAddsException() throws SAXParseException { + SAXParseException errorException = new SAXParseException("Error message", null); + errorHandler.error(errorException); + + List exceptions = errorHandler.getExceptions(); + assertEquals(1, exceptions.size(), "Exceptions list should contain one error"); + assertEquals(errorException, exceptions.get(0), "Stored exception should match the error exception"); + } + + @Test + public void testFatalErrorAddsException() throws SAXParseException { + SAXParseException fatalException = new SAXParseException("Fatal error message", null); + errorHandler.fatalError(fatalException); + + List exceptions = errorHandler.getExceptions(); + assertEquals(1, exceptions.size(), "Exceptions list should contain one fatal error"); + assertEquals(fatalException, exceptions.get(0), "Stored exception should match the fatal error exception"); + } + + @Test + public void testMultipleExceptionsAdded() throws SAXParseException { + SAXParseException warningException = new SAXParseException("Warning message", null); + SAXParseException errorException = new SAXParseException("Error message", null); + SAXParseException fatalException = new SAXParseException("Fatal error message", null); + + errorHandler.warning(warningException); + errorHandler.error(errorException); + errorHandler.fatalError(fatalException); + + List exceptions = errorHandler.getExceptions(); + assertEquals(3, exceptions.size(), "Exceptions list should contain three exceptions"); + assertEquals(warningException, exceptions.get(0), "First stored exception should match the warning exception"); + assertEquals(errorException, exceptions.get(1), "Second stored exception should match the error exception"); + assertEquals(fatalException, exceptions.get(2), "Third stored exception should match the fatal error exception"); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlSchemaURLResolverTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlSchemaURLResolverTest.java new file mode 100644 index 000000000..4e2b716dc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlSchemaURLResolverTest.java @@ -0,0 +1,83 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.ls.LSInput; + +import static org.junit.jupiter.api.Assertions.*; + +class XmlSchemaURLResolverTest { + + private static final String BASE_RESOURCE_URL = "http://example.com/schemas"; + private XmlSchemaURLResolver resolver; + + @BeforeEach + public void setUp() { + resolver = new XmlSchemaURLResolver(BASE_RESOURCE_URL); + } + + @Test + void testResolveResourceWithLocalResolution() { + String systemId = "http://www.w3.org/2001/xml.xsd"; + + LSInput lsInput = resolver.resolveResource(null, null, null, systemId, null); + + assertNotNull(lsInput); + assertEquals(systemId, lsInput.getSystemId()); + assertNotNull(lsInput.getCharacterStream()); + } + + // @Test + // void testResolveResourceWithBaseResourceURL() throws Exception { + // String systemId = "test.xsd"; + // String expectedContent = ""; + // + // URL mockURL = mock(URL.class); + // URLConnection mockConnection = mock(URLConnection.class); + // InputStream mockInputStream = new ByteArrayInputStream(expectedContent.getBytes()); + // + // when(mockURL.openStream()).thenReturn(mockInputStream); + // when(mockConnection.getInputStream()).thenReturn(mockInputStream); + // + // // Instead of mocking the URL constructor, we can mock URL.openStream directly. + // URL.setURLStreamHandlerFactory(protocol -> { + // return new java.net.URLStreamHandler() { + // @Override + // protected URLConnection openConnection(URL url) { + // return mockConnection; + // } + // }; + // }); + // + // LSInput lsInput = resolver.resolveResource(null, null, null, systemId, null); + // + // assertNotNull(lsInput); + // assertEquals(systemId, lsInput.getSystemId()); + // assertNotNull(lsInput.getCharacterStream()); + // } + + @Test + void testResolveResourceWithNonExistentResource() { + String systemId = "non-existent.xsd"; + + LSInput lsInput = resolver.resolveResource(null, null, null, systemId, null); + + assertNull(lsInput); + } +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlSchemaValidatorTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlSchemaValidatorTest.java new file mode 100644 index 000000000..c47b732b8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlSchemaValidatorTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.xml.sax.SAXParseException; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class XmlSchemaValidatorTest { + + @Test + public void testValidXmlAgainstValidSchema() throws Exception { + InputStream schemaStream = new FileInputStream("target/test-classes/io/github/microcks/util/valid-schema.xsd"); + String validXml = """ + + Tove + Jani + Reminder + Don't forget me this weekend! + + """; + + List errors = XmlSchemaValidator.validateXml(schemaStream, validXml); + assertTrue(errors.isEmpty(), "Expected no validation errors, but got: " + errors); + } + + @Test + public void testValidXmlAgainstInvalidSchema() throws Exception { + InputStream schemaStream = new FileInputStream("target/test-classes/io/github/microcks/util/invalid-schema.xsd"); + String validXml = """ + + Tove + Jani + Reminder + Don't forget me this weekend! + + """; + + Executable validationExecutable = () -> XmlSchemaValidator.validateXml(schemaStream, validXml); + assertThrows(SAXParseException.class, validationExecutable, "Expected SAXParseException due to schema mismatch."); + } +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlUtilTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlUtilTest.java new file mode 100644 index 000000000..91ee0f6e9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/XmlUtilTest.java @@ -0,0 +1,85 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class XmlUtilTest { + + private Document document; + private Element parent; + + @BeforeEach + void setUp() throws ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + document = builder.newDocument(); + + parent = document.createElementNS("http://example.com", "parent"); + document.appendChild(parent); + + Element child1 = document.createElementNS("http://example.com", "child"); + parent.appendChild(child1); + + Element child2 = document.createElementNS("http://example.com", "child"); + parent.appendChild(child2); + + Element child3 = document.createElementNS("http://example.com", "otherChild"); + parent.appendChild(child3); + } + + @Test + void getDirectChildren() { + List children = XmlUtil.getDirectChildren(parent, "http://example.com", "child"); + assertEquals(2, children.size()); + assertEquals("child", children.get(0).getLocalName()); + assertEquals("child", children.get(1).getLocalName()); + } + + @Test + void getUniqueDirectChild() { + assertThrows(MalformedXmlException.class, () -> { + XmlUtil.getUniqueDirectChild(parent, "http://example.com", "child1"); + }); + + Element uniqueChild = document.createElementNS("http://example.com", "uniqueChild"); + parent.appendChild(uniqueChild); + + try { + Element retrievedChild = XmlUtil.getUniqueDirectChild(parent, "http://example.com", "uniqueChild"); + assertEquals("uniqueChild", retrievedChild.getLocalName()); + } catch (MalformedXmlException e) { + fail("Exception should not have been thrown"); + } + } + + @Test + void hasDirectChild() { + assertTrue(XmlUtil.hasDirectChild(parent, "http://example.com", "child")); + assertFalse(XmlUtil.hasDirectChild(parent, "http://example.com", "nonExistentChild")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/asyncapi/AsyncAPISchemaValidatorTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/asyncapi/AsyncAPISchemaValidatorTest.java new file mode 100644 index 000000000..7cc21a807 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/asyncapi/AsyncAPISchemaValidatorTest.java @@ -0,0 +1,792 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.asyncapi; + +import com.fasterxml.jackson.databind.JsonNode; +import io.github.microcks.util.SchemaMap; +import org.apache.avro.Schema; +import org.apache.avro.SchemaBuilder; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericRecord; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for AsyncAPISchemaValidator utility. + * @author laurent + */ +class AsyncAPISchemaValidatorTest { + + @Test + void testValidateJsonSuccess() { + boolean valid = false; + String schemaText = null; + String jsonText = "{\"fullName\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-schema.json")); + // Validate Json according schema. + valid = AsyncAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is valid. + assertTrue(valid); + } + + @Test + void testValidateJsonSuccessFromYaml() { + boolean valid = false; + String schemaText = null; + String jsonText = "{\"fullName\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-schema.yaml")); + // Validate Json according schema. + valid = AsyncAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is valid. + assertTrue(valid); + } + + @Test + void testValidateJsonFailure() { + boolean valid = false; + String schemaText = null; + String jsonText = "{\"name\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-schema.json")); + // Validate Json according schema. + valid = AsyncAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is not valid. + assertFalse(valid); + } + + @Test + void testValidateJsonFailureFromYaml() { + boolean valid = false; + String schemaText = null; + String jsonText = "{\"name\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-schema.yaml")); + // Validate Json according schema. + valid = AsyncAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is not valid. + assertFalse(valid); + } + + @Test + void testFullProcedureFromAsyncAPIResource() { + String asyncAPIText = null; + String jsonText = "{\"fullName\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIResourceWithNumberFormats() { + String asyncAPIText = null; + String jsonText = "{\"displayName\": \"Laurent Broudoux\", \"age\": 43, \"size\": 1.8, \"exp\": 1234567891011, \"rewards\": 12345.67}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIResourceWithNumberFormatsWithRef() { + String asyncAPIText = null; + String jsonText = "{\"displayName\": \"Laurent Broudoux\", \"age\": 43, \"size\": 1.8, \"exp\": { \"level\": 1234567891011 }, \"rewards\": 12345.67}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/account-service-ref-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIResourceWithNumberFormatsWithRefRef() { + String asyncAPIText = null; + String jsonText = "{\"displayName\": \"Laurent Broudoux\", \"age\": 43, \"size\": 1.8, \"exp\": { \"level\": 1234567891011 }, \"rewards\": 12345.67}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/account-service-ref-ref-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIResourceNulls() { + String asyncAPIText = null; + String jsonText = "{\n" + " \"throwable\": null,\n" + " \"person\": {\n" + + " \"taille\": 110,\n" + " \"nom\": \"Bennour\",\n" + + " \"prenom\": \"Hassen\",\n" + + " \"dateNaissance\": \"2000-08-24T14:15:22Z\"\n" + " }\n" + " }"; + String errorJsonText = "{\n" + " \"throwable\": {\n" + + " \"detailMessage\": \"Exception message\",\n" + + " \"clazz\": \"org.acme.MyProducer\"\n" + " },\n" + + " \"person\": null" + " }"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + JsonNode errorContentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString(new File( + "target/test-classes/io/github/microcks/util/asyncapi/spring-cloud-stream-asyncapi-nulls.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + errorContentNode = AsyncAPISchemaValidator.getJsonNode(errorJsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of domaineA.service1.replier.v1.0.0 subscribe channel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/domaineA.service1.replier.v1.0.0/subscribe/message"); + assertTrue(errors.isEmpty()); + + errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, errorContentNode, + "/channels/domaineA.service1.replier.v1.0.0/subscribe/message"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIResourceFailure() { + String asyncAPIText = null; + String jsonText = "{\"id\": \"123456\", \"name\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/user~1signedup/subscribe/message"); + assertFalse(errors.isEmpty()); + + assertEquals(2, errors.size()); + // First error is because payload does not have any ref to components. + assertEquals("property 'id' is not defined in the schema and the schema does not allow additional properties", + errors.get(0)); + assertEquals("property 'name' is not defined in the schema and the schema does not allow additional properties", + errors.get(1)); + } + + @Test + void testFullProcedureFromAsyncAPIWithRefsResource() { + String asyncAPIText = null; + String jsonText = "{\"fullName\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-out-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + e.printStackTrace(); + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe channel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIWithDeepRefsResource() { + String asyncAPIText = null; + String jsonText = "{\"streetlightId\":\"dev0\", \"lumens\":1000, \"sentAt\":\"2020-11-20T21:46:38Z\"}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + e.printStackTrace(); + fail("Exception should not be thrown"); + } + + // Validate the content of smartylighting/streetlights/event/lighting/measured subscribe channel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/smartylighting~1streetlights~1event~1lighting~1measured/subscribe/message"); + + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIWithDeepRefsResourceFailure() { + String asyncAPIText = null; + String jsonText = "{\"streetlightId\":\"dev0\", \"location\":\"47.8509682604982, 0.11136576784773598\", \"sentAt\":\"2020-11-20T21:46:38Z\"}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + e.printStackTrace(); + fail("Exception should not be thrown"); + } + + // Validate the content of smartylighting/streetlights/event/lighting/measured subscribe channel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/smartylighting~1streetlights~1event~1lighting~1measured/subscribe/message"); + assertFalse(errors.isEmpty()); + assertEquals(2, errors.size()); + System.out.println(errors); + // First error is because payload does not have any ref to components. + assertEquals( + "property 'location' is not defined in the schema and the schema does not allow additional properties", + errors.get(0)); + assertEquals("required property 'lumens' not found", errors.get(1)); + } + + @Test + void testFullProcedureFromAsyncAPIWithOneOf21() { + String asyncAPIText = null; + String jsonTextAlt1 = "{\"displayName\":\"Alice\"}"; + String jsonTextAlt2 = "{\"email\":\"bob@example.com\"}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode1 = null; + JsonNode contentNode2 = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/account-service-asyncapi-oneof-2.1.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode1 = AsyncAPISchemaValidator.getJsonNode(jsonTextAlt1); + contentNode2 = AsyncAPISchemaValidator.getJsonNode(jsonTextAlt2); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe channel with 1st content. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode1, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + + // Validate the content of user/signedup subscribe channel with 2nd content. + errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode2, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIWithOneOf23() { + String asyncAPIText = null; + String jsonTextAlt1 = "{\"displayName\":\"Alice\"}"; + String jsonTextAlt2 = "{\"email\":\"bob@example.com\"}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode1 = null; + JsonNode contentNode2 = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/account-service-asyncapi-oneof-2.3.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode1 = AsyncAPISchemaValidator.getJsonNode(jsonTextAlt1); + contentNode2 = AsyncAPISchemaValidator.getJsonNode(jsonTextAlt2); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe channel with 1st content. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode1, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + + // Validate the content of user/signedup subscribe channel with 2nd content. + errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode2, + "/channels/user~1signedup/subscribe/message"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPI3() { + String asyncAPIText = null; + String jsonText = """ + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 45} + """; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of publishUserSignedUps operation with content. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/operations/publishUserSignedUps/messages"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPI3WithOneOf() { + String asyncAPIText = null; + String jsonTextAlt1 = """ + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 45} + """; + String jsonTextAlt2 = """ + {"id": "706a1af6-6a65-4b2a-b350-ece4ea4f7929"} + """; + JsonNode asyncAPISpec = null; + JsonNode contentNode1 = null; + JsonNode contentNode2 = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-oneof-3.0.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode1 = AsyncAPISchemaValidator.getJsonNode(jsonTextAlt1); + contentNode2 = AsyncAPISchemaValidator.getJsonNode(jsonTextAlt2); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of publishUserSignedUpOut operation with alternative contents. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode1, + "/operations/publishUserSignedUpOut/messages"); + assertTrue(errors.isEmpty()); + + errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode2, + "/operations/publishUserSignedUpOut/messages"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromAsyncAPIWithExternalRelativeReference() { + String asyncAPIText = null; + String jsonText = "{\"fullName\":\"Laurent Broudoux\", \"email\":\"laurent@acme.com\", \"age\": 44}"; + JsonNode asyncAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml")); + // Extract JSON nodes using AsyncAPISchemaValidator methods. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + contentNode = AsyncAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateJsonMessage(asyncAPISpec, contentNode, + "/channels/user~1signedup/subscribe/message", + "https://raw.githubusercontent.com/microcks/microcks/1.7.x/commons/util/src/test/resources/io/github/microcks/util/asyncapi/"); + for (String error : errors) { + System.out.println("Validation error: " + error); + } + assertTrue(errors.isEmpty()); + } + + @Test + void testValidateAvroSuccessFromAsyncAPIResource() { + String asyncAPIText = null; + JsonNode asyncAPISpec = null; + Schema avroSchema = null; + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml")); + // Extract JSON node using AsyncAPISchemaValidator method. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + + // Load schema from file. + avroSchema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup.avsc")); + + GenericRecord record = new GenericData.Record(avroSchema); + record.put("fullName", "Laurent Broudoux"); + record.put("email", "laurent@microcks.io"); + record.put("age", 42); + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, record, + "/channels/user~1signedup/subscribe/message", null); + assertTrue(errors.isEmpty()); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testValidateAvroFailureFromAsyncAPIResource() { + String asyncAPIText = null; + JsonNode asyncAPISpec = null; + Schema avroSchema = null; + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml")); + // Extract JSON node using AsyncAPISchemaValidator method. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + + // Load schema from file. + avroSchema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-bad.avsc")); + + GenericRecord record = new GenericData.Record(avroSchema); + record.put("name", "Laurent"); + record.put("email", "laurent@microcks.io"); + record.put("age", 42); + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, record, + "/channels/user~1signedup/subscribe/message", null); + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertEquals("Required field fullName cannot be found in record", errors.get(0)); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testValidateAvroSuccessFromAsyncAPIWithRefsResource() { + String asyncAPIText = null; + JsonNode asyncAPISpec = null; + Schema avroSchema = null; + + SchemaMap schemaMap = new SchemaMap(); + schemaMap.putSchemaEntry("./user-signedup.avsc", + "{\"namespace\": \"microcks.avro\",\n" + " \"type\": \"record\",\n" + " \"name\": \"User\",\n" + + " \"fields\": [\n" + " {\"name\": \"fullName\", \"type\": \"string\"},\n" + + " {\"name\": \"email\", \"type\": \"string\"},\n" + + " {\"name\": \"age\", \"type\": \"int\"}\n" + " ]\n" + "}"); + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml")); + // Extract JSON node using AsyncAPISchemaValidator method. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + + // Load schema from file. + avroSchema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup.avsc")); + + GenericRecord record = new GenericData.Record(avroSchema); + record.put("fullName", "Laurent Broudoux"); + record.put("email", "laurent@microcks.io"); + record.put("age", 42); + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, record, + "/channels/user~1signedup/subscribe/message", schemaMap); + assertTrue(errors.isEmpty()); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testValidateAvroFailureFromAsyncAPIWithRefsResource() { + String asyncAPIText = null; + JsonNode asyncAPISpec = null; + Schema avroSchema = null; + + SchemaMap schemaMap = new SchemaMap(); + schemaMap.putSchemaEntry("./user-signedup.avsc", + "{\"namespace\": \"microcks.avro\",\n" + " \"type\": \"record\",\n" + " \"name\": \"User\",\n" + + " \"fields\": [\n" + " {\"name\": \"fullName\", \"type\": \"string\"},\n" + + " {\"name\": \"email\", \"type\": \"string\"},\n" + + " {\"name\": \"age\", \"type\": \"int\"}\n" + " ]\n" + "}"); + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml")); + // Extract JSON node using AsyncAPISchemaValidator method. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + + // Load schema from file. + avroSchema = new Schema.Parser() + .parse(new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-bad.avsc")); + + GenericRecord record = new GenericData.Record(avroSchema); + record.put("name", "Laurent"); + record.put("email", "laurent@microcks.io"); + record.put("age", 42); + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, record, + "/channels/user~1signedup/subscribe/message", schemaMap); + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertEquals("Required field fullName cannot be found in record", errors.get(0)); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testValidateAvroSuccessFromAsyncAPIWithOneOf23() { + String asyncAPIText = null; + JsonNode asyncAPISpec = null; + + Schema signedupSchema = SchemaBuilder.record("SignupUser").fields().requiredString("displayName").endRecord(); + Schema loginSchema = SchemaBuilder.record("LoginUser").fields().requiredString("email").endRecord(); + + SchemaMap schemaMap = new SchemaMap(); + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi-oneof-2.3.yaml"), + StandardCharsets.UTF_8); + // Extract JSON node using AsyncAPISchemaValidator method. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + + // Check with first alternative among oneOfs. + GenericRecord signedupRecord = new GenericData.Record(signedupSchema); + signedupRecord.put("displayName", "Laurent Broudoux"); + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, signedupRecord, + "/channels/user~1signedup/subscribe/message", schemaMap); + assertTrue(errors.isEmpty()); + + // Check with second alternative among oneOfs. + GenericRecord loginRecord = new GenericData.Record(loginSchema); + loginRecord.put("email", "laurent@microcks.io"); + + // Validate the content of user/signedup subscribe chanel. + errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, loginRecord, + "/channels/user~1signedup/subscribe/message", schemaMap); + assertTrue(errors.isEmpty()); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testValidateAvroSuccessFromAsyncAPIWithOneOf23AndRefsResources() { + String asyncAPIText = null; + JsonNode asyncAPISpec = null; + + Schema signedupSchema = SchemaBuilder.record("SignupUser").namespace("microcks.avro").fields() + .requiredString("displayName").endRecord(); + Schema loginSchema = SchemaBuilder.record("LoginUser").namespace("microcks.avro").fields().requiredString("email") + .endRecord(); + + SchemaMap schemaMap = new SchemaMap(); + schemaMap.putSchemaEntry("./user-signedup-signup.avsc", + "{\"namespace\": \"microcks.avro\",\n" + " \"type\": \"record\",\n" + " \"name\": \"SignupUser\",\n" + + " \"fields\": [\n" + " {\"name\": \"displayName\", \"type\": \"string\"}\n" + " ]\n" + "}"); + schemaMap.putSchemaEntry("./user-signedup-login.avsc", + "{\"namespace\": \"microcks.avro\",\n" + " \"type\": \"record\",\n" + " \"name\": \"LoginUser\",\n" + + " \"fields\": [\n" + " {\"name\": \"email\", \"type\": \"string\"}\n" + " ]\n" + "}"); + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString(new File( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi-oneof-2.3.yaml"), + StandardCharsets.UTF_8); + // Extract JSON node using AsyncAPISchemaValidator method. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + + // Check with first alternative among oneOfs. + GenericRecord signedupRecord = new GenericData.Record(signedupSchema); + signedupRecord.put("displayName", "Laurent Broudoux"); + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, signedupRecord, + "/channels/user~1signedup/subscribe/message", schemaMap); + assertTrue(errors.isEmpty()); + + // Check with second alternative among oneOfs. + GenericRecord loginRecord = new GenericData.Record(loginSchema); + loginRecord.put("email", "laurent@microcks.io"); + + // Validate the content of user/signedup subscribe chanel. + errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, loginRecord, + "/channels/user~1signedup/subscribe/message", schemaMap); + assertTrue(errors.isEmpty()); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testValidateAvroSuccessFromAsyncAPI3WithOneOf() { + String asyncAPIText = null; + JsonNode asyncAPISpec = null; + + Schema signedupSchema = SchemaBuilder.record("SignupUser").fields().requiredString("displayName").endRecord(); + Schema loginSchema = SchemaBuilder.record("LoginUser").fields().requiredString("email").endRecord(); + + SchemaMap schemaMap = new SchemaMap(); + + try { + // Load full specification from file. + asyncAPIText = FileUtils.readFileToString( + new File( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi-oneof-3.0.yaml"), + StandardCharsets.UTF_8); + // Extract JSON node using AsyncAPISchemaValidator method. + asyncAPISpec = AsyncAPISchemaValidator.getJsonNodeForSchema(asyncAPIText); + + // Check with first alternative among oneOfs. + GenericRecord signedupRecord = new GenericData.Record(signedupSchema); + signedupRecord.put("displayName", "Laurent Broudoux"); + + // Validate the content of user/signedup subscribe chanel. + List errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, signedupRecord, + "/operations/publishUserSignUpLogin/messages", schemaMap); + assertTrue(errors.isEmpty()); + + // Check with second alternative among oneOfs. + GenericRecord loginRecord = new GenericData.Record(loginSchema); + loginRecord.put("email", "laurent@microcks.io"); + + // Validate the content of user/signedup subscribe chanel. + errors = AsyncAPISchemaValidator.validateAvroMessage(asyncAPISpec, loginRecord, + "/operations/publishUserSignUpLogin/messages", schemaMap); + assertTrue(errors.isEmpty()); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/graphql/GraphQLSchemaValidatorTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/graphql/GraphQLSchemaValidatorTest.java new file mode 100644 index 000000000..23362a6cf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/graphql/GraphQLSchemaValidatorTest.java @@ -0,0 +1,168 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for GraphQLSchemaValidator utility. + * @author laurent + */ +class GraphQLSchemaValidatorTest { + + @Test + void testBuildResponseJsonSchema() { + String schemaText; + String queryText = "{\n" + " hero {\n" + " name\n" + " email\n" + " family\n" + " affiliate\n" + + " movies {\n" + " title\n" + " }\n" + " }\n" + "}"; + + JsonNode responseSchema = null; + try { + // Load schema from file. + schemaText = FileUtils + .readFileToString(new File("target/test-classes/io/github/microcks/util/graphql/basic-heroes.graphql")); + // Build JsonSchema for response. + responseSchema = GraphQLSchemaValidator.buildResponseJsonSchema(schemaText, queryText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + assertFalse( + responseSchema.path("properties").path("data").path("properties").path("hero") instanceof MissingNode); + + ObjectNode heroNode = (ObjectNode) responseSchema.path("properties").path("data").path("properties").path("hero"); + assertEquals("object", heroNode.get("type").asText()); + assertEquals(JsonNodeType.OBJECT, heroNode.get("properties").getNodeType()); + assertEquals(JsonNodeType.ARRAY, heroNode.get("required").getNodeType()); + assertEquals(JsonNodeType.BOOLEAN, heroNode.get("additionalProperties").getNodeType()); + + ArrayNode requiredHero = (ArrayNode) heroNode.get("required"); + assertEquals(5, requiredHero.size()); + Iterator requiredHeroElements = requiredHero.elements(); + while (requiredHeroElements.hasNext()) { + String requiredHeroField = requiredHeroElements.next().asText(); + assertTrue("name".equals(requiredHeroField) || "email".equals(requiredHeroField) + || "family".equals(requiredHeroField) || "affiliate".equals(requiredHeroField) + || "movies".equals(requiredHeroField)); + } + + ObjectNode moviesNode = (ObjectNode) heroNode.path("properties").path("movies"); + assertEquals("array", moviesNode.get("type").asText()); + assertEquals(JsonNodeType.OBJECT, moviesNode.get("items").getNodeType()); + + ObjectNode movieItemsNode = (ObjectNode) moviesNode.get("items"); + assertEquals("object", movieItemsNode.get("type").asText()); + assertEquals(JsonNodeType.OBJECT, movieItemsNode.get("properties").getNodeType()); + assertEquals(JsonNodeType.ARRAY, movieItemsNode.get("required").getNodeType()); + assertEquals(JsonNodeType.BOOLEAN, movieItemsNode.get("additionalProperties").getNodeType()); + + ArrayNode requiredMovie = (ArrayNode) movieItemsNode.get("required"); + assertEquals(1, requiredMovie.size()); + } + + @Test + void testValidateJson() { + String schemaText; + String queryText = "{\n" + " hero {\n" + " name\n" + " email\n" + " family\n" + " affiliate\n" + + " movies {\n" + " title\n" + " }\n" + " }\n" + "}"; + String responseText = "{\n" + " \"data\": {\n" + " \"hero\": {\n" + " \"name\": \"Iron Man\",\n" + + " \"email\": \"tony@stark.inc\",\n" + " \"family\": \"MARVEL\",\n" + + " \"affiliate\": \"DC\",\n" + " \"movies\": [\n" + " {\"title\": \"Iron Man 1\"},\n" + + " {\"title\": \"Iron Man 2\"},\n" + " {\"title\": \"Iron Man 3\"}\n" + " ]\n" + + " }\n" + " }\n" + "}"; + String badResponseText = "{\n" + " \"data\": {\n" + " \"hero\": {\n" + " \"name\": \"Iron Man\",\n" + + " \"family\": \"MARVEL\",\n" + " \"affiliate\": \"DC\",\n" + " \"movies\": [\n" + + " {\"title\": \"Iron Man 1\"},\n" + " {\"title\": \"Iron Man 2\"},\n" + + " {\"title\": \"Iron Man 3\"}\n" + " ]\n" + " }\n" + " }\n" + "}"; + + ObjectMapper mapper = new ObjectMapper(); + + JsonNode responseSchema = null; + List validationErrors = null; + try { + // Load schema from file. + schemaText = FileUtils + .readFileToString(new File("target/test-classes/io/github/microcks/util/graphql/basic-heroes.graphql")); + // Build JsonSchema for response. + responseSchema = GraphQLSchemaValidator.buildResponseJsonSchema(schemaText, queryText); + // Validate a correct response. + validationErrors = GraphQLSchemaValidator.validateJson(responseSchema, mapper.readTree(responseText)); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + assertTrue(validationErrors.isEmpty()); + + try { + // Validate a bad response with missing email. + validationErrors = GraphQLSchemaValidator.validateJson(responseSchema, mapper.readTree(badResponseText)); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + assertEquals(1, validationErrors.size()); + assertEquals("required property 'email' not found", validationErrors.get(0)); + } + + @Test + void testValidateJsonAdvanced() { + String schemaText; + String queryText = "query allFilms {\n" + " allFilms {\n" + " films {\n" + " id\n" + + " title\n" + " episodeID\n" + " director\n" + " starCount\n" + + " rating\n" + " }\n" + " }\n" + "}"; + String responseText = "{\n" + " \"data\": {\n" + " \"allFilms\": {\n" + " \"films\": [\n" + " {\n" + + " \"id\": \"ZmlsbXM6MQ==\",\n" + " \"title\": \"A New Hope\",\n" + + " \"episodeID\": 4,\n" + " \"director\": \"George Lucas\",\n" + + " \"starCount\": 432,\n" + " \"rating\": 4.3\n" + " },\n" + " {\n" + + " \"id\": \"ZmlsbXM6Mg==\",\n" + " \"title\": \"The Empire Strikes Back\",\n" + + " \"episodeID\": 5,\n" + " \"director\": \"Irvin Kershner\",\n" + + " \"starCount\": 433,\n" + " \"rating\": 4.3\n" + " }\n" + " ]\n" + + " }\n" + " }\n" + "}"; + + ObjectMapper mapper = new ObjectMapper(); + + JsonNode responseSchema = null; + List validationErrors = null; + try { + // Load schema from file. + schemaText = FileUtils + .readFileToString(new File("target/test-classes/io/github/microcks/util/graphql/films.graphql")); + // Build JsonSchema for response. + responseSchema = GraphQLSchemaValidator.buildResponseJsonSchema(schemaText, queryText); + + mapper.enable(SerializationFeature.INDENT_OUTPUT); + System.err.println(mapper.writeValueAsString(responseSchema)); + + // Validate a correct response. + validationErrors = GraphQLSchemaValidator.validateJson(responseSchema, mapper.readTree(responseText)); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + assertTrue(validationErrors.isEmpty()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/graphql/JsonSchemaBuilderQueryVisitorTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/graphql/JsonSchemaBuilderQueryVisitorTest.java new file mode 100644 index 000000000..18bcb17f5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/graphql/JsonSchemaBuilderQueryVisitorTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import graphql.analysis.QueryVisitorFieldEnvironment; +import graphql.language.EnumValueDefinition; +import graphql.language.FieldDefinition; +import graphql.language.TypeName; +import graphql.schema.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class JsonSchemaBuilderQueryVisitorTest { + + private ObjectNode jsonSchemaData; + private JsonSchemaBuilderQueryVisitor visitor; + + @BeforeEach + public void setUp() { + ObjectMapper mapper = new ObjectMapper(); + jsonSchemaData = mapper.createObjectNode(); + jsonSchemaData.putObject(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_PROPERTIES); + visitor = new JsonSchemaBuilderQueryVisitor(jsonSchemaData); + } + + @Test + public void testVisitFieldWithScalarType() { + QueryVisitorFieldEnvironment environment = mock(QueryVisitorFieldEnvironment.class); + GraphQLOutputType outputType = mock(GraphQLOutputType.class); + TypeName definitionType = TypeName.newTypeName().name("String").build(); + + when(environment.getFieldDefinition()).thenReturn(mock(GraphQLFieldDefinition.class)); + when(environment.getFieldDefinition().getDefinition()).thenReturn(mock(FieldDefinition.class)); + when(environment.getFieldDefinition().getType()).thenReturn(outputType); + when(environment.getFieldDefinition().getDefinition().getType()).thenReturn(definitionType); + when(environment.getFieldDefinition().getName()).thenReturn("scalarField"); + + visitor.visitField(environment); + + JsonNode fieldNode = jsonSchemaData.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_PROPERTIES).get("scalarField"); + assertEquals("string", fieldNode.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_TYPE).asText()); + } + + @Test + public void testVisitFieldWithObjectType() { + QueryVisitorFieldEnvironment environment = mock(QueryVisitorFieldEnvironment.class); + GraphQLOutputType outputType = mock(GraphQLObjectType.class); + TypeName definitionType = TypeName.newTypeName().name("Object").build(); + + when(environment.getFieldDefinition()).thenReturn(mock(GraphQLFieldDefinition.class)); + when(environment.getFieldDefinition().getDefinition()).thenReturn(mock(FieldDefinition.class)); + when(environment.getFieldDefinition().getType()).thenReturn(outputType); + when(environment.getFieldDefinition().getDefinition().getType()).thenReturn(definitionType); + when(environment.getFieldDefinition().getName()).thenReturn("objectField"); + + visitor.visitField(environment); + + JsonNode fieldNode = jsonSchemaData.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_PROPERTIES).get("objectField"); + assertEquals("object", fieldNode.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_TYPE).asText()); + assertFalse(fieldNode.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_ADDITIONAL_PROPERTIES).asBoolean()); + } + + @Test + public void testVisitFieldWithEnumType() { + QueryVisitorFieldEnvironment environment = mock(QueryVisitorFieldEnvironment.class); + GraphQLEnumType enumType = mock(GraphQLEnumType.class); + TypeName definitionType = TypeName.newTypeName().name("EnumType").build(); + + when(environment.getFieldDefinition()).thenReturn(mock(GraphQLFieldDefinition.class)); + when(environment.getFieldDefinition().getDefinition()).thenReturn(mock(FieldDefinition.class)); + when(environment.getFieldDefinition().getType()).thenReturn(enumType); + when(environment.getFieldDefinition().getDefinition().getType()).thenReturn(definitionType); + when(environment.getFieldDefinition().getName()).thenReturn("enumField"); + + EnumValueDefinition valueDef1 = EnumValueDefinition.newEnumValueDefinition().name("VALUE1").build(); + EnumValueDefinition valueDef2 = EnumValueDefinition.newEnumValueDefinition().name("VALUE2").build(); + + GraphQLEnumValueDefinition enumValueDef1 = new GraphQLEnumValueDefinition.Builder().name("VALUE1") + .value("Description").build(); + GraphQLEnumValueDefinition enumValueDef2 = new GraphQLEnumValueDefinition.Builder().name("VALUE2") + .value("Description").build(); + + when(enumType.getValues()).thenReturn(List.of(enumValueDef1, enumValueDef2)); + + visitor.visitField(environment); + + JsonNode fieldNode = jsonSchemaData.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_PROPERTIES).get("enumField"); + assertEquals("string", fieldNode.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_TYPE).asText()); + assertEquals(2, fieldNode.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_ENUM).size()); + assertEquals("VALUE1", fieldNode.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_ENUM).get(0).asText()); + assertEquals("VALUE2", fieldNode.get(JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_ENUM).get(1).asText()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/OpenAPISchemaBuilderTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/OpenAPISchemaBuilderTest.java new file mode 100644 index 000000000..cfbd47a2d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/OpenAPISchemaBuilderTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a test case for OpenAPISchemaBuilder class. + * @author laurent + */ +class OpenAPISchemaBuilderTest { + + @Test + void testBuildTypeSchemaFromJson() { + JsonNode jsonNode = null; + ObjectMapper mapper = new ObjectMapper(); + + String jsonText = "{\"foo\": \"bar\", \"flag\": true, " + "\"list\": [1, 2], " + "\"obj\": {\"fizz\": \"bar\"}, " + + "\"objList\": [{\"number\": 1}, {\"number\": 2}]}"; + + String expectedSchema = "{\"type\":\"object\",\"properties\":{\"foo\":{\"type\":\"string\"},\"flag\":{\"type\":\"boolean\"},\"list\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}},\"obj\":{\"type\":\"object\",\"properties\":{\"fizz\":{\"type\":\"string\"}}},\"objList\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"number\":{\"type\":\"number\"}}}}}}"; + try { + jsonNode = mapper.readTree(jsonText); + } catch (Exception e) { + fail("Exception should not occur"); + } + + JsonNode schemaNode = OpenAPISchemaBuilder.buildTypeSchemaFromJson(jsonNode); + try { + String actual = mapper.writeValueAsString(schemaNode); + assertEquals(expectedSchema, actual); + } catch (JsonProcessingException e) { + fail("No exception should be thrown"); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/OpenAPISchemaValidatorTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/OpenAPISchemaValidatorTest.java new file mode 100644 index 000000000..1bfa0c9da --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/OpenAPISchemaValidatorTest.java @@ -0,0 +1,662 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for OpenAPISchemaValidator utility. + * @author laurent + */ +class OpenAPISchemaValidatorTest { + + @Test + void testValidateJsonSuccess() { + boolean valid = false; + String schemaText = null; + String jsonText = "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/car-schema.json"), StandardCharsets.UTF_8); + // Validate Json according schema. + valid = OpenAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is valid. + assertTrue(valid); + } + + @Test + void testValidateJsonSuccessWithNull() { + boolean valid = false; + String schemaText = null; + String jsonText = "{\"name\": \"307\", \"model\": null, \"year\": 2003}"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/car-schema.json"), StandardCharsets.UTF_8); + // Validate Json according schema. + valid = OpenAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is valid. + assertTrue(valid); + } + + @Test + void testValidateJsonFailure() { + boolean valid = true; + String schemaText = null; + String jsonText = "{\"id\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/car-schema.json"), StandardCharsets.UTF_8); + // Validate Json according schema. + valid = OpenAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is not valid. + assertFalse(valid); + + // Now revalidate and check validation messages. + List errors = null; + try { + errors = OpenAPISchemaValidator.validateJson(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + assertEquals(1, errors.size()); + assertEquals("required property 'name' not found", errors.get(0)); + } + + @Test + void testValidateJsonUnknownNodeFailure() { + boolean valid = true; + String schemaText = null; + String jsonText = "{\"name\": \"307\", " + "\"model\": \"Peugeot 307\", \"year\": 2003, \"energy\": \"GO\"}"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/car-schema-no-addon.json"), + StandardCharsets.UTF_8); + // Validate Json according schema. + valid = OpenAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is not valid. + assertFalse(valid); + + // Now revalidate and check validation messages. + List errors = null; + try { + errors = OpenAPISchemaValidator.validateJson(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + assertEquals(1, errors.size()); + assertEquals("property 'energy' is not defined in the schema and the schema does not allow additional properties", + errors.get(0)); + } + + @Test + void testValidateJsonSchemaWithReferenceSuccess() { + boolean valid = true; + String schemaText = null; + String jsonText = "[{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}," + + "{\"name\": \"jean-pierre\", \"model\": \"Peugeot Traveler\", \"year\": 2017}]"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/cars-schema.json"), + StandardCharsets.UTF_8); + // Validate Json according schema. + valid = OpenAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + e.printStackTrace(); + fail("Exception should not be thrown"); + } + + // Assert Json object is valid. + assertTrue(valid); + } + + @Test + void testValidateJsonWithExternalReferenceSuccess() { + boolean valid = false; + String schemaText = null; + String jsonText = "[{\"region\": \"north\", \"weather\": \"snowy\", \"temp\": -1.5, \"visibility\": 25}, " + + "{\"region\": \"west\", \"weather\": \"rainy\", \"temp\": 12.2, \"visibility\": 300}]"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/weather-forecasts.json"), + StandardCharsets.UTF_8); + // Validate Json according schema. + valid = OpenAPISchemaValidator.isJsonValid(schemaText, jsonText, + "https://raw.githubusercontent.com/microcks/microcks/1.6.x/commons/util/src/test/resources/io/github/microcks/util/openapi/"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Assert Json object is valid. + assertTrue(valid); + } + + @Test + void testValidateJsonSchemaWithReferenceFailure() { + boolean valid = true; + String schemaText = null; + String jsonText = "[{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}," + + "{\"name\": \"jean-pierre\", \"model\": \"Peugeot Traveler\", \"year\": \"2017\"}]"; + + try { + // Load schema from file. + schemaText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/cars-schema.json"), + StandardCharsets.UTF_8); + // Validate Json according schema. + valid = OpenAPISchemaValidator.isJsonValid(schemaText, jsonText); + } catch (Exception e) { + e.printStackTrace(); + fail("Exception should not be thrown"); + } + + // Assert Json object is not valid. + assertFalse(valid); + + List errors = null; + try { + errors = OpenAPISchemaValidator.validateJson(schemaText, jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Don't know why but when a failure occurs, validator also complains + // about components reference not being found. + assertEquals(1, errors.size()); + assertEquals("string found, integer expected", errors.get(0)); + } + + @Test + void testFullProcedureFromOpenAPIResource() { + String openAPIText = null; + String jsonText = """ + [ + { "resourceId": "396be545-e2d4-4497-a5b5-700e89ab99c0" }, + { "resourceId": "f377afb3-5c62-40cc-8f07-1f4749a780eb" } + ] + """; + JsonNode openAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/response-refs-openapi.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1accounts/get/responses/200", "application/json"); + assertTrue(errors.isEmpty()); + + // Now try with another message. + jsonText = "{ \"account\": {\"resourceId\": \"396be545-e2d4-4497-a5b5-700e89ab99c0\" } }"; + + try { + // Extract JSON nodes using OpenAPISchemaValidator methods. + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts/{accountId} response message. + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1accounts~1{accountId}/get/responses/200", "application/json"); + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromOpenAPIResourceWithLooseCharsetContentType() { + String openAPIText = null; + String jsonText = """ + [ + { "resourceId": "396be545-e2d4-4497-a5b5-700e89ab99c0" }, + { "resourceId": "f377afb3-5c62-40cc-8f07-1f4749a780eb" } + ] + """; + JsonNode openAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/response-refs-openapi.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1accounts/get/responses/200", "application/json;charset=UTF-8"); + assertTrue(errors.isEmpty()); + + // Now try with another message. + jsonText = "{ \"account\": {\"resourceId\": \"396be545-e2d4-4497-a5b5-700e89ab99c0\" } }"; + + try { + // Extract JSON nodes using OpenAPISchemaValidator methods. + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts/{accountId} response message. + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1accounts~1{accountId}/get/responses/200", "application/hal+json; charset=utf-8"); + assertTrue(errors.isEmpty()); + + // Now try with failing message. + jsonText = "{ \"account\": {\"resourceIdentifier\": \"396be545-e2d4-4497-a5b5-700e89ab99c0\" } }"; + + try { + // Extract JSON nodes using OpenAPISchemaValidator methods. + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts/{accountId} response message. + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1accounts~1{accountId}/get/responses/200", "application/hal+json; charset=UTF-8"); + assertFalse(errors.isEmpty()); + assertEquals("required property 'resourceId' not found", errors.get(1)); + + // Validate again the content for Get /accounts/{accountId} response message with no charset. + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1accounts~1{accountId}/get/responses/200", "application/hal+json; charset=UTF-8"); + assertFalse(errors.isEmpty()); + assertEquals("required property 'resourceId' not found", errors.get(1)); + } + + @Test + void testFullProcedureFromOpenAPIResourceWithRef() { + String openAPIText = null; + String jsonText = """ + { + "region": "north", + "temp": -1.5, + "weather": "snowy", + "visibility": 25 + } + """; + JsonNode openAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-local-ref.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /forecast/{region} response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1forecast~1{region}/get/responses/200", "application/json"); + assertTrue(errors.isEmpty()); + + // Now try with an external absolute ref. + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File( + "target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /forecast/{region} response message. + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1forecast~1{region}/get/responses/200", "application/json"); + for (String error : errors) { + System.out.println("Validation error: " + error); + } + assertTrue(errors.isEmpty()); + + // Now try with an external relative ref. + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File( + "target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /forecast/{region} response message but without specifying a namespace. + // This should fail as type cannot be resolved. + try { + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1forecast~1{region}/get/responses/200", "application/json"); + } catch (Exception e) { + assertInstanceOf(IllegalArgumentException.class, e); + } + + // Validate the content for Get /forecast/{region} response message. Now specifying a namespace. + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1forecast~1{region}/get/responses/200", "application/json", + "https://raw.githubusercontent.com/microcks/microcks/1.5.x/commons/util/src/test/resources/io/github/microcks/util/openapi/"); + + assertTrue(errors.isEmpty()); + + + // Now test with a $ref at the items level of an array. + // We cannot use relative notation here (eg. ./schema.json) but should use direct notation like schema.json + // Check the com.github.fge.jsonschema.core.keyword.syntax.checkers.helpers.URISyntaxChercker class. + jsonText = "[{\"region\": \"north\", \"weather\": \"snowy\", \"temp\": -1.5, \"visibility\": 25}, " + + "{\"region\": \"west\", \"weather\": \"rainy\", \"temp\": 12.2, \"visibility\": 300}]"; + + // Now try with an external relative ref. + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File( + "target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /forecast response message but without specifying a namespace. + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1forecast/get/responses/200", "application/json", + "https://raw.githubusercontent.com/microcks/microcks/1.5.x/commons/util/src/test/resources/io/github/microcks/util/openapi/"); + + assertTrue(errors.isEmpty()); + } + + @Test + void testFullProcedureFromOpenAPIResourceFailure() { + String openAPIText = null; + String jsonText = """ + [ + { "resource": "396be545-e2d4-4497-a5b5-700e89ab99c0", "id": "01" }, + { "resource": "f377afb3-5c62-40cc-8f07-1f4749a780eb", "id": "01" } + ] + """; + JsonNode openAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/response-refs-openapi.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1accounts/get/responses/200", "application/json"); + assertFalse(errors.isEmpty()); + assertEquals(6, errors.size()); + + // Now try with another message. + jsonText = "{ \"account\": {\"resource\": \"396be545-e2d4-4497-a5b5-700e89ab99c0\" } }"; + + try { + // Extract JSON nodes using OpenAPISchemaValidator methods. + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts/{accountId} response message. + errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1accounts~1{accountId}/get/responses/200", "application/json"); + assertFalse(errors.isEmpty()); + } + + @Test + void testFullProcedureFromOpenAPIResourceWithStructures() { + String openAPIText = null; + String jsonText = """ + { + "id": "396be545-e2d4-4497-a5b5-700e89ab99c0", + "realm_id": "f377afb3-5c62-40cc-8f07-1f4749a780eb", + "slug": "gore", + "tagline": "Blood! Blood! Blood!", + "avatar_url": "/gore.png", + "accent_color": "#f96680", + "delisted": false, + "logged_in_only": false, + "descriptions": [] + } + """; + JsonNode openAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/boba-openapi.json"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1boards~1{slug}/get/responses/200", "application/json"); + + assertFalse(errors.isEmpty()); + } + + @ParameterizedTest() + @CsvSource(value = { "null | {\"seasonData\":null} | true", + "notNull | {\"seasonData\": {\"seasonType\": \"BratSummer\", \"isTooHot\": false, \"isTooCold\": true}} | true", + "notNullInvalid | {\"seasonData\": {\"seasonType\": \"365PartyGirl\", \"isTooHot\": false, \"isTooCold\": true}} | false" }, delimiter = '|') + void testNullableAllOf(String testType, String jsonText, boolean expected) { + String openAPIText = null; + + JsonNode openAPISpec = null; + JsonNode contentNode = null; + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/season-nullable-all-of.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1seasonAllOf/post/responses/201", "application/json"); + + assertEquals(expected, errors.isEmpty()); + } + + + @ParameterizedTest() + @CsvSource(value = { "null | {\"seasonData\":null} | true", + "notNullOne | {\"seasonData\": {\"seasonType\": \"BratSummer\", \"isTooHot\": false, \"isTooCold\": true}} | true", + "notNullTwo | {\"seasonData\": {\"isUmami\": true, \"isSalty\": true}} | true", + "notNullInvalidAllMatching | {\"seasonData\": {\"seasonType\": \"BratSummer\", \"isTooHot\": false, \"isTooCold\": true, \"isUmami\": true, \"isSalty\": true}} | false", + "notNullInvalidNoneMatching | {\"seasonData\": {\"seasonType\": \"365PartyGirl\", \"isTooHot\": false, \"isTooCold\": true, \"isUmami\": true}} | false", }, delimiter = '|') + void testNullableOneOf(String testType, String jsonText, boolean expected) { + String openAPIText = null; + + JsonNode openAPISpec = null; + JsonNode contentNode = null; + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/season-nullable-all-of.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1seasonOneOf/post/responses/201", "application/json"); + + assertEquals(expected, errors.isEmpty()); + } + + @ParameterizedTest() + @CsvSource(value = { "null | {\"seasonData\":null} | true", + "notNullOne | {\"seasonData\": {\"seasonType\": \"BratSummer\", \"isTooHot\": false, \"isTooCold\": true}} | true", + "notNullTwo | {\"seasonData\": {\"isUmami\": true, \"isSalty\": true}} | true", + "notNullAllMatch | {\"seasonData\": {\"seasonType\": \"BratSummer\", \"isTooHot\": false, \"isTooCold\": true, \"isUmami\": true, \"isSalty\": true}} | true", + "notNullInvalid | {\"seasonData\": {\"seasonType\": \"365PartyGirl\", \"isTooHot\": false, \"isTooCold\": true, \"isUmami\": true}} | false", }, delimiter = '|') + void testNullableAnyOf(String testType, String jsonText, boolean expected) { + String openAPIText = null; + + JsonNode openAPISpec = null; + JsonNode contentNode = null; + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/season-nullable-all-of.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1seasonAnyOf/post/responses/201", "application/json"); + + assertEquals(expected, errors.isEmpty()); + } + + @Test + void testNullableFieldInComponentRef() { + String openAPIText = null; + String jsonText = """ + [ + { + "name": "Brian May", + "birthDate": "1947-07-19T00:00:00.0Z", + "deathDate": null + }, + { + "name": "Roger Taylor", + "birthDate": "1949-07-26T00:00:00.0Z", + "deathDate": null + }, + { + "name": "John Deacon", + "birthDate": "1951-08-19T00:00:00.0Z", + "deathDate": null + }, + { + "name": "Freddy Mercury", + "birthDate": "1946-09-15T00:00:00.0Z", + "deathDate": "1946-11-24T00:00:00.0Z" + } + ] + """; + JsonNode openAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/nullable-fields-openapi.yaml"), + StandardCharsets.UTF_8); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /accounts response message. + List errors = OpenAPISchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1queen~1members/get/responses/200", "application/json"); + assertTrue(errors.isEmpty()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/SwaggerSchemaValidatorTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/SwaggerSchemaValidatorTest.java new file mode 100644 index 000000000..d71ce8669 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/openapi/SwaggerSchemaValidatorTest.java @@ -0,0 +1,58 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a test case for SwaggerSchemaValidator utility. + * @author laurent + */ +class SwaggerSchemaValidatorTest { + + @Test + void testFullProcedureFromSwaggerResource() { + String openAPIText = null; + String jsonText = "{\n" + " \"name\": \"Rodenbach\",\n" + " \"country\": \"Belgium\",\n" + + " \"type\": \"Fruit\",\n" + " \"rating\": 4.3,\n" + " \"status\": \"available\"\n" + "}"; + JsonNode openAPISpec = null; + JsonNode contentNode = null; + + try { + // Load full specification from file. + openAPIText = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml")); + // Extract JSON nodes using OpenAPISchemaValidator methods. + openAPISpec = OpenAPISchemaValidator.getJsonNodeForSchema(openAPIText); + contentNode = OpenAPISchemaValidator.getJsonNode(jsonText); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + + // Validate the content for Get /beer/{name} response message. + List errors = SwaggerSchemaValidator.validateJsonMessage(openAPISpec, contentNode, + "/paths/~1beer~1{name}/get/responses/200"); + assertTrue(errors.isEmpty()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/soap/SoapMessageValidatorTest.java b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/soap/SoapMessageValidatorTest.java new file mode 100644 index 000000000..2f395a0c1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/java/io/github/microcks/util/soap/SoapMessageValidatorTest.java @@ -0,0 +1,193 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soap; + +import org.junit.jupiter.api.Test; + +import javax.xml.namespace.QName; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for SoapMessageValidator class. + * @author laurent + */ +class SoapMessageValidatorTest { + + private String validSoap = """ + + + + + Hello Andrew ! + + + + """; + + private String validSoapRequest = """ + + + + + Andrew + + + + """; + + private String validSoapNSOnBody = """ + + + + + Hello Andrew ! + + + + """; + + private String invalidSoap = """ + + + + + Hello Andrew ! + + + + """; + + private String invalidSoapMessage = """ + + + + + Hello Andrew ! + Bar + + + + """; + + private String validSoap12 = """ + + + + + Hello Andrew ! + + + + """; + + private String invalidSoap12 = """ + + + + + Hello Andrew ! + + + + bar + + + """; + + @Test + void testValidateSoapEnvelope() { + List errors = SoapMessageValidator.validateSoapEnvelope(validSoap); + assertTrue(errors.isEmpty()); + + errors = SoapMessageValidator.validateSoapEnvelope(invalidSoap); + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertTrue(errors.get(0).contains("soap:Envelope")); + + errors = SoapMessageValidator.validateSoapEnvelope(validSoap12); + assertTrue(errors.isEmpty()); + + errors = SoapMessageValidator.validateSoapEnvelope(invalidSoap12); + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertTrue(errors.get(0).contains("soap:Footer")); + } + + @Test + void testValidateSoapMessage() { + String wsdlContent = null; + try { + wsdlContent = new String( + Files.readAllBytes(Paths.get("target/test-classes/io/github/microcks/util/soap/HelloService.wsdl")), + StandardCharsets.UTF_8); + } catch (Exception e) { + fail("No exception should not occur here"); + } + + List errors = SoapMessageValidator.validateSoapMessage(wsdlContent, + new QName("http://www.example.com/hello", "sayHelloResponse"), validSoap, "http://localhost:8080"); + assertTrue(errors.isEmpty()); + + errors = SoapMessageValidator.validateSoapMessage(wsdlContent, + new QName("http://www.example.com/hello", "sayHelloResponse"), validSoapNSOnBody, "http://localhost:8080"); + assertTrue(errors.isEmpty()); + + errors = SoapMessageValidator.validateSoapMessage(wsdlContent, + new QName("http://www.example.com/hello", "sayHelloResponse"), validSoapRequest, "http://localhost:8080"); + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertTrue(errors.get(0).contains("Expecting a {http://www.example.com/hello}sayHelloResponse element")); + + errors = SoapMessageValidator.validateSoapMessage(wsdlContent, + new QName("http://www.example.com/hello", "sayHelloResponse"), invalidSoapMessage, "http://localhost:8080"); + assertFalse(errors.isEmpty()); + assertEquals(1, errors.size()); + assertTrue(errors.get(0).contains("foo")); + } + + @Test + void testValidateSoapMessageWithDistributedNS() { + String soapMessage = """ + + + + + Andrew + + + + """; + + String wsdlContent = null; + try { + wsdlContent = new String( + Files.readAllBytes(Paths.get("target/test-classes/io/github/microcks/util/soap/HelloService.wsdl")), + StandardCharsets.UTF_8); + } catch (Exception e) { + fail("No exception should not occur here"); + } + + List errors = SoapMessageValidator.validateSoapMessage(wsdlContent, + new QName("http://www.example.com/hello", "sayHello"), soapMessage, "http://localhost:8080"); + assertTrue(errors.isEmpty()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi-oneof-2.1.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi-oneof-2.1.yaml new file mode 100644 index 000000000..bf65d30ef --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi-oneof-2.1.yaml @@ -0,0 +1,35 @@ +asyncapi: '2.1.0' +info: + title: Account Service + version: 1.2.1 + description: This service is in charge of processing user signups +channels: + user/signedup: + subscribe: + message: + payload: + oneOf: + - $ref: '#/components/schemas/signup' + - $ref: '#/components/schemas/login' + examples: + - name: Alice + payload: + displayName: Alice + - name: Bob + payload: + email: bob@example.com +components: + schemas: + signup: + type: object + properties: + displayName: + type: string + additionalProperties: false + login: + type: object + properties: + email: + type: string + format: email + additionalProperties: false \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi-oneof-2.3.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi-oneof-2.3.yaml new file mode 100644 index 000000000..75dbdc538 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi-oneof-2.3.yaml @@ -0,0 +1,39 @@ +asyncapi: '2.3.0' +info: + title: Account Service + version: 1.2.2 + description: This service is in charge of processing user signups +channels: + user/signedup: + subscribe: + message: + oneOf: + - $ref: '#/components/messages/signup' + - $ref: '#/components/messages/login' +components: + messages: + signup: + payload: + type: object + properties: + displayName: + type: string + required: + - displayName + examples: + - name: Alice + payload: + displayName: Alice + login: + payload: + type: object + properties: + email: + type: string + format: email + required: + - email + examples: + - name: Bob + payload: + email: bob@example.com \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml new file mode 100644 index 000000000..bcd65dfb6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml @@ -0,0 +1,41 @@ +asyncapi: '2.1.0' +info: + title: Account Service + version: 1.2.0 + description: This service is in charge of processing user signups +channels: + user/signedup: + subscribe: + message: + $ref: '#/components/messages/UserSignedUp' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + age: + type: integer + format: int32 + size: + type: number + format: float + exp: + type: integer + format: int64 + rewards: + type: number + format: double + required: + - displayName + additionalProperties: false + examples: + - name: Laurent + payload: + displayName: Laurent Broudoux + age: 43 + size: 1.8 + exp: 1234567891011 + rewards: 12345.67 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-ref-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-ref-asyncapi.yaml new file mode 100644 index 000000000..89604e101 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-ref-asyncapi.yaml @@ -0,0 +1,49 @@ +asyncapi: '2.1.0' +info: + title: Account Service + version: 1.2.0 + description: This service is in charge of processing user signups +channels: + user/signedup: + subscribe: + message: + $ref: '#/components/messages/UserSignedUp' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + age: + type: integer + format: int32 + size: + $ref: '#/components/schemas/size' + exp: + $ref: '#/components/schemas/exp' + rewards: + type: number + format: double + required: + - displayName + additionalProperties: false + examples: + - name: Laurent + payload: + displayName: Laurent Broudoux + age: 43 + size: 1.8 + exp: 1234567891011 + rewards: 12345.67 + schemas: + size: + type: number + format: float + exp: + type: object + properties: + level: + type: integer + format: int64 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-ref-ref-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-ref-ref-asyncapi.yaml new file mode 100644 index 000000000..0cf4d49d7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/account-service-ref-ref-asyncapi.yaml @@ -0,0 +1,52 @@ +asyncapi: '2.1.0' +info: + title: Account Service + version: 1.2.0 + description: This service is in charge of processing user signups +channels: + user/signedup: + subscribe: + message: + $ref: '#/components/messages/UserSignedUp' +components: + schemas: + size: + type: number + format: float + exp: + type: object + properties: + level: + type: integer + format: int64 + UserSignedUpType: + type: object + properties: + displayName: + type: string + age: + type: integer + format: int32 + size: + $ref: '#/components/schemas/size' + exp: + $ref: '#/components/schemas/exp' + rewards: + type: number + format: double + required: + - displayName + additionalProperties: false + messages: + UserSignedUp: + payload: + oneOf: + - $ref: '#/components/schemas/UserSignedUpType' + examples: + - name: Laurent + payload: + displayName: Laurent Broudoux + age: 43 + size: 1.8 + exp: 1234567891011 + rewards: 12345.67 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/spring-cloud-stream-asyncapi-nulls.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/spring-cloud-stream-asyncapi-nulls.yaml new file mode 100644 index 000000000..9f34bfabd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/spring-cloud-stream-asyncapi-nulls.yaml @@ -0,0 +1,331 @@ +asyncapi: 2.1.0 +info: + title: spring-cloud-stream-request-reply API + version: 1.0.2 + description: spring-cloud-stream-request-reply + x-view: client + x-java-package: org.acme.asyncapi.client.api + contact: + name: API Support, Team 33 + x-microcks: + labels: + domain: domaineA + status: GA + team: Team 33 +servers: + local: + url: '{host}:{port}' + protocol: amqp + description: Local broker + security: + - localRabbitMQ: [] + variables: + host: + description: rabbit local. + default: localhost + port: + default: '5672' + enum: + - '5672' + dev: + url: '{host}:{port}' + protocol: amqp + description: DEV broker + security: + - devRabbitMQ: [] + variables: + host: + description: rabbit dev. + default: rabbit.dev.be + port: + default: '5672' + enum: + - '5672' +defaultContentType: application/json +channels: + domaineA.service1.getPerson.v1.0.0: + description: The topic on which measured values may be produced and consumed. + bindings: + amqp: + is: routingKey + queue: + name: domaineA.service1.getPerson.v1.0.0 + durable: true + autoDelete: false + exchange: + name: domaineA.service1.getPerson.v1.0.0 + type: topic + durable: true + autoDelete: false + publish: + description: PUBLISH, qui définit les messages consommés par l'application à partir du canal. + operationId: getPersonByQuery-in + x-scs-function-name: getPersonByQuery + x-scs-group: getPersonByQuery + traits: + - $ref: '#/components/operationTraits/rabbit' + message: + $ref: '#/components/messages/personQueryMessage' + domaineA.service1.replier.v1.0.0: + bindings: + amqp: + is: routingKey + queue: + name: domaineA.service1.replier.v1.0.0 + durable: true + autoDelete: false + exchange: + name: domaineA.service1.replier.v1.0.0 + type: topic + durable: true + autoDelete: false + subscribe: + operationId: getPersonByQuery-out + summary: Receive information + description: subscribe signifie que l'application permet aux consommateurs de s'abonner au canal pour recevoir les messages produits par l'application. + x-scs-function-name: getPersonByQuery + x-scs-group: respond + traits: + - $ref: '#/components/operationTraits/rabbit' + x-microcks-operation: + frequency: 30 + # https://microcks.io/documentation/using/asyncapi/#using-asyncapi-extensions + message: + $ref: '#/components/messages/messagePayload' + publish: + operationId: respond + summary: cette méthode nécessite le header amqp_replyTo + description: cette méthode nécessite le header amqp_replyTo + x-scs-group: respond + traits: + - $ref: '#/components/operationTraits/rabbit' + message: + $ref: '#/components/messages/responseMessage' +components: + messages: + responseMessage: + payload: + $ref: '#/components/schemas/responseMessage' + examples: + - name: Résponse Random OK + summary: Exemple De réponse Random OK vers l'appellant + headers: + correlationId: my-correlation-id + responseCode: 200 + amqp_replyTo: jeSaisOuTuEs:P + payload: + { + "throwable": null, + "person": { + "taille": 180, + "nom": "{{randomLastName()}}", + "prenom": "{{randomFirstName()}}", + "dateNaissance": "{{now(yyyy-MM-dd'T'HH:mm:ssZ)}}" + } + } + - name: Résponse OK + summary: Exemple De réponse OK vers l'appellant + headers: + correlationId: my-correlation-id + responseCode: 200 + amqp_replyTo: jeSaisOuTuEs:P + payload: + { + "throwable": null, + "person": { + "taille": 110, + "nom": "Bennour", + "prenom": "Hassen", + "dateNaissance": "2000-08-24T14:15:22Z" + } + } + - name: Résponse NullPointerException + summary: Exemple De réponse NOK vers l'appellant + headers: + correlationId: my-correlation-id + responseCode: 500 + amqp_replyTo: jeSaisOuTuEs:P + payload: + { + "throwable": { + "detailMessage": "un petit coucou si tu oublie le prenom par exemple", + "clazz": "java.lang.NullPointerException" + }, + "person": { + "taille": 110, + "nom": "Bennour", + "prenom": null, + "dateNaissance": "2000-08-24T14:15:22Z" + } + } + personQueryMessage: + payload: + $ref: '#/components/schemas/personQueryMessage' + headers: + $ref: '#/components/schemas/amqp_replyTo' + examples: + - name: inputNomOK + summary: Exemple D'envois acceptés + payload: + { + "nom": "Bennour" + } + - name: inputNomNOK + summary: Exemple D'envois non acceptés + payload: + { + "nom": null + } + - name: inputPrenomOK + summary: Exemple D'envois acceptés + payload: + { + "prenom": "Hassen" + } + - name: inputPrenomNOK + summary: Exemple D'envois non acceptés + payload: + { + "prenom": null + } + messagePayload: + payload: + $ref: '#/components/schemas/messagePayload' + examples: + - name: Résponse Random OK + summary: Exemple De réponse Random OK vers l'appellant + headers: + correlationId: my-correlation-id + responseCode: 200 + amqp_replyTo: jeSaisOuTuEs:P + payload: + { + "throwable": null, + "person": { + "taille": 180, + "nom": "{{randomLastName()}}", + "prenom": "{{randomFirstName()}}", + "dateNaissance": "{{now(yyyy-MM-dd'T'HH:mm:ssZ)}}" + } + } + - name: Résponse OK + summary: Exemple De réponse OK vers l'appellant + headers: + correlationId: my-correlation-id + responseCode: 200 + amqp_replyTo: jeSaisOuTuEs:P + payload: + { + "throwable": null, + "person": { + "taille": 110, + "nom": "Bennour", + "prenom": "Hassen", + "dateNaissance": "2000-08-24T14:15:22Z" + } + } + - name: Résponse NullPointerException + summary: Exemple De réponse NOK vers l'appellant + headers: + correlationId: my-correlation-id + responseCode: 500 + amqp_replyTo: jeSaisOuTuEs:P + payload: + { + "throwable": { + "detailMessage": "un petit coucou si tu oublie le prenom par exemple", + "clazz": "java.lang.NullPointerException" + }, + "person": { + "taille": 110, + "nom": "Bennour", + "prenom": null, + "dateNaissance": "2000-08-24T14:15:22Z" + } + } + schemas: + messagePayload: + type: object + properties: + throwable: + $ref: '#/components/schemas/nullableThrowable' + person: + $ref: '#/components/schemas/person' + required: [ + person + ] + nullableThrowable: + oneOf: + - $ref: '#/components/schemas/throwable' + - type: 'null' + throwable: + type: object + properties: + detailMessage: + type: string + clazz: + type: string + person: + type: [object, 'null'] + properties: + taille: + type: number + format: integer + description: size + formatFloat: + type: number + format: float + nom: + type: string + description: nom + prenom: + type: string + description: prenom + dateNaissance: + type: string + format: date-time + required: [ + nom,prenom + ] + amqp_replyTo: + type: object + location: $message.header#/amqp_replyTo + properties: + amqp_replyTo: + description: adresse de réponse de forme org.springframework.amqp.support.AmqpHeaders#REPLY_TO + type: string + required: [ + amqp_replyTo + ] + personQueryMessage: + type: object + properties: + prenom: + type: string + description: prenom + nom: + type: string + description: nom + minProperties: 1 + responseMessage: + type: object + properties: + throwable: + $ref: '#/components/schemas/throwable' + data: + oneOf: + - $ref: '#/components/schemas/person' + required: [ + person + ] + securitySchemes: + localRabbitMQ: + description: local user/pwd, defaults guest/guest + type: userPassword + devRabbitMQ: + description: dev user/pwd, defaults test/test + type: userPassword + operationTraits: + rabbit: + bindings: + rabbit: null diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml new file mode 100644 index 000000000..665ffa5f8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml @@ -0,0 +1,89 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.streetlights' +info: + title: Streetlights API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + smartylighting/streetlights/event/lighting/measured: + description: The topic on which measure values may be consumed + subscribe: + summary: Receive informations about streelights measures + operationId: receiveStreelightsMeasures + bindings: + mqtt: + qos: 0 + retain: false + message: + $ref: '#/components/messages/lightMeasured' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: Inform about environmental lighting conditions of a particular streetlight. + contentType: application/json + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + examples: + - dev0: + summary: Example for Device 0 + headers: |- + {"my-app-header": 14} + payload: |- + {"streetlightId":"dev0", "lumens":1000, "sentAt":"{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}"} + - dev1: + summary: Example for Device 1 + headers: + my-app-header: 14 + payload: + streetlightId: dev1 + lumens: 1100 + sentAt: "{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}" + - dev2: + summary: Example for Device 2 + headers: + my-app-header: 14 + payload: + streetlightId: dev2 + lumens: 1200 + sentAt: "{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}" + schemas: + lightMeasuredPayload: + type: object + properties: + streetlightId: + type: string + description: The ID of the streetlight. + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + additionalProperties: false + required: + - streetlightId + - lumens + - sentAt + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml new file mode 100644 index 000000000..bc9005754 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml @@ -0,0 +1,86 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.3.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent@microcks.io + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user-signedup: + description: The topic on which user signed up events may be consumed + messages: + userSignedUp: + $ref: '#/components/messages/userSignedUp' +operations: + publishUserSignedUps: + action: 'send' + channel: + $ref: '#/channels/user-signedup' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/user-signedup/messages/userSignedUp' +components: + messages: + userSignedUp: + description: An event describing that a user just signed up + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: + fullName: Laurent Broudoux + email: "laurent@microcks.io" + age: 41 + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + - laurent: + my-app-header: 21 + - yacine: + my-app-header: 22 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-oneof-3.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-oneof-3.0.yaml new file mode 100644 index 000000000..e80cdea70 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-oneof-3.0.yaml @@ -0,0 +1,121 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.3.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent@microcks.io + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user-signedupout: + description: The topic on which user signed up or out events may be consumed + messages: + userSignedUp: + $ref: '#/components/messages/userSignedUp' + userSignedOut: + $ref: '#/components/messages/userSignedOut' +operations: + publishUserSignedUpOut: + action: 'send' + channel: + $ref: '#/channels/user-signedupout' + summary: Receive information about user signed up or out + messages: + - $ref: '#/channels/user-signedupout/messages/userSignedUp' + - $ref: '#/channels/user-signedupout/messages/userSignedOut' +components: + messages: + userSignedUp: + description: An event describing that a user just signed up + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: + fullName: Laurent Broudoux + email: "laurent@microcks.io" + age: 41 + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 + userSignedOut: + description: An event describing that a user just signed out + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + id: + type: string + examples: + - name: laurent + summary: Example for Laurent user + headers: + my-app-header: 23 + sentAt: "2024-02-06T08:02:38Z" + payload: + id: c62a2b1e-0dfc-44b5-88cc-9aba252a1368 + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2024-02-06T08:03:38Z" + payload: + id: dc38d465-bbeb-4fab-a84d-6cff46394716 + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + - laurent: + my-app-header: 21 + - yacine: + my-app-header: 22 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml new file mode 100644 index 000000000..41688f759 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml @@ -0,0 +1,79 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi-oneof-2.3.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi-oneof-2.3.yaml new file mode 100644 index 000000000..9702c787f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi-oneof-2.3.yaml @@ -0,0 +1,45 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up Avro API + version: 0.1.1 + description: Sample AsyncAPI for user signedup/login events defined using Avro +defaultContentType: application/json +channels: + user/signedup: + subscribe: + message: + description: An event describing that a user just signed up. + oneOf: + - $ref: '#/components/messages/signup' + - $ref: '#/components/messages/login' +components: + messages: + signup: + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + doc: User information + name: SignupUser + fields: + - name: displayName + type: string + examples: + - name: Alice + payload: + displayName: Alice + login: + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + doc: User information + name: LoginUser + fields: + - name: email + type: string + examples: + - name: Bob + payload: + email: bob@example.com diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi-oneof-3.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi-oneof-3.0.yaml new file mode 100644 index 000000000..7dda6026e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi-oneof-3.0.yaml @@ -0,0 +1,54 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up Avro API + version: 0.3.1 + description: Sample AsyncAPI for user signedup/login events defined using Avro +defaultContentType: application/json +channels: + user-signedup: + description: An event describing that a user just signed up. + messages: + userSignUp: + $ref: '#/components/messages/signup' + userLogin: + $ref: '#/components/messages/login' +operations: + publishUserSignUpLogin: + action: 'send' + channel: + $ref: '#/channels/user-signedup' + summary: Receive information about user signed up or login + messages: + - $ref: '#/channels/user-signedup/messages/userSignUp' + - $ref: '#/channels/user-signedup/messages/userLogin' +components: + messages: + signup: + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + doc: User information + name: SignupUser + fields: + - name: displayName + type: string + examples: + - name: Alice + payload: + displayName: Alice + login: + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + doc: User information + name: LoginUser + fields: + - name: email + type: string + examples: + - name: Bob + payload: + email: bob@example.com diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml new file mode 100644 index 000000000..a0da34234 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml @@ -0,0 +1,80 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up Avro API + version: 0.1.1 + description: Sample AsyncAPI for user signedup events defined using Avro + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + doc: User information + name: User + fields: + - name: fullName + type: string + - name: email + type: string + - name: age + type: int + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi-oneof-2.3.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi-oneof-2.3.yaml new file mode 100644 index 000000000..dcb4e0479 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi-oneof-2.3.yaml @@ -0,0 +1,35 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up Avro API + version: 0.1.1 + description: Sample AsyncAPI for user signedup/login events defined using Avro +defaultContentType: application/json +channels: + user/signedup: + subscribe: + message: + description: An event describing that a user just signed up. + oneOf: + - $ref: '#/components/messages/signup' + - $ref: '#/components/messages/login' +components: + messages: + signup: + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + $ref: './user-signedup-signup.avsc#/SignUser' + examples: + - name: Alice + payload: + displayName: Alice + login: + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + $ref: './user-signedup-login.avsc#/LoginUser' + examples: + - name: Bob + payload: + email: bob@example.com diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml new file mode 100644 index 000000000..c8137b9f1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml @@ -0,0 +1,71 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up Avro API + version: 0.1.1 + description: Sample AsyncAPI for user signedup events defined using Avro + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + $ref: './user-signedup.avsc#/User' + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-bad.avsc b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-bad.avsc new file mode 100644 index 000000000..2b3ec0540 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-bad.avsc @@ -0,0 +1,9 @@ +{"namespace": "microcks.avro", + "type": "record", + "name": "User", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "email", "type": "string"}, + {"name": "age", "type": "int"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml new file mode 100644 index 000000000..2b958000e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml @@ -0,0 +1,74 @@ +asyncapi: '2.4.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + $ref: '#/components/schemas/UserSignedupEvent' + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + schemas: + UserSignedupEvent: + type: object + allOf: + - $ref: "user-signedup.json" + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-login.avsc b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-login.avsc new file mode 100644 index 000000000..3301586bd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-login.avsc @@ -0,0 +1,7 @@ +{"namespace": "microcks.avro", + "type": "record", + "name": "LoginUser", + "fields": [ + {"name": "email", "type": "string"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-out-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-out-asyncapi.yaml new file mode 100644 index 000000000..9357f14f5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-out-asyncapi.yaml @@ -0,0 +1,121 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + $ref: '#/components/schemas/userSigned' + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 + user/signedout: + description: The topic on which user signed out events may be consumed + subscribe: + summary: Receive informations about user signed out + operationId: receivedUserSIgnedOut + message: + description: An event describing that a user just signed out. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + $ref: '#/components/schemas/userSigned' + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + schemas: + userSigned: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schema.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schema.json new file mode 100644 index 000000000..673c895c3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schema.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "fullName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer", + "minimum": 18 + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schema.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schema.yaml new file mode 100644 index 000000000..cdecbaa96 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schema.yaml @@ -0,0 +1,11 @@ +type: object +additionalProperties: false +properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-signup.avsc b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-signup.avsc new file mode 100644 index 000000000..0939ada2c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-signup.avsc @@ -0,0 +1,7 @@ +{"namespace": "microcks.avro", + "type": "record", + "name": "SignupUser", + "fields": [ + {"name": "displayName", "type": "string"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.avsc b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.avsc new file mode 100644 index 000000000..09b4d4cb6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.avsc @@ -0,0 +1,9 @@ +{"namespace": "microcks.avro", + "type": "record", + "name": "User", + "fields": [ + {"name": "fullName", "type": "string"}, + {"name": "email", "type": "string"}, + {"name": "age", "type": "int"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.json new file mode 100644 index 000000000..5c4f0f483 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "UserSignedupEvent Specification JSON Schema", + "type": "object", + "additionalProperties": false, + "properties": { + "fullName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer", + "minimum": 18 + } + } +} + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/car-schema-no-addon.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/car-schema-no-addon.json new file mode 100644 index 000000000..1b53fc23e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/car-schema-no-addon.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Car", + "description": "A description of a Car", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of the car", + "type": "string" + }, + "year": { + "description": "Build year", + "type": "integer", + "minimum": 1950, + "exclusiveMinimum": true + }, + "model": { + "description": "Model of the car", + "type": "string" + } + }, + "required": ["name"] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/car-schema.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/car-schema.json new file mode 100644 index 000000000..2fe5d0618 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/car-schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Car", + "description": "A description of a Car", + "type": "object", + "properties": { + "name": { + "description": "Name of the car", + "type": "string" + }, + "year": { + "description": "Build year", + "type": "integer", + "minimum": 1950, + "exclusiveMinimum": true + }, + "model": { + "description": "Model of the car", + "type": "string" + } + }, + "required": ["name"] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/graphql/basic-heroes.graphql b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/graphql/basic-heroes.graphql new file mode 100644 index 000000000..e6cf49102 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/graphql/basic-heroes.graphql @@ -0,0 +1,20 @@ +schema { + query: Query +} +type Query { + hero: Hero +} +type Hero { + name: String! + email: String + movies: [Movie] + family: Family! + affiliate: Family +} +type Movie { + title: String! +} +enum Family { + MARVEL + DC +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/graphql/films.graphql b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/graphql/films.graphql new file mode 100644 index 000000000..f6cd1f0ae --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/graphql/films.graphql @@ -0,0 +1,34 @@ +# microcksId: Movie Graph API : 1.0 +schema { + query: Query + mutation: Mutation +} +type Film { + id: String! + title: String! + episodeID: Int! + director: String! + starCount: Int! + rating: Float! +} + +type FilmsConnection { + totalCount: Int! + films: [Film] +} + +input Review { + comment: String + rating: Int +} + +type Query { + allFilms: FilmsConnection + film(id: String): Film +} + +type Mutation { + addStar(filmId: String): Film + addReview(filmId: String, review: Review): Film +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/invalid-schema.xsd b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/invalid-schema.xsd new file mode 100644 index 000000000..976f5a858 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/invalid-schema.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml new file mode 100644 index 000000000..37c3e0864 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml @@ -0,0 +1,143 @@ +swagger: '2.0' +info: + title: Beer Catalog API + version: '0.9' + description: An API for querying beer catalog of Acme Inc. + contact: + name: Laurent Broudoux + url: 'http://github.com/lbroudoux' + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: 'https://opensource.org/licenses/MIT' + x-microcks: + labels: + domain: beers + status: beta + team: Team A +paths: + '/beer/{name}': + get: + produces: + - application/json + tags: + - beer + responses: + '200': + description: Beer having requested name + schema: + $ref: '#/definitions/Beer' + examples: + application/json: |- + { + "name": "Rodenbach", + "country": "Belgium", + "type": "Fruit", + "rating": 4.3, + "status": "available" + } + operationId: GetBeer + summary: Get beer having name + description: Get beer having name + parameters: + - + name: name + description: Name of beer to retrieve + in: path + required: true + type: string + '/beer/findByStatus/{status}': + get: + produces: + - application/json + tags: + - beer + responses: + '200': + description: List of beers having requested status + schema: + type: array + items: + $ref: '#/definitions/Beer' + examples: + application/json: |- + [ + { + "name": "Rodenbach, + "country": "Belgium", + "type": "Fruit", + "rating": 4.2, + "status": "available" + }, + { + "name": "Weissbier", + "country": "Germany", + "type": "Wheat", + "rating": 4.1, + "status": "available" + } + ] + operationId: FindBeersByStatus + summary: Get beers having status + description: Get beers having status + parameters: + - + name: status + description: Status of beers to retrieve + in: path + required: true + type: string + - + name: page + description: Number of page to retrieve + in: query + type: number + /beer: + get: + tags: + - beer + parameters: + - + name: myparam + description: Description + in: query + required: true + type: string + responses: + '200': + description: Array of beers + schema: + type: array + items: + $ref: '#/definitions/Beer' + operationId: ListBeers + summary: List beers within catalog + description: List beers within catalog + parameters: + - + name: page + description: Number of page to retrieve + in: query + type: number +definitions: + Beer: + properties: + name: + description: Name of Beer + type: string + country: + description: Origin country of Beer + type: string + type: + description: Type of Beer + type: string + rating: + description: Rating from customers + type: number + status: + description: Stock status + type: string +tags: + - + name: beer + description: Beer resource \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/boba-openapi.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/boba-openapi.json new file mode 100644 index 000000000..4f8c5598c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/boba-openapi.json @@ -0,0 +1,1654 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "BobaBoard's API documentation. Woho!", + "version": "0.0.1", + "description": "\n# Intro\nWelcome to the BobaBoard's backend API. This is still a WIP.\n\n# Example Section\nThis is just to test that sections work. It will be written better later.\n ", + "contact": { + "name": "Ms. Boba", + "url": "url", + "email": "email" + } + }, + "servers": [ + { + "url": "http://localhost:4200/", + "description": "Development server" + } + ], + "tags": [ + { + "name": "/posts/", + "description": "APIs related to the /posts/ endpoints." + }, + { + "name": "/threads/", + "description": "APIs related to the /threads/ endpoints." + }, + { + "name": "/boards/", + "description": "APIs related to the /boards/ endpoints." + }, + { + "name": "/realms/", + "description": "APIs related to the /realms/ endpoints." + }, + { + "name": "/users/", + "description": "APIs related to the /users/ endpoints." + }, + { + "name": "todo", + "description": "APIs whose documentation still needs work." + }, + { + "name": "models", + "x-displayName": "Models", + "description": "\n## Contribution\n\n\n## Tags\n\n\n## Comment\n\n\n## Board Summary (logged in)\n\n\n## Descriptions\n\n" + } + ], + "x-tagGroups": [ + { + "name": "general", + "tags": ["/realms/", "/boards/", "/threads/", "/posts/", "/users/"] + }, + { + "name": "models", + "tags": ["models"] + } + ], + "paths": { + "/boards/{slug}": { + "get": { + "summary": "Fetches board metadata.", + "tags": ["/boards/"], + "security": [ + { + "firebase": [] + }, + [] + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "The slug of the board to update.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "examples": { + "existing": { + "summary": "An existing board", + "value": "gore" + }, + "locked": { + "summary": "A board for logged in users only", + "value": "restricted" + }, + "not-found": { + "summary": "A board that does not exists", + "value": "this_does_not_exist" + } + } + } + ], + "responses": { + "200": { + "description": "The board metadata.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/BoardMetadata" + }, + { + "$ref": "#/components/schemas/LoggedInBoardMetadata" + } + ] + }, + "examples": { + "existing": { + "$ref": "#/components/examples/BoardsGore" + } + } + } + } + }, + "401": { + "description": "User was not found and board requires authentication." + }, + "403": { + "description": "User is not authorized to fetch the metadata of this board." + }, + "404": { + "description": "The board was not found." + } + } + } + }, + "boards/{slug}/metadata/update": { + "post": { + "summary": "Update boards metadata", + "tags": ["/boards/", "todo"] + } + }, + "boards/{slug}/visits": { + "get": { + "summary": "Sets last visited time for board", + "tags": ["/boards/"], + "security": [ + { + "firebase": [] + }, + [] + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "The slug of the board to update.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The visit was successfully registered." + }, + "401": { + "description": "User was not found." + } + } + } + }, + "boards/{slug}/mute": { + "post": { + "summary": "Mutes a board.", + "description": "Mutes the specified board for the current user.", + "tags": ["/boards/"], + "security": [ + { + "firebase": [] + } + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "The name of the board to mute.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The board was successfully muted." + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + }, + "delete": { + "summary": "Unmutes a board.", + "description": "Unmutes the specified board for the current user.", + "tags": ["/boards/"], + "security": [ + { + "firebase": [] + } + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "The name of the board to unmute.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The board was successfully unmuted." + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + } + }, + "boards/{slug}/pin": { + "post": { + "summary": "Pins a board.", + "description": "Pins the specified board for the current user.", + "tags": ["/boards/"], + "security": [ + { + "firebase": [] + } + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "The name of the board to pin.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The board was successfully pinned." + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + }, + "delete": { + "summary": "Unpins a board.", + "description": "Unpins the specified board for the current user.", + "tags": ["/boards/"], + "security": [ + { + "firebase": [] + } + ], + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "The name of the board to unpin.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The board was successfully unpinned." + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + } + }, + "boards/{slug}/notifications/dismiss": { + "post": { + "summary": "Dismiss all notifications for board {slug}", + "tags": ["/boards/", "todo"] + } + }, + "boards/{slug}/activity/latest": { + "get": { + "summary": "Get latest board activity (TODO).", + "tags": ["/boards/", "todo"], + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "The slug of the board to fetch the activity of.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The board activity.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoardActivity" + } + } + } + }, + "404": { + "description": "The board was not found." + } + } + } + }, + "posts/{post_id}/contribution": { + "post": { + "summary": "Replies to a contribution.", + "description": "Posts a contribution replying to the one with id {postId}.", + "tags": ["/posts/"], + "security": [ + { + "firebase": [] + } + ], + "parameters": [ + { + "name": "post_id", + "in": "path", + "description": "The uuid of the contribution to reply to.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The details of the contribution to post.", + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "properties": { + "content": { + "required": true, + "type": "string", + "format": "quill-delta" + } + } + }, + { + "$ref": "#/components/schemas/Tags" + }, + { + "$ref": "#/components/params/Identity" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "The contribution was successfully created.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contribution": { + "$ref": "#/components/schemas/Contribution", + "description": "Finalized details of the contributions just posted." + } + } + } + } + } + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + }, + "patch": { + "summary": "Edits a contribution.", + "description": "Edits a contribution (for now just its tags).", + "tags": ["/posts/"], + "security": [ + { + "firebase": [] + } + ], + "parameters": [ + { + "name": "post_id", + "in": "path", + "description": "The uuid of the contribution to edit.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The details of the contribution to edit.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tags" + } + } + } + }, + "responses": { + "200": { + "description": "The contribution was successfully edited.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contribution": { + "$ref": "#/components/schemas/Contribution", + "description": "Finalized details of the contributions just edited." + } + } + } + } + } + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + } + }, + "posts/{post_id}/comment": { + "post": { + "summary": "Add comments to a contribution, optionally nested under another comment.", + "description": "Creates a comment nested under the contribution with id {post_id}.", + "tags": ["/posts/"], + "security": [ + { + "firebase": [] + } + ], + "parameters": [ + { + "name": "post_id", + "in": "path", + "description": "The uuid of the contribution to reply to.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "The details of the comment to post.", + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "properties": { + "contents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Comment" + } + }, + "reply_to_comment_id": { + "nullable": true, + "type": "string", + "format": "uuid" + } + } + }, + { + "$ref": "#/components/params/Identity" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "The comments were successfully created.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "comments": { + "description": "Finalized details of the comments just posted.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Comment" + } + } + } + } + } + } + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + } + }, + "realms/slug/{realm_slug}/": { + "get": { + "summary": "Fetches the top-level realm metadata by slug.", + "tags": ["/realms/"], + "parameters": [ + { + "name": "realm_slug", + "in": "path", + "description": "The slug of the realm.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The realm metadata.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Realm" + } + } + } + } + } + } + }, + "realms/{realm_id}/activity": { + "get": { + "summary": "Fetches latest activity summary for the realm.", + "tags": ["/realms/"], + "security": [ + [], + { + "firebase": [] + } + ], + "parameters": [ + { + "name": "realm_id", + "in": "path", + "description": "The id of the realm.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The realm activity summary.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmActivity" + } + } + } + } + } + } + }, + "threads/{thread_id}": { + "get": { + "summary": "Fetches thread data.", + "tags": ["/threads/"], + "security": [ + { + "firebase": [] + }, + [] + ], + "parameters": [ + { + "name": "thread_id", + "in": "path", + "description": "The id of the thread to fetch.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The thread data.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Thread" + } + } + } + }, + "401": { + "description": "User was not found and thread requires authentication." + }, + "403": { + "description": "User is not authorized to fetch this thread." + }, + "404": { + "description": "The thread was not found." + } + } + } + }, + "users/@me": { + "get": { + "summary": "Gets data for the current user.", + "tags": ["/users/"], + "security": [ + { + "firebase": [] + } + ], + "responses": { + "200": { + "description": "The user data.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "pinned_boards": { + "description": "A map from board id to its LoggedInSummary for each pinned board.\n", + "type": "object", + "additionalProperties": { + "allOf": [ + { + "$ref": "#/components/schemas/LoggedInBoardSummary" + }, + { + "type": "object", + "properties": { + "index": { + "type": "number" + } + }, + "required": ["index"] + } + ] + } + } + }, + "required": ["pinned_boards"] + } + } + } + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + } + }, + "users/@me/notifications": { + "get": { + "summary": "Gets notifications data for the current user.", + "description": "Gets notifications data for the current user, including pinned boards.\nIf `realm_id` is present, also fetch notification data for the current realm.\n", + "tags": ["/users/"], + "security": [ + { + "firebase": [] + } + ], + "responses": { + "200": { + "description": "The notifications data.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "has_notifications": { + "type": "boolean" + }, + "is_outdated_notifications": { + "type": "boolean" + }, + "realm_boards": { + "description": "A map from board id to its NotificationsStatus for each realm board.\nIf `realm_id` is not present in the params, it will be empty.\n", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BoardNotifications" + } + }, + "pinned_boards": { + "description": "A map from board id to its NotificationsStatus for each pinned board.\n", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BoardNotifications" + } + } + }, + "required": [ + "has_notifications", + "is_outdated_notifications", + "pinned_boards", + "realm_boards" + ] + } + } + } + }, + "401": { + "description": "User was not found in request that requires authentication." + }, + "403": { + "description": "User is not authorized to perform the action." + } + } + } + } + }, + "components": { + "schemas": { + "BoardActivity": { + "type": "object", + "properties": { + "cursor": { + "$ref": "#/components/schemas/Cursor" + }, + "activity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ThreadSummary" + } + } + } + }, + "BoardActivitySummary": { + "type": "object", + "properties": { + "last_post_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_comment_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_activity_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_activity_from_others_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_visit_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "required": [ + "id", + "last_post_at", + "last_comment_at", + "last_activity_at", + "last_activity_from_others_at", + "last_visit_at" + ] + }, + "BoardSummary": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "realm_id": { + "type": "string", + "format": "uuid" + }, + "slug": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "tagline": { + "type": "string" + }, + "accent_color": { + "type": "string", + "format": "color" + }, + "logged_in_only": { + "type": "boolean" + }, + "delisted": { + "type": "boolean" + } + }, + "required": [ + "id", + "realm_id", + "slug", + "avatar_url", + "tagline", + "accent_color", + "logged_in_only", + "delisted" + ] + }, + "LoggedInBoardSummary": { + "allOf": [ + { + "$ref": "#/components/schemas/BoardSummary" + }, + { + "type": "object", + "properties": { + "muted": { + "type": "boolean" + }, + "pinned": { + "type": "boolean" + } + } + } + ], + "required": ["muted", "pinned"] + }, + "BoardMetadata": { + "allOf": [ + { + "$ref": "#/components/schemas/BoardSummary" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "slug": { + "type": "string" + }, + "descriptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Description" + } + } + } + } + ] + }, + "LoggedInBoardMetadata": { + "allOf": [ + { + "$ref": "#/components/schemas/LoggedInBoardSummary" + }, + { + "$ref": "#/components/schemas/BoardMetadata" + }, + { + "type": "object", + "properties": { + "accessories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Accessory" + } + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "posting_identities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PostingIdentity" + } + } + } + } + ] + }, + "Comment": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "parent_post_id": { + "type": "string", + "format": "uuid" + }, + "parent_comment_id": { + "type": "string", + "format": "uuid" + }, + "chain_parent_id": { + "type": "string", + "format": "uuid" + }, + "content": { + "type": "string", + "format": "quill-delta" + }, + "secret_identity": { + "description": "The public-facing identity associated with the comment.", + "$ref": "#/components/schemas/SecretIdentity" + }, + "user_identity": { + "description": "The identity of the original poster, if visible to the requester.", + "$ref": "#/components/schemas/Identity" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "own": { + "type": "boolean" + }, + "new": { + "type": "boolean" + }, + "friend": { + "type": "boolean" + } + }, + "required": [ + "id", + "parent_post_id", + "content", + "created_at", + "secret_identity", + "new", + "own", + "friend" + ] + }, + "Contribution": { + "description": "A contribution to a thread.", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "parent_thread_id": { + "type": "string", + "format": "uuid" + }, + "parent_post_id": { + "type": "string", + "format": "uuid" + }, + "parent_board_slug": { + "type": "string", + "format": "string" + }, + "content": { + "type": "string", + "format": "quill-delta" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "secret_identity": { + "description": "The public-facing identity associated with the contribution.", + "$ref": "#/components/schemas/SecretIdentity" + }, + "user_identity": { + "description": "The identity of the original poster, if visible to the requester.", + "$ref": "#/components/schemas/Identity" + }, + "new": { + "type": "boolean" + }, + "own": { + "type": "boolean" + }, + "friend": { + "type": "boolean" + }, + "total_comments_amount": { + "type": "number" + }, + "new_comments_amount": { + "type": "number" + }, + "comments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Comment" + } + }, + "tags": { + "type": "object", + "$ref": "#/components/schemas/Tags" + }, + "type": { + "type": "string", + "enum": ["text"], + "deprecated": true + }, + "anonymity_type": { + "type": "string", + "enum": ["everyone", "friends_only"], + "deprecated": true + } + }, + "required": [ + "id", + "parent_post_id", + "parent_thread_id", + "parent_board_slug", + "content", + "created_at", + "secret_identity", + "friend", + "own", + "new", + "total_comments_amount", + "new_comments_amount", + "comments", + "tags" + ] + }, + "Cursor": { + "type": "object", + "properties": { + "next": { + "description": "Pagination link pointing to the next page.", + "type": "string", + "format": "uri" + } + }, + "required": ["next"] + }, + "Description": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextDescription" + }, + { + "$ref": "#/components/schemas/CategoryFilterDescription" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "text": "#/components/schemas/TextDescription", + "category_filter": "#/components/schemas/CategoryFilterDescription" + } + } + }, + "BaseDescription": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "index": { + "type": "number" + }, + "title": { + "type": "string" + } + }, + "required": ["id", "index", "title"] + }, + "TextDescription": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseDescription" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "description": { + "type": "string" + } + }, + "required": ["type", "description"] + } + ] + }, + "CategoryFilterDescription": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseDescription" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["category_filter"] + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["type", "categories"] + } + ] + }, + "Feed": { + "type": "object", + "properties": { + "cursor": { + "$ref": "#/components/schemas/Cursor" + }, + "activity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ThreadSummary" + } + } + } + }, + "Identity": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "avatar": { + "type": "string", + "format": "url" + } + }, + "required": ["name", "avatar"] + }, + "SecretIdentity": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "avatar": { + "type": "string", + "format": "url" + }, + "color": { + "type": "string" + }, + "accessory": { + "type": "string", + "format": "url" + } + }, + "required": ["name", "avatar"] + }, + "Accessory": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "accessory": { + "type": "string", + "format": "uri" + } + }, + "required": ["id", "name", "accessory"] + }, + "PostingIdentity": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "color": { + "type": "string", + "format": "color" + }, + "accessory": { + "type": "string", + "format": "uri" + } + }, + "required": ["id", "name", "avatar"] + }, + "BoardNotifications": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "has_updates": { + "description": "Whether the board has a notification.", + "type": "boolean" + }, + "is_outdated": { + "description": "Whether the board's notifications are older than the user's last visit.", + "type": "boolean" + }, + "last_activity_at": { + "description": "When the board was last updated.", + "type": "date-time", + "nullable": true + }, + "last_activity_from_others_at": { + "description": "When the board was last updated by someone other than the current user.", + "type": "date-time", + "nullable": true + }, + "last_visited at": { + "description": "When the board was last visited by the current user.", + "type": "date-time", + "nullable": true + } + }, + "required": [ + "id", + "realm_id", + "slug", + "avatar_url", + "accent_color", + "logged_in_only", + "delisted" + ] + }, + "Permissions": { + "type": "object", + "properties": { + "board_permissions": { + "$ref": "#/components/schemas/BoardPermissions" + }, + "post_permissions": { + "$ref": "#/components/schemas/PostPermissions" + }, + "thread_permissions": { + "$ref": "#/components/schemas/ThreadPermission" + } + }, + "required": [ + "board_permissions", + "post_permissions", + "thread_permissions" + ] + }, + "BoardPermissions": { + "type": "array", + "items": { + "type": "string", + "enum": ["edit_metadata"] + } + }, + "PostPermissions": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "edit_content", + "edit_whisper_tags", + "edit_category_tags", + "edit_index_tags", + "edit_content_notices" + ] + } + }, + "ThreadPermission": { + "type": "array", + "items": { + "type": "string", + "enum": ["move_thread"] + } + }, + "RealmSettings": { + "type": "object", + "properties": { + "root": { + "type": "obect", + "properties": { + "cursor": { + "type": "object" + } + } + }, + "index_page": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Setting" + } + }, + "board_page": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Setting" + } + }, + "thread_page": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Setting" + } + } + }, + "required": ["root", "index_page", "board_page", "thread_page"] + }, + "Realm": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "slug": { + "type": "string" + }, + "settings": { + "$ref": "#/components/schemas/RealmSettings" + }, + "boards": { + "$ref": "#/components/schemas/BoardSummary" + } + }, + "required": ["id", "slug", "settings"] + }, + "RealmActivity": { + "type": "object", + "properties": { + "id": { + "description": "The Realm id.", + "type": "string", + "format": "uuid" + }, + "boards": { + "description": "The activity summary for each board in the realm. |\nKeys are the uuid of each board.\n", + "type": "object", + "additionalProperties": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + } + } + }, + { + "$ref": "#/components/schemas/BoardActivitySummary" + } + ] + } + } + } + }, + "Setting": { + "type": "object", + "properties": { + "todo": { + "type": "string" + } + } + }, + "Tags": { + "description": "Types of tags associated to a contribution.", + "type": "object", + "properties": { + "whisper_tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "index_tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "category_tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "content_warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ThreadActivitySummary": { + "type": "object", + "properties": { + "new_posts_amount": { + "type": "number" + }, + "new_comments_amount": { + "type": "number" + }, + "total_comments_amount": { + "type": "number" + }, + "total_posts_amount": { + "type": "number" + }, + "direct_threads_amount": { + "type": "number" + }, + "last_activity_at": { + "type": "number" + } + }, + "required": [ + "new_posts_amount", + "new_comments_amount", + "total_comments_amount", + "total_posts_amount", + "direct_threads_amount", + "last_activity" + ] + }, + "ThreadSummary": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "parent_board_slug": { + "type": "string" + }, + "starter": { + "description": "The contribution that starts the thread.", + "$ref": "#/components/schemas/Contribution" + }, + "default_view": { + "type": "string", + "enum": ["thread", "gallery", "timeline"] + }, + "muted": { + "description": "Whether the contribution is muted. False when the user is logged out.", + "type": "boolean" + }, + "hidden": { + "description": "Whether the contribution is hidden. False when the user is logged out.", + "type": "boolean" + } + }, + "required": [ + "id", + "parent_board_slug", + "starter", + "default_view", + "muted", + "hidden" + ] + }, + { + "$ref": "#/components/schemas/ThreadActivitySummary" + } + ] + }, + "Thread": { + "allOf": [ + { + "type": "object", + "properties": { + "posts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contribution" + } + }, + "comments": { + "description": "A map from post_id to its comments.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Comment" + } + } + } + }, + "required": ["posts", "comments"] + }, + { + "$ref": "#/components/schemas/ThreadSummary" + } + ] + } + }, + "securitySchemes": { + "firebase": { + "description": "Default bobaserver authentication, powered by firebase.", + "type": "http", + "scheme": "bearer", + "in": "header", + "bearerFormat": "JWT", + "x-google-issuer": "https://securetoken.google.com/bobaboard-fb", + "x-google-jwks_uri": "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com", + "x-google-audiences": "bobaboard-fb" + } + }, + "params": { + "Identity": { + "type": "object", + "properties": { + "accessory_id": { + "description": "The accessory to associate with the attached entity.", + "nullable": true, + "type": "string", + "format": "uuid" + }, + "identity_id": { + "description": "The identity to associate with the attached entity, if fixed.", + "nullable": true, + "type": "string", + "format": "uuid" + } + } + } + }, + "examples": { + "BoardsParam": [ + { + "existing": { + "summary": "An existing board", + "value": "gore" + } + }, + { + "locked": { + "summary": "A board for logged in users only", + "value": "restricted" + } + }, + { + "not-found": { + "summary": "A board that does not exists", + "value": "this_does_not_exist" + } + } + ], + "BoardsGore": { + "summary": "An existing board", + "value": { + "id": "396be545-e2d4-4497-a5b5-700e89ab99c0", + "realm_id": "f377afb3-5c62-40cc-8f07-1f4749a780eb", + "slug": "gore", + "tagline": "Blood! Blood! Blood!", + "avatar_url": "/gore.png", + "accent_color": "#f96680", + "delisted": false, + "logged_in_only": false, + "descriptions": [] + } + } + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/car-schema-no-addon.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/car-schema-no-addon.json new file mode 100644 index 000000000..bfa7ea83c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/car-schema-no-addon.json @@ -0,0 +1,22 @@ +{ + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name of the car", + "type": "string" + }, + "year": { + "description": "Build year", + "type": "integer" + }, + "model": { + "description": "Model of the car", + "type": "string", + "nullable": true + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/car-schema.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/car-schema.json new file mode 100644 index 000000000..a52f50930 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/car-schema.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name of the car", + "type": "string" + }, + "year": { + "description": "Build year", + "type": "integer" + }, + "model": { + "description": "Model of the car", + "type": "string", + "nullable": true + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/cars-schema.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/cars-schema.json new file mode 100644 index 000000000..e3ed8482a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/cars-schema.json @@ -0,0 +1,29 @@ +{ + "type": "array", + "items": { + "$ref": "#/components/schemas/Car" + }, + "components": { + "schemas": { + "Car": { + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name of the car", + "type": "string" + }, + "year": { + "description": "Build year", + "type": "integer" + }, + "model": { + "description": "Model of the car", + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/nullable-fields-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/nullable-fields-openapi.yaml new file mode 100644 index 000000000..a45c6965f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/nullable-fields-openapi.yaml @@ -0,0 +1,67 @@ +openapi: 3.0.3 +info: + title: Sample API + version: '1.0' +paths: + /queen/members: + get: + parameters: + - $ref: "#/components/parameters/alive" + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/human" + examples: + "Alive": + $ref: '#/components/examples/alive' + "Dead": + $ref: '#/components/examples/dead' + +components: + parameters: + alive: + name: isAlive + in: path + required: true + schema: + type: boolean + examples: + "Alive": + value: true + "Dead": + value: false + schemas: + human: + type: object + properties: + name: + type: string + nullable: false + birthDate: + type: string + format: date-time + nullable: false + deathDate: + type: string + format: date-time + nullable: true + + examples: + alive: + value: + - name: "Brian May" + birthDate: "1947-07-19T00:00:00.0Z" + - name: "Roger Taylor" + birthDate: "1949-07-26T00:00:00.0Z" + - name: "John Deacon" + birthDate: "1951-08-19T00:00:00.0Z" + dead: + value: + - name: "Freddy Mercury" + birthDate: "1946-09-15T00:00:00.0Z" + deathDate: "1946-11-24T00:00:00.0Z" diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/response-refs-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/response-refs-openapi.yaml new file mode 100644 index 000000000..51e0897ff --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/response-refs-openapi.yaml @@ -0,0 +1,115 @@ +openapi: 3.0.1 +info: + title: Sample API + version: '1.0' +paths: + /accounts: + get: + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/accountDetails" + examples: + "Example 0": + value: |- + [ + { "resourceId": "396be545-e2d4-4497-a5b5-700e89ab99c0" }, + { "resourceId": "f377afb3-5c62-40cc-8f07-1f4749a780eb" } + ] + application/json; charset=utf-8: + schema: + type: array + items: + $ref: "#/components/schemas/accountDetails" + examples: + "Example 0": + value: |- + [ + { "resourceId": "396be545-e2d4-4497-a5b5-700e89ab99c0" }, + { "resourceId": "f377afb3-5c62-40cc-8f07-1f4749a780eb" } + ] + /accounts/{accountId}: + get: + parameters: + - $ref: "#/components/parameters/accountId" + responses: + 200: + $ref: "#/components/responses/OK_200_AccountDetails" + +components: + parameters: + accountId: + name: accountId + in: path + required: true + schema: + $ref: "#/components/schemas/accountId" + examples: + "Example 1": + value: 396be545-e2d4-4497-a5b5-700e89ab99c0 + + responses: + OK_200_AccountDetails: + description: OK + content: + application/json: + schema: + type: object + required: + - account + properties: + account: + $ref: "#/components/schemas/accountDetails" + examples: + "Example 1": + $ref: "#/components/examples/accountDetailsRegularAccount" + application/hal+json: + schema: + type: object + required: + - account + properties: + account: + $ref: "#/components/schemas/accountDetails" + examples: + "Example 1": + $ref: "#/components/examples/accountDetailsRegularAccount" + application/hal+json;charset=UTF-8: + schema: + type: object + required: + - account + properties: + account: + $ref: "#/components/schemas/accountDetails" + examples: + "Example 1": + $ref: "#/components/examples/accountDetailsRegularAccount" + + schemas: + accountId: + type: string + format: uuid + accountDetails: + type: object + additionalProperties: false + required: + - resourceId + properties: + resourceId: + type: string + + examples: + accountDetailsRegularAccount: + value: + { + "account": + { + "resourceId": "396be545-e2d4-4497-a5b5-700e89ab99c0", + } + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/season-nullable-all-of.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/season-nullable-all-of.yaml new file mode 100644 index 000000000..4d22a62c6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/season-nullable-all-of.yaml @@ -0,0 +1,159 @@ +openapi: 3.0.1 +info: + title: Season API + description: useless API + version: v1 +servers: +- url: / +tags: +- name: season-controller +paths: + /seasonAllOf: + post: + tags: + - season-controller + operationId: createSeason + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreatableSeason' + examples: + bumpinThat: + description: a Season + value: + seasonType: "BratSummer" + required: true + responses: + "201": + description: season data + content: + application/json: + schema: + $ref: '#/components/schemas/SeasonAllOf' + examples: + bumpinThat: + value: '{"seasonData":null}' + /seasonOneOf: + post: + tags: + - season-controller + operationId: createSeason + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreatableSeason' + examples: + bumpinThat: + description: a Season + value: + seasonType: "BratSummer" + required: true + responses: + "201": + description: season data + content: + application/json: + schema: + $ref: '#/components/schemas/SeasonOneOf' + examples: + bumpinThat: + value: '{"seasonData":null}' + /seasonAnyOf: + post: + tags: + - season-controller + operationId: createSeason + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreatableSeason' + examples: + bumpinThat: + description: a Season + value: + seasonType: "BratSummer" + required: true + responses: + "201": + description: season data + content: + application/json: + schema: + $ref: '#/components/schemas/SeasonAnyOf' + examples: + bumpinThat: + value: '{"seasonData":null}' +components: + schemas: + SeasonType: + type: string + enum: + - BratSummer + - SpookySeason + - RustySpring + - ColdHeartWinter + + CreatableSeason: + title: CreatableSeason + type: object + properties: + seasonType: + $ref: '#/components/schemas/SeasonType' + + SeasonData: + nullable: true + title: SeasonData + required: + - isTooHot + - isTooCold + type: object + properties: + seasonType: + $ref: '#/components/schemas/SeasonType' + isTooHot: + type: boolean + isTooCold: + type: boolean + + SeasoningData: + nullable: true + title: SeasoningData + required: + - isSalty + - isUmami + type: object + properties: + isSalty: + type: boolean + isUmami: + type: boolean + + SeasonAllOf: + type: object + properties: + seasonData: + nullable: true + allOf: + - $ref: '#/components/schemas/SeasonData' + + SeasonOneOf: + type: object + properties: + seasonData: + nullable: true + oneOf: + - $ref: '#/components/schemas/SeasonData' + - $ref: '#/components/schemas/SeasoningData' + + SeasonAnyOf: + type: object + properties: + seasonData: + nullable: true + anyOf: + - $ref: '#/components/schemas/SeasonData' + - $ref: '#/components/schemas/SeasoningData' + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref.yaml new file mode 100644 index 000000000..fb98dba68 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref.yaml @@ -0,0 +1,83 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: 'https://raw.githubusercontent.com/microcks/microcks/1.5.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.json' + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + value: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + value: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-local-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-local-ref.yaml new file mode 100644 index 000000000..a007eb0dc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-local-ref.yaml @@ -0,0 +1,105 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: '#/components/schemas/Forecast' + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + value: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + value: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." +components: + schemas: + Forecast: + title: Root Type for Forecast + description: A weather forecast for a requested region + type: object + properties: + region: + type: string + temp: + format: double + type: number + weather: + type: string + visibility: + format: int32 + type: integer + example: + region: west + temp: 25.2 + weather: cloudy + visibility: 1000 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml new file mode 100644 index 000000000..4a97ac566 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml @@ -0,0 +1,112 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast: + get: + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: 'weather-forecast-schema.json' + examples: + all: + value: + - region: north + temp: -1.5 + weather: snowy + visibility: 25 + - region: west + temp: 12.2 + weather: rainy + visibility: 300 + - region: east + temp: -6.6 + weather: frosty + visibility: 523 + - region: south + temp: 28.3 + weather: sunny + visibility: 1500 + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: './weather-forecast-schema.json' + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + value: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + value: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.json new file mode 100644 index 000000000..c28f58f0f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.json @@ -0,0 +1,27 @@ +{ + "title": "Root Type for Forecast", + "description": "A weather forecast for a requested region", + "type": "object", + "properties": { + "region": { + "type": "string" + }, + "temp": { + "format": "double", + "type": "number" + }, + "weather": { + "type": "string" + }, + "visibility": { + "format": "int32", + "type": "integer" + } + }, + "example": { + "region": "west", + "temp": 25.2, + "weather": "cloudy", + "visibility": 1000 + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml new file mode 100644 index 000000000..dabd98c85 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml @@ -0,0 +1,19 @@ +title: Root Type for Forecast +description: A weather forecast for a requested region +type: object +properties: + region: + type: string + temp: + format: double + type: number + weather: + type: string + visibility: + format: int32 + type: integer +example: + region: west + temp: 25.2 + weather: cloudy + visibility: 1000 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecasts.json b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecasts.json new file mode 100644 index 000000000..79137e550 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/openapi/weather-forecasts.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "weather-forecast-schema.json" + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/soap/HelloService.wsdl b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/soap/HelloService.wsdl new file mode 100644 index 000000000..d3b2e759e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/soap/HelloService.wsdl @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user-signedup-bad.avsc b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user-signedup-bad.avsc new file mode 100644 index 000000000..2b3ec0540 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user-signedup-bad.avsc @@ -0,0 +1,9 @@ +{"namespace": "microcks.avro", + "type": "record", + "name": "User", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "email", "type": "string"}, + {"name": "age", "type": "int"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user-signedup.avsc b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user-signedup.avsc new file mode 100644 index 000000000..09b4d4cb6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user-signedup.avsc @@ -0,0 +1,9 @@ +{"namespace": "microcks.avro", + "type": "record", + "name": "User", + "fields": [ + {"name": "fullName", "type": "string"}, + {"name": "email", "type": "string"}, + {"name": "age", "type": "int"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user.avsc b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user.avsc new file mode 100644 index 000000000..2b3ec0540 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/user.avsc @@ -0,0 +1,9 @@ +{"namespace": "microcks.avro", + "type": "record", + "name": "User", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "email", "type": "string"}, + {"name": "age", "type": "int"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/valid-schema.xsd b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/valid-schema.xsd new file mode 100644 index 000000000..e8c589ca2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/commons/util/src/test/resources/io/github/microcks/util/valid-schema.xsd @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/dev/.gitignore b/jdk_21_maven/cs/rest-gui/microcks/dev/.gitignore new file mode 100644 index 000000000..d0bf0806d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/dev/.gitignore @@ -0,0 +1 @@ +microcks-data diff --git a/jdk_21_maven/cs/rest-gui/microcks/dev/start-keycloak-docker.sh b/jdk_21_maven/cs/rest-gui/microcks/dev/start-keycloak-docker.sh new file mode 100644 index 000000000..7e050a672 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/dev/start-keycloak-docker.sh @@ -0,0 +1,8 @@ +#docker run -it -v $(pwd)/../install/docker-compose/keycloak-realm:/opt/keycloak/data/import -p 8180:8080 \ +# -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ +# -e KC_HOSTNAME_URL=http://localhost:8180 -e KC_HOSTNAME_ADMIN_URL=http://localhost:8180 \ +# quay.io/keycloak/keycloak:24.0.4 start-dev --import-realm + +docker run -it -v $(pwd)/../install/docker-compose/keycloak-realm:/opt/keycloak/data/import -p 8180:8080 \ + -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ + quay.io/keycloak/keycloak:26.0.0 start-dev --hostname http://localhost:8180 --import-realm diff --git a/jdk_21_maven/cs/rest-gui/microcks/dev/start-keycloak-podman.sh b/jdk_21_maven/cs/rest-gui/microcks/dev/start-keycloak-podman.sh new file mode 100644 index 000000000..682b82d8a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/dev/start-keycloak-podman.sh @@ -0,0 +1,4 @@ +podman run -it -v $(pwd)/../install/docker-compose/keycloak-realm:/opt/keycloak/data/import:Z -p 8180:8080 \ + -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ + -e KC_HOSTNAME_URL=http://localhost:8180 -e KC_HOSTNAME_ADMIN_URL=http://localhost:8180 \ + quay.io/keycloak/keycloak:24.0.4 start-dev --import-realm diff --git a/jdk_21_maven/cs/rest-gui/microcks/dev/start-mongodb-docker.sh b/jdk_21_maven/cs/rest-gui/microcks/dev/start-mongodb-docker.sh new file mode 100644 index 000000000..80e12c0bd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/dev/start-mongodb-docker.sh @@ -0,0 +1 @@ +docker run -it -v $(pwd)/microcks-data:/data/db -p 27017:27017 mongo:4.4.29 diff --git a/jdk_21_maven/cs/rest-gui/microcks/dev/start-mongodb-podman.sh b/jdk_21_maven/cs/rest-gui/microcks/dev/start-mongodb-podman.sh new file mode 100644 index 000000000..8541a5ec2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/dev/start-mongodb-podman.sh @@ -0,0 +1 @@ +podman run -it -v $(pwd)/microcks-data:/data/db:Z -p 27017:27017 mongo:4.4.29 diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/distro/pom.xml new file mode 100644 index 000000000..2b941b637 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + microcks + io.github.microcks + 1.12.2-SNAPSHOT + + + Microcks Distros + microcks-distro + pom + + + .. + + + + uber + uber-async-minion + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/pom.xml new file mode 100644 index 000000000..b88d3aa75 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + + io.github.microcks + microcks-distro + 1.12.2-SNAPSHOT + + + Microcks Uber Async Minion + microcks-uber-async-minion + + + UTF-8 + ../.. + + 3.11.0 + 21 + + quarkus-universe-bom + io.quarkus + 3.15.4 + 3.15.4 + + 3.2.5 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.github.microcks + microcks-async-minion + ${project.version} + + + io.quarkiverse.reactivemessaging.http + quarkus-reactive-messaging-http + 2.0.2 + + + io.quarkus + quarkus-container-image-docker + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus-plugin.version} + + + + build + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + + + + + + + + + native + + + native + + + + false + true + + + + + org.brotli + dec + 0.1.2 + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.jvm b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.jvm new file mode 100644 index 000000000..76a09457b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.jvm @@ -0,0 +1,59 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the docker image run: +# +# mvn package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t microcks/microcks-async-minion-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 microcks/microcks-async-minion-jvm +# +### +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5-1742914212 + +# Some version information +LABEL maintainer="Laurent Broudoux " \ + org.opencontainers.image.authors="Laurent Broudoux " \ + org.opencontainers.image.title="Microcks Async Minion Uber" \ + org.opencontainers.image.description="Microcks is Open Source cloud-native native tool for API Mocking and Testing" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.documentation="https://github.com/microcks/microcks/tree/master/distro/uber-async-minion" \ + io.artifacthub.package.readme-url="https://raw.githubusercontent.com/microcks/microcks/master/README.md" + +ARG JAVA_PACKAGE=java-21-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl-minimal ca-certificates ${JAVA_PACKAGE} -y \ + && microdnf update -y \ + && microdnf clean all \ + && rm /var/lib/rpm/rpmdb.sqlite \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 550 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +COPY target/quarkus-app/lib/ /deployments/lib/ +COPY target/quarkus-app/*.jar /deployments/ +COPY target/quarkus-app/app/ /deployments/app/ +COPY target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.legacy-jar b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 000000000..17a35387a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,51 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the docker image run: +# +# mvn package -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t microcks/microcks-async-minion-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 microcks/microcks-async-minion-legacy-jar +# +### +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5-1742914212 + +ARG JAVA_PACKAGE=java-21-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl-minimal ca-certificates ${JAVA_PACKAGE} -y \ + && microdnf update -y \ + && microdnf clean all \ + && rm /var/lib/rpm/rpmdb.sqlite \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 550 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +ENV JAVA_APP_DIR="/deployments" +ENV JAVA_MAIN_CLASS="io.quarkus.runner.GeneratedMain" + +COPY target/lib/ /deployments/lib/ +COPY target/*-runner.jar /deployments/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.native b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.native new file mode 100644 index 000000000..a05b32e2a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/docker/Dockerfile.native @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode +# +# Before building the docker image run: +# +# mvn package -Pnative -Dquarkus.native.container-build=true +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t microcks/microcks-async-minion . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 microcks/microcks-async-minion +# +### +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5-1742914212 +WORKDIR /work/ +COPY --chown=1001:root target/*-runner /work/application + +# set up permissions for user `1001` +RUN chmod 775 /work /work/application \ + && chown -R 1001 /work \ + && chmod -R "g+rwX" /work \ + && chown -R 1001:root /work + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/java/io/github/microcks/minions/async/client/MicrocksWebSocketConnector.java b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/java/io/github/microcks/minions/async/client/MicrocksWebSocketConnector.java new file mode 100644 index 000000000..81c20b4c1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/java/io/github/microcks/minions/async/client/MicrocksWebSocketConnector.java @@ -0,0 +1,113 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minions.async.client; + +import io.github.microcks.event.ServiceViewChangeEvent; +import io.github.microcks.minion.async.AsyncMockDefinitionUpdater; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.arc.Unremovable; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * This is a connector to Microcks app WebSocket endpoints. It listens to /api/services-updates endpoint for incoming + * {@code ServiceViewChangeEvent} and propagate them to the {@code AsyncMockDefinitionUpdater}. + * @author laurent + */ +@Unremovable +@ApplicationScoped +public class MicrocksWebSocketConnector { + + /** Get a JBoss logging logger. */ + private static final Logger logger = Logger.getLogger(MicrocksWebSocketConnector.class); + + @ConfigProperty(name = "minion.microcks-host-port") + String microcksHostAndPort; + + /** Application startup method. */ + void onStart(@Observes StartupEvent event) throws URISyntaxException, DeploymentException, IOException { + URI uri = null; + + try { + uri = new URI("ws://" + microcksHostAndPort + "/api/services-updates"); + ContainerProvider.getWebSocketContainer().connectToServer(WebSocketClient.class, uri); + logger.debug("Now connected to Microcks /api/services-updates WS endpoint"); + } catch (URISyntaxException e) { + logger.error("Exception while building the Microcks WebSocket URI, check your settings", e); + throw e; + } catch (DeploymentException e) { + logger.error("Caught a WebSocket DeploymentException", e); + throw e; + } catch (IOException e) { + logger.info("Caught an IOException while connecting Microcks WebSocket endpoint", e); + throw e; + } + } + + @ClientEndpoint + public static class WebSocketClient { + + private final AsyncMockDefinitionUpdater definitionUpdater; + private final ObjectMapper mapper; + + /** + * Create a WebSocketClient with mandatory dependencies. + * @param definitionUpdater to update Async mocks definition when needed + * @param mapper to deserialize ServiceViewChangeEvents + */ + public WebSocketClient(AsyncMockDefinitionUpdater definitionUpdater, ObjectMapper mapper) { + this.definitionUpdater = definitionUpdater; + this.mapper = mapper; + } + + @OnOpen + public void open(Session session) { + logger.debug("Opening a WebSocket Session on Microcks server"); + // Send a message to indicate that we are ready, + // as the message handler may not be registered immediately after this callback. + session.getAsyncRemote().sendText("_ready_"); + } + + @OnMessage + public void message(String message) { + logger.debugf("Received this WebSocket message: " + message); + try { + ServiceViewChangeEvent serviceViewChangeEvent = mapper.readValue(message, ServiceViewChangeEvent.class); + logger.infof("Received a new change event [%s] for '%s', at %d", serviceViewChangeEvent.getChangeType(), + serviceViewChangeEvent.getServiceId(), serviceViewChangeEvent.getTimestamp()); + + definitionUpdater.applyServiceChangeEvent(serviceViewChangeEvent); + } catch (Exception e) { + logger.error("WebSocket message cannot be converted into a ServiceViewChangeEvent", e); + logger.error("Ignoring this WebSocket message"); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/java/io/github/microcks/minions/async/config/RuntimeReflectionRegistrationFeature.java b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/java/io/github/microcks/minions/async/config/RuntimeReflectionRegistrationFeature.java new file mode 100644 index 000000000..6112166bd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/java/io/github/microcks/minions/async/config/RuntimeReflectionRegistrationFeature.java @@ -0,0 +1,85 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.minions.async.config; + +import io.github.microcks.domain.Binding; +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Message; +import io.github.microcks.domain.Metadata; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.ServiceView; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.event.ServiceViewChangeEvent; +import io.github.microcks.minion.async.client.ServiceViewChangeEventDeserializer; + +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeReflection; +import org.jboss.logging.Logger; + +/** + * A feature for providing runtime reflection registration hints to Graal VM. + * @author laurent + */ +public class RuntimeReflectionRegistrationFeature implements Feature { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + registerClassForReflection(ServiceViewChangeEvent.class); + registerClassForReflection(ServiceViewChangeEventDeserializer.class); + + registerClassForReflection(ServiceView.class); + registerClassForReflection(Metadata.class); + registerClassForReflection(Operation.class); + registerClassForReflection(Binding.class); + registerClassForReflection(ParameterConstraint.class); + registerClassForReflection(Message.class); + registerClassForReflection(Header.class); + registerClassForReflection(Request.class); + registerClassForReflection(Response.class); + registerClassForReflection(Parameter.class); + registerClassForReflection(EventMessage.class); + registerClassForReflection(EventMessage.class); + registerClassForReflection(Exchange.class); + registerClassForReflection(RequestResponsePair.class); + registerClassForReflection(UnidirectionalEvent.class); + } + + /** + * Register all class elements (constructors, methods, fields) for reflection in GraalVM. + * @param clazz The class to register. + */ + public void registerClassForReflection(Class clazz) { + logger.debugf("Registering %s for reflection", clazz.getCanonicalName()); + RuntimeReflection.register(clazz); + RuntimeReflection.register(clazz.getDeclaredConstructors()); + RuntimeReflection.register(clazz.getDeclaredMethods()); + RuntimeReflection.register(clazz.getDeclaredFields()); + RuntimeReflection.register(clazz.getConstructors()); + RuntimeReflection.register(clazz.getMethods()); + RuntimeReflection.register(clazz.getFields()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/application.properties b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/application.properties new file mode 100644 index 000000000..99f4d5227 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/application.properties @@ -0,0 +1,51 @@ +# Configure the log level. +quarkus.log.level=INFO +quarkus.log.console.level=INFO + +# Access to Microcks API server. +io.github.microcks.minion.async.client.MicrocksAPIConnector/mp-rest/url=http://${MICROCKS_HOST_PORT:localhost:8080} + +# Only supports WebSocket by default, but you can extend it. +minion.supported-bindings=WS${ASYNC_PROTOCOLS:} +minion.microcks-host-port=${MICROCKS_HOST_PORT:localhost:8080} + +# Override microcks-services-updates reactive messaging channel +mp.messaging.incoming.microcks-services-updates.connector=quarkus-websocket +mp.messaging.incoming.microcks-services-updates.url=ws://${MICROCKS_HOST_PORT:localhost:8080}/ +mp.messaging.incoming.microcks-services-updates.path=/api/services-updates + +# Access to Kafka broker +kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVER:localhost:9092} + +# Access to MQTT broker. +mqtt.server=${MQTT_SERVER:localhost:1883} +mqtt.username=${MQTT_USERNAME:microcks} +mqtt.password=${MQTT_PASSWORD:microcks} + +# Access to RabbitMQ broker. +amqp.server=${AMQP_SERVER:localhost:5672} +amqp.username=${AMQP_USERNAME:microcks} +amqp.password=${AMQP_PASSWORD:microcks} + +# Access to Amazon SQS +amazonsqs.region=${AWS_SQS_REGION:eu-west-3} +amazonsqs.credentials-type=env-variable +amazonsqs.endpoint-override=${AWS_SQS_ENDPOINT} + +# Access to Amazon SNS +amazonsns.region=${AWS_SNS_REGION:eu-west-3} +amazonsns.credentials-type=env-variable +amazonsns.endpoint-override=${AWS_SNS_ENDPOINT} + + +# Configuration for native image build +quarkus.native.additional-build-args=\ + -H:IncludeLocales=en,\ + -H:IncludeResourceBundles=org.eclipse.paho.client.mqttv3.internal.nls.logcat\\,org.eclipse.paho.client.mqttv3.internal.nls.messages\\,org.eclipse.paho.client.mqttv3.logging.jsr47min,\ + -H:ReflectionConfigurationFiles=reflect-config.json,\ + -H:ResourceConfigurationFiles=resource-config.json,\ + --trace-object-instantiation=java.lang.Thread\\,java.util.Random\\,java.security.SecureRandom,\ + --initialize-at-run-time=io.github.microcks.util.el.function.AbstractRandomELFunction\\,io.nats.client.NUID\\,io.nats.client.support.SSLUtils\\,io.nats.client.support.RandomUtils\\,net.datafaker.service.RandomService\\,org.apache.hc.client5.http.impl.auth.NTLMEngineImpl,\ + --features=io.github.microcks.minions.async.config.RuntimeReflectionRegistrationFeature + +quarkus.native.builder-image.pull=always diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/reflect-config.json b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/reflect-config.json new file mode 100644 index 000000000..d7e7a7cc0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/reflect-config.json @@ -0,0 +1,74 @@ +[ + { + "name": "org.eclipse.paho.client.mqttv3.logging.JSR47Logger", + "condition": { + "typeReachable": "org.eclipse.paho.client.mqttv3.logging.LoggerFactory" + }, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.eclipse.paho.client.mqttv3.internal.TCPNetworkModuleFactory", + "queryAllDeclaredConstructors" : true, + "queryAllPublicConstructors" : true, + "queryAllDeclaredMethods" : true, + "queryAllPublicMethods" : true, + "allDeclaredClasses" : true, + "allPublicClasses" : true, + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true, + "allDeclaredFields" : true, + "allPublicFields" : true + }, + { + "name": "org.eclipse.paho.client.mqttv3.internal.TCPNetworkModule", + "queryAllDeclaredConstructors" : true, + "queryAllPublicConstructors" : true, + "queryAllDeclaredMethods" : true, + "queryAllPublicMethods" : true, + "allDeclaredClasses" : true, + "allPublicClasses" : true, + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true, + "allDeclaredFields" : true, + "allPublicFields" : true + }, + { + "name": "org.eclipse.paho.client.mqttv3.internal.SSLNetworkModuleFactory", + "queryAllDeclaredConstructors" : true, + "queryAllPublicConstructors" : true, + "queryAllDeclaredMethods" : true, + "queryAllPublicMethods" : true, + "allDeclaredClasses" : true, + "allPublicClasses" : true, + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true, + "allDeclaredFields" : true, + "allPublicFields" : true + }, + { + "name": "org.eclipse.paho.client.mqttv3.internal.SSLNetworkModule", + "queryAllDeclaredConstructors" : true, + "queryAllPublicConstructors" : true, + "queryAllDeclaredMethods" : true, + "queryAllPublicMethods" : true, + "allDeclaredClasses" : true, + "allPublicClasses" : true, + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true, + "allDeclaredFields" : true, + "allPublicFields" : true + } +] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/resource-config.json b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/resource-config.json new file mode 100644 index 000000000..9177ad555 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber-async-minion/src/main/resources/resource-config.json @@ -0,0 +1,19 @@ +{ + "resources": { + "includes": [ + { "pattern": "\\Qproto2/include/google/protobuf/descriptor.proto\\E" }, + { "pattern": "proto3/include/google/protobuf/.*.proto" }, + { "pattern": "\\Qproto3/include/google/protobuf/compiler/plugin.proto\\E" }, + { + "pattern": "\\QMETA-INF/services/org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory\\E", + "condition": { + "typeReachable": "org.eclipse.paho.client.mqttv3.internal.NetworkModuleService" + } + } + ] + }, + "bundles": [ + {"name":"org.eclipse.paho.client.mqttv3.internal.nls.logcat"}, + {"name":"org.eclipse.paho.client.mqttv3.internal.nls.messages"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/README.md b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/README.md new file mode 100644 index 000000000..941450407 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/README.md @@ -0,0 +1,70 @@ +This is an all-in-one distribution of Microcks that disabled Keycloak need and removed dependency to external MongoDB server. + +For that we're using an embedded MongoDB-protocol compatible Java server that is started conditionally when Spring `uber` profile is active. +That MongoDB Java server is able to run in two different modes: +* Ephemeral in-memory persistence that will not survive a JVM restart (that is the default mode), +* File persistence that will survive restarts. + +## In-memory mode + +Just start the server using `mvn spring-boot:run` command: + +```shell +$ mvn spring-boot:run +====== OUTPUT ====== +[INFO] Attaching agents: [] + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v3.1.1) + +11:51:41.518 INFO 10712 --- [ main] i.g.microcks.MicrocksApplication : Starting MicrocksApplication using Java 17.0.6 with PID 10712 (/Users/laurent/Development/repository/io/github/microcks/microcks-app/1.8.0-SNAPSHOT/microcks-app-1.8.0-SNAPSHOT.jar started by laurent in /Users/laurent/Development/github/microcks/distro/uber) +11:51:41.520 DEBUG 10712 --- [ main] i.g.microcks.MicrocksApplication : Running with Spring Boot v3.1.1, Spring v6.0.10 +11:51:41.520 INFO 10712 --- [ main] i.g.microcks.MicrocksApplication : The following 1 profile is active: "uber" +11:51:42.363 INFO 10712 --- [ main] i.g.microcks.config.WebConfiguration : Starting web application configuration, using profiles: [uber] +11:51:42.364 INFO 10712 --- [ main] i.g.microcks.config.WebConfiguration : Web application fully configured +11:51:42.393 INFO 10712 --- [ main] i.g.m.c.EmbeddedMongoConfiguration : Creating a new embedded Mongo Java Server with in-memory persistence +11:51:42.474 INFO 10712 --- [ main] de.bwaldvogel.mongo.MongoServer : started MongoServer(port: 50935, ssl: false) +[...] +11:51:43.177 INFO 10712 --- [ main] i.g.microcks.MicrocksApplication : Started MicrocksApplication in 1.798 seconds (process running for 2.08) +``` + +You can see in above logs that `The following 1 profile is active: "uber"` and that is creates a new `embedded Mongo Java Server with in-memory persistence`` + +## Persistent mode + +Persistent file location is controlled via the `mongodb.storage.file` configuration in `application.properties` file, +but you can just set its value using the `MONGODB_STORAGE_PATH` environment variable. + +Use this one-line command below: + +```shell +$ MONGODB_STORAGE_PATH=target/microcks.mv mvn spring-boot:run +====== OUTPUT ====== +[INFO] Attaching agents: [] + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v3.1.1) + +11:53:32.292 INFO 10974 --- [ main] i.g.microcks.MicrocksApplication : Starting MicrocksApplication using Java 17.0.6 with PID 10974 (/Users/laurent/Development/repository/io/github/microcks/microcks-app/1.8.0-SNAPSHOT/microcks-app-1.8.0-SNAPSHOT.jar started by laurent in /Users/laurent/Development/github/microcks/distro/uber) +11:53:32.294 DEBUG 10974 --- [ main] i.g.microcks.MicrocksApplication : Running with Spring Boot v3.1.1, Spring v6.0.10 +11:53:32.294 INFO 10974 --- [ main] i.g.microcks.MicrocksApplication : The following 1 profile is active: "uber" +11:53:33.151 INFO 10974 --- [ main] i.g.microcks.config.WebConfiguration : Starting web application configuration, using profiles: [uber] +11:53:33.152 INFO 10974 --- [ main] i.g.microcks.config.WebConfiguration : Web application fully configured +11:53:33.172 INFO 10974 --- [ main] i.g.m.c.EmbeddedMongoConfiguration : Creating a new embedded Mongo Java Server with disk persistence at target/microcks.mv +11:53:33.176 INFO 10974 --- [ main] d.b.mongo.backend.h2.H2Backend : opening MVStore in 'target/microcks.mv' +11:53:33.273 INFO 10974 --- [ main] de.bwaldvogel.mongo.MongoServer : started MongoServer(port: 51430, ssl: false) +[...] +11:53:33.946 INFO 10974 --- [ main] i.g.microcks.MicrocksApplication : Started MicrocksApplication in 1.793 seconds (process running for 2.073) +``` + +You can see in above logs that `The following 1 profile is active: "uber"` and that is creates a new `embedded Mongo Java Server with disk persistence at target/microcks.mv`. \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/pom.xml new file mode 100644 index 000000000..29328d92c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/pom.xml @@ -0,0 +1,203 @@ + + + + microcks-distro + io.github.microcks + 1.12.2-SNAPSHOT + + 4.0.0 + + Microcks Uber App + microcks-uber-app + + + UTF-8 + ../.. + 21 + 1.45.0 + + + + + io.github.microcks + microcks-app + ${project.version} + + + org.springframework.boot + spring-boot-starter-websocket + + + de.bwaldvogel + mongo-java-server + ${mongo-java-server.version} + + + de.bwaldvogel + mongo-java-server-h2-backend + ${mongo-java-server.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + io.github.microcks.MicrocksApplication + + uber + + + --spring.profiles.active=uber + + + + + + + + + prod + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + io.github.microcks.MicrocksApplication + + uber + + + --spring.profiles.active=uber + --spring.aot.enabled=true + + exec + + + + + repackage + + + + process-aot + + process-aot + + + uber + + + + + + + + + INFO + + uber + + + + native + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + io.github.microcks.MicrocksApplication + + uber + + + --spring.profiles.active=uber + + + + + + + + + + + + microcks/builder-noble-java-tiny + + + + + + + + + + + + + 21 + + true + --verbose --initialize-at-run-time=io.grpc.netty.shaded.io.netty.bootstrap,io.grpc.netty.shaded.io.netty.buffer,io.grpc.netty.shaded.io.netty.channel,io.grpc.netty.shaded.io.netty.channel.socket.nio,io.grpc.netty.shaded.io.netty.handler,io.grpc.netty.shaded.io.netty.handler.ssl,io.grpc.netty.shaded.io.netty.internal.tcnative,io.grpc.netty.shaded.io.netty.resolver,io.grpc.netty.shaded.io.netty.util.internal,io.grpc.netty.shaded.io.netty.util.internal.logging,io.grpc.netty.shaded.io.netty.util,io.grpc.netty.shaded.io.netty.util.concurrent --trace-class-initialization=ch.qos.logback.classic.PatternLayout,ch.qos.logback.core.subst.Token,ch.qos.logback.core.pattern.parser.Parser,ch.qos.logback.core.model.processor.DefaultProcessor$1,org.springframework.boot.logging.logback.ColorConverter,ch.qos.logback.classic.Level,ch.qos.logback.classic.Logger,ch.qos.logback.core.CoreConstants,ch.qos.logback.core.status.StatusBase,ch.qos.logback.core.model.processor.ChainedModelFilter$1,ch.qos.logback.core.util.StatusPrinter,ch.qos.logback.core.util.Loader,ch.qos.logback.core.status.InfoStatus,ch.qos.logback.classic.model.processor.LogbackClassicDefaultNestedComponentRules,ch.qos.logback.core.util.Duration,ch.qos.logback.core.subst.NodeToStringTransformer$1,org.slf4j.LoggerFactory,ch.qos.logback.core.model.processor.ImplicitModelHandler$1,ch.qos.logback.core.subst.Parser$1 + + + + + + process-aot + + process-aot + + + uber + + + + + + org.graalvm.buildtools + native-maven-plugin + + 0.10.1 + true + + ${project.build.outputDirectory} + + true + + + 23.1 + true + + + + add-reachability-metadata + + add-reachability-metadata + + + + + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/docker/Dockerfile b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/docker/Dockerfile new file mode 100644 index 000000000..a0c549562 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/docker/Dockerfile @@ -0,0 +1,50 @@ +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5-1742914212 + +# Some version information +LABEL maintainer="Laurent Broudoux " \ + org.opencontainers.image.authors="Laurent Broudoux " \ + org.opencontainers.image.title="Microcks Application Uber" \ + org.opencontainers.image.description="Microcks is Open Source cloud-native native tool for API Mocking and Testing" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.documentation="https://github.com/microcks/microcks/tree/master/distro/uber" \ + io.artifacthub.package.readme-url="https://raw.githubusercontent.com/microcks/microcks/master/distro/uber/README.md" + +# Install Java runtime +RUN microdnf install java-21-openjdk-headless openssl curl-minimal ca-certificates -y \ + && microdnf clean all \ + && rm /var/lib/rpm/rpmdb.sqlite \ + && mkdir -p /deployments + +# JAVA_APP_DIR is used by run-java.sh for finding the binaries +ENV JAVA_APP_DIR=/deployments \ + JAVA_MAJOR_VERSION=11 + +# Set working directory at /deployments +WORKDIR /deployments +VOLUME /deployments/config + +# Setup permissions for user '1001'. Necessary to permit running with a randomised UID +# Runtime user will need to be able to self-insert in /etc/passwd +# Also, use /dev/urandom to speed up startups +RUN chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && chmod g+rw /etc/passwd \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/1.3.8/run-java-sh-1.3.8-sh.sh \ + -o /deployments/run-java.sh \ + && mkdir -p /deployments/data \ + && chown 1001 /deployments/run-java.sh \ + && chmod 550 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /usr/lib/jvm/jre/lib/security/java.security + +# Gives uid +USER 1001 + +# Copy corresponding jar file +COPY target/*-exec.jar app.jar +EXPOSE 8080 + +# Run it in uber mode with AOT support +ENV SPRING_PROFILES_ACTIVE=uber +ENV JAVA_OPTIONS="-Dspring.aot.enabled=true" +ENTRYPOINT [ "/deployments/run-java.sh" ] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/EmbeddedMongoConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/EmbeddedMongoConfiguration.java new file mode 100644 index 000000000..18a611ff0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/EmbeddedMongoConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import de.bwaldvogel.mongo.MongoServer; +import de.bwaldvogel.mongo.backend.h2.H2Backend; +import de.bwaldvogel.mongo.backend.memory.MemoryBackend; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.beans.factory.annotation.Value; + +import java.net.InetSocketAddress; +import java.util.Optional; + +/** + * A configuration for starting an embedded MongoDB server, thanks to mongo-java-server. Only activated when using the + * "uber" Spring profile. + * @author laurent + */ +@Configuration +@Profile("uber") +public class EmbeddedMongoConfiguration { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(EmbeddedMongoConfiguration.class); + + @Value("${mongodb.storage.file:#{null}}") + private Optional storageFileName; + + private MongoClient client; + private MongoServer server; + + private InetSocketAddress serverAddress; + + @Bean(destroyMethod = "shutdown") + public MongoServer mongoServer() { + if (storageFileName.isEmpty()) { + log.info("Creating a new embedded Mongo Java Server with in-memory persistence"); + server = new MongoServer(new MemoryBackend()); + } else { + log.info("Creating a new embedded Mongo Java Server with disk persistence at {}", storageFileName.get()); + server = new MongoServer(new H2Backend(storageFileName.get())); + } + + serverAddress = server.bind(); + return server; + } + + @Bean(destroyMethod = "close") + public MongoClient mongoClient() { + if (server == null) { + mongoServer(); + } + if (client == null) { + client = MongoClients.create("mongodb://localhost:" + serverAddress.getPort()); + } + return client; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/NativeConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/NativeConfiguration.java new file mode 100644 index 000000000..d498b155e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/NativeConfiguration.java @@ -0,0 +1,109 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Message; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceView; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.event.ServiceViewChangeEvent; +import io.github.microcks.event.ServiceViewChangeEventSerializer; +import io.github.microcks.util.dispatcher.DispatchCases; +import io.github.microcks.util.dispatcher.FallbackSpecification; +import io.github.microcks.util.dispatcher.JsonEvaluationSpecification; +import io.github.microcks.util.dispatcher.ProxyFallbackSpecification; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.context.annotation.Profile; + +/** + * A configuration for providing native compilation hints to Graal VM. + * @author laurent + */ +@Configuration +@Profile("uber") +@ImportRuntimeHints(NativeConfiguration.NativeRuntimeHints.class) +public class NativeConfiguration { + + static class NativeRuntimeHints implements RuntimeHintsRegistrar { + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.reflection().registerType(TypeReference.of(Service.class)); + + hints.reflection().registerType(TypeReference.of(ServiceView.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(Message.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(Header.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(Request.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(Response.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(Parameter.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(EventMessage.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(Exchange.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(RequestResponsePair.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(UnidirectionalEvent.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + + hints.reflection().registerType(TypeReference.of(ServiceViewChangeEvent.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(ServiceViewChangeEventSerializer.class), + MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(DispatchCases.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(FallbackSpecification.class), MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(JsonEvaluationSpecification.class), + MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of(ProxyFallbackSpecification.class), + MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + + hints.reflection().registerType( + TypeReference.of("org.springframework.security.web.access.HandlerMappingIntrospectorRequestTransformer"), + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of( + "org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy"), + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + hints.reflection().registerType(TypeReference.of( + "org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$HandlerMappingIntrospectorCachFilterFactoryBean"), + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + + hints.reflection().registerType(TypeReference.of("io.grpc.netty.shaded.io.netty.util.ReferenceCountUtil"), + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/WebSocketConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/WebSocketConfiguration.java new file mode 100644 index 000000000..c4f47e1b9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/config/WebSocketConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.listener.WebSocketServiceChangeEventChannel; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +/** + * A configuration for enabling WebSocket and configuring CORS on our services changes notification endpoint. Only + * activated when using the "uber" Spring profile. + * @author laurent + */ +@Configuration +@EnableWebSocket +@Profile("uber") +public class WebSocketConfiguration implements WebSocketConfigurer { + + private final WebSocketServiceChangeEventChannel channel; + + /** + * Build a new WebSocketConfiguration with mandatory dependencies + * @param channel The channel to register as a handler + */ + public WebSocketConfiguration(WebSocketServiceChangeEventChannel channel) { + this.channel = channel; + } + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(channel, "/api/services-updates").setAllowedOrigins("*"); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/listener/WebSocketServiceChangeEventChannel.java b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/listener/WebSocketServiceChangeEventChannel.java new file mode 100644 index 000000000..8064dfff5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/java/io/github/microcks/listener/WebSocketServiceChangeEventChannel.java @@ -0,0 +1,71 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.listener; + +import io.github.microcks.event.ServiceViewChangeEvent; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This is an implementation of {@code ServiceChangeEventChannel} that uses a WebSocket endpoint as a destination + * recipient for {@code ServiceViewChangeEvent}. + * @author laurent + */ +@Component +@Primary +@Profile("uber") +public class WebSocketServiceChangeEventChannel extends TextWebSocketHandler implements ServiceChangeEventChannel { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(WebSocketServiceChangeEventChannel.class); + + private ObjectMapper mapper = new ObjectMapper(); + + private List sessions = new CopyOnWriteArrayList<>(); + + @Override + public void sendServiceViewChangeEvent(ServiceViewChangeEvent event) throws Exception { + log.debug("Sending ServiceViewChangeEvent to {} connected WS sessions", sessions.size()); + for (WebSocketSession wsSession : sessions) { + wsSession.sendMessage(new TextMessage(mapper.writeValueAsString(event))); + } + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + log.debug("afterConnectionEstablished, session: {}", session.getId()); + sessions.add(session); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + log.debug("afterConnectionClosed, session: {}", session.getId()); + sessions.remove(session); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/META-INF/native-image/reflect-config.json b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 000000000..1f491e200 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,262 @@ +[ + { + "name":"java.util.HashSet", + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, + "queryAllPublicConstructors": true, + "queryAllPublicFields": true, + "allDeclaredClasses": true, + "allDeclaredConstructors": true, + "allPublicClasses": true, + "allRecordComponents": true, + "allNestMembers": true, + "allSigners": true, + "allPermittedSubclasses": true + }, + { + "name":"java.util.TreeMap", + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, + "queryAllPublicConstructors": true, + "queryAllPublicFields": true, + "allDeclaredClasses": true, + "allDeclaredConstructors": true, + "allPublicClasses": true, + "allRecordComponents": true, + "allNestMembers": true, + "allSigners": true, + "allPermittedSubclasses": true + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields" + }, + "name": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields", + "fields": [ + { + "name": "producerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields" + }, + "name": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields", + "fields": [ + { + "name": "consumerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields" + }, + "name": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields", + "fields": [ + { + "name": "producerLimit" + } + ] + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueue" + }, + "name": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueue", + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, + "queryAllPublicConstructors": true, + "queryAllPublicFields": true, + "allDeclaredClasses": true, + "allPublicClasses": true, + "allRecordComponents": true, + "allNestMembers": true, + "allSigners": true, + "allPermittedSubclasses": true + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField" + }, + "name": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField", + "fields": [ + { + "name": "producerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField" + }, + "name": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField", + "fields": [ + { + "name": "consumerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField" + }, + "name": "io.grpc.netty.shaded.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField", + "fields": [ + { + "name": "producerLimit" + } + ] + }, + { + "condition": { + "typeReachable": "io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueue" + }, + "name": "io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueue", + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, + "queryAllPublicConstructors": true, + "queryAllPublicFields": true, + "allDeclaredClasses": true, + "allPublicClasses": true, + "allRecordComponents": true, + "allNestMembers": true, + "allSigners": true, + "allPermittedSubclasses": true + }, + { + "condition": { + "typeReachable": "io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerIndexField" + }, + "name": "io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerIndexField", + "fields": [ + { + "name": "producerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueConsumerIndexField" + }, + "name": "io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueConsumerIndexField", + "fields": [ + { + "name": "consumerIndex" + } + ] + }, + { + "condition": { + "typeReachable": "io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerLimitField" + }, + "name": "io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerLimitField", + "fields": [ + { + "name": "producerLimit" + } + ] + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.buffer.AbstractByteBufAllocator" + }, + "name": "io.grpc.netty.shaded.io.netty.buffer.AbstractByteBufAllocator", + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, + "queryAllPublicConstructors": true, + "queryAllPublicFields": true, + "allDeclaredClasses": true, + "allPublicClasses": true, + "allRecordComponents": true, + "allNestMembers": true, + "allSigners": true, + "allPermittedSubclasses": true + }, + { + "condition": { + "typeReachable": "io.grpc.netty.shaded.io.netty.channel.socket.nio.NioSocketChannel" + }, + "name": "io.grpc.netty.shaded.io.netty.channel.socket.nio.NioSocketChannel", + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, + "queryAllPublicConstructors": true, + "queryAllPublicFields": true, + "allDeclaredClasses": true, + "allPublicClasses": true, + "allRecordComponents": true, + "allNestMembers": true, + "allSigners": true, + "allPermittedSubclasses": true, + "allDeclaredConstructors": true + }, + { + "name": "org.springframework.security.web.access.HandlerMappingIntrospectorRequestTransformer", + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "queryAllDeclaredFields": true, + "queryAllPublicMethods": true, + "queryAllPublicConstructors": true, + "queryAllPublicFields": true, + "allDeclaredClasses": true, + "allPublicClasses": true, + "allRecordComponents": true, + "allNestMembers": true, + "allSigners": true, + "allPermittedSubclasses": true + }, + { + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration", + "queryAllDeclaredMethods":true + }, + { + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration$MeterRegistryCloser", + "queryAllDeclaredMethods":true, + "methods":[{"name":"onApplicationEvent","parameterTypes":["org.springframework.context.ApplicationEvent"] }] + }, + { + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration", + "queryAllDeclaredMethods":true + }, + { + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"getDistribution","parameterTypes":[] }] + }, + { + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties$Distribution", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getPercentilesHistogram","parameterTypes":[] }, {"name":"getSlo","parameterTypes":[] }] + }, + { + "name":"org.springframework.boot.actuate.autoconfigure.metrics.PropertiesMeterFilter", + "queryAllDeclaredMethods":true, + "methods":[{"name":"accept","parameterTypes":["io.micrometer.core.instrument.Meter$Id"] }, {"name":"configure","parameterTypes":["io.micrometer.core.instrument.Meter$Id","io.micrometer.core.instrument.distribution.DistributionStatisticConfig"] }, {"name":"map","parameterTypes":["io.micrometer.core.instrument.Meter$Id"] }] + }, + { + "name":"org.springframework.boot.actuate.autoconfigure.metrics.ServiceLevelObjectiveBoundary", + "queryAllDeclaredMethods":true, + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] + }, + { + "name": "graphql.ExecutionResult", + "allDeclaredMethods": true, + "allDeclaredConstructors": true + } +] diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/META-INF/native-image/resource-config.json b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 000000000..ae7cab2a1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,13 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\Qproto2/include/google/protobuf/descriptor.proto\\E" + }, { + "pattern":"proto3/include/google/protobuf/.*.proto" + }, { + "pattern":"\\Qproto3/include/google/protobuf/compiler/plugin.proto\\E" + }, { + "pattern":"leap_second_dates.csv" + }] + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/config/application.properties b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/config/application.properties new file mode 100644 index 000000000..eb6169397 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/distro/uber/src/main/resources/config/application.properties @@ -0,0 +1,12 @@ +# Mandatory to allow the override of the KafkaServiceChangeEventChannel by the WebSocket-based one. +spring.main.allow-bean-definition-overriding=true + +# Application configuration properties +mongodb.storage.file=${MONGODB_STORAGE_PATH:#{null}} +#mongodb.storage.file=target/microcks.mv + +# Keycloak adapter configuration properties +keycloak.enabled=false + +# Default to enabled as the async-minion-uber is supposed to pull on WebSocket now. +async-api.enabled=true \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/eclipse-formatter.xml b/jdk_21_maven/cs/rest-gui/microcks/eclipse-formatter.xml new file mode 100644 index 000000000..414228120 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/eclipse-formatter.xml @@ -0,0 +1,417 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/README.md b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/README.md new file mode 100644 index 000000000..adb732802 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/README.md @@ -0,0 +1,37 @@ +# Getting started with Docker Compose + +[Docker Compose](https://docs.docker.com/compose/) is a tool for easily test and run multi-container applications. [Microcks](https://microcks.io/) offers a simple way to set up the minimal required containers to have a functional environment in your local computer. + +## Usage + +To get started, make sure you have [Docker installed](https://docs.docker.com/get-docker/) on your system. + +In your terminal issue the following commands: + +1. Clone this repository. + + ```bash + git clone https://github.com/microcks/microcks.git + ``` + +2. Change to the install folder + + ```bash + cd microcks/install/docker-compose + ``` + +3. Spin up the containers + + ```bash + docker-compose up -d + ``` + +This will start the required containers and setup a simple environment for your usage. + +Open a new browser tab and point to the `http://localhost:8080` endpoint. This will redirect you to the [Keycloak](https://www.keycloak.org/) Single Sign On page for login. Use the following default credentials: + +* **Username:** admin +* **Password:** microcks123 + +You will be redirected to the main dashboard page. You can now start [using Microcks](https://microcks.io/documentation/tutorials/getting-started/#using-microcks). + diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/async-addon.yml b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/async-addon.yml new file mode 100644 index 000000000..0c7fdce3e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/async-addon.yml @@ -0,0 +1,91 @@ +services: + zookeeper: + image: quay.io/strimzi/kafka:0.43.0-kafka-3.8.0 + container_name: microcks-zookeeper + command: [ + "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" + ] + ports: + - "2181:2181" + environment: + LOG_DIR: /tmp/logs + healthcheck: + test: nc -z localhost 2181 || exit -1 + interval: 10s + timeout: 5s + retries: 3 + + kafka: + depends_on: + - zookeeper + image: quay.io/strimzi/kafka:0.43.0-kafka-3.8.0 + container_name: microcks-kafka + command: [ + "sh", "-c", + "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override listener.security.protocol.map=$${KAFKA_LISTENER_SECURITY_PROTOCOL_MAP} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" + ] + ports: + - "9092:9092" + - "19092:19092" + environment: + LOG_DIR: "/tmp/logs" + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:19092,EXTERNAL://localhost:9092 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:19092,EXTERNAL://0.0.0.0:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + healthcheck: + test: ["CMD-SHELL", "timeout 5 bash -c 'echo > /dev/tcp/localhost/19092'"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + + app: + depends_on: + - mongo + - keycloak + - postman + image: quay.io/microcks/microcks:1.12.1 + container_name: microcks + volumes: + - "./config:/deployments/config" + ports: + - "8080:8080" + - "9090:9090" + environment: + - SPRING_PROFILES_ACTIVE=prod + - SPRING_DATA_MONGODB_URI=mongodb://mongo:27017 + - SPRING_DATA_MONGODB_DATABASE=microcks + - POSTMAN_RUNNER_URL=http://postman:3000 + - TEST_CALLBACK_URL=http://microcks:8080 + - SERVICES_UPDATE_INTERVAL=0 0 0/2 * * * + - KEYCLOAK_URL=http://keycloak:8080 + - KEYCLOAK_PUBLIC_URL=http://localhost:18080 + - JAVA_OPTIONS=-Dspring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:18080/realms/microcks -Dspring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/realms/microcks/protocol/openid-connect/certs + - ASYNC_MINION_URL=http://microcks-async-minion:8081 + - KAFKA_BOOTSTRAP_SERVER=kafka:19092 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + start_period: 35s + interval: 10s + timeout: 3s + retries: 3 + + async-minion: + depends_on: + - app + ports: + - "8081:8081" + image: quay.io/microcks/microcks-async-minion:1.12.1 + container_name: microcks-async-minion + restart: on-failure + volumes: + - "./config:/deployments/config" + environment: + - QUARKUS_PROFILE=docker-compose + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/q/health/ready"] + start_period: 10s + interval: 10s + timeout: 2s + retries: 3 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/config/application.properties b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/config/application.properties new file mode 100644 index 000000000..0411e5f7d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/config/application.properties @@ -0,0 +1,28 @@ +# Async mocking support. +async-api.enabled=true + +# Access to Microcks API server. +%docker-compose.io.github.microcks.minion.async.client.MicrocksAPIConnector/mp-rest/url=http://microcks:8080 + +# Access to Keycloak through docker network +%docker-compose.keycloak.auth.url=http://keycloak:8080 + +# Access to Kafka broker. +%docker-compose.kafka.bootstrap.servers=kafka:19092 + +# Do not save any consumer-offset on the broker as there's a re-sync on each minion startup. +%docker-compose.mp.messaging.incoming.microcks-services-updates.enable.auto.commit=false +%docker-compose.mp.messaging.incoming.microcks-services-updates.bootstrap.servers=kafka:19092 + +# Explicitly telling the minion the protocols we want to support +%docker-compose.minion.supported-bindings=KAFKA,WS + +# %docker-compose.minion.supported-bindings=KAFKA,WS,MQTT,AMQP,NATS + +# %docker-compose.mqtt.server=localhost:1883 +# %docker-compose.mqtt.username=microcks +# %docker-compose.mqtt.password=microcks + +# %docker-compose.amqp.server=localhost:5672 +# %docker-compose.amqp.username=microcks +# %docker-compose.amqp.password=microcks diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/config/features.properties b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/config/features.properties new file mode 100644 index 000000000..b088c9c8f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/config/features.properties @@ -0,0 +1,8 @@ +features.feature.async-api.enabled=true +features.feature.async-api.frequencies=3,10,30 +features.feature.async-api.default-binding=KAFKA +features.feature.async-api.endpoint-KAFKA=localhost:9092 +features.feature.async-api.endpoint-MQTT=my-mqtt-broker.apps.try.microcks.io:1883 +features.feature.async-api.endpoint-AMQP=my-amqp-broker.apps.try.microcks.io:5672 +features.feature.async-api.endpoint-WS=localhost:8081 +features.feature.async-api.endpoint-NATS=localhost:4222 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose-devmode.yml b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose-devmode.yml new file mode 100644 index 000000000..ad25528f8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose-devmode.yml @@ -0,0 +1,88 @@ +services: + mongo: + image: mongo:4.4.29 + container_name: microcks-db + volumes: + - "~/tmp/microcks-data:/data/db" + healthcheck: + test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"] + interval: 30s + timeout: 1s + retries: 3 + + postman: + image: quay.io/microcks/microcks-postman-runtime:0.6.0 + container_name: microcks-postman-runtime + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + start_period: 5s + interval: 20s + timeout: 3s + retries: 3 + + kafka: + image: redpandadata/redpanda:v24.3.1 + container_name: microcks-kafka + command: [ + "redpanda", "start", + "--overprovisioned --smp 1 --memory 1G --reserve-memory 0M --node-id 0 --check=false", + "--kafka-addr PLAINTEXT://0.0.0.0:19092,EXTERNAL://0.0.0.0:9092", + "--advertise-kafka-addr PLAINTEXT://kafka:19092,EXTERNAL://localhost:9092" + ] + ports: + - "9092:9092" + - "19092:19092" + healthcheck: + test: ["CMD-SHELL", "timeout 5 bash -c 'echo > /dev/tcp/localhost/19092'"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + + app: + depends_on: + - mongo + - postman + image: quay.io/microcks/microcks:1.12.1 + container_name: microcks + volumes: + - "./config:/deployments/config" + ports: + - "8080:8080" + - "9090:9090" + environment: + - SPRING_PROFILES_ACTIVE=prod + - SPRING_DATA_MONGODB_URI=mongodb://mongo:27017 + - SPRING_DATA_MONGODB_DATABASE=microcks + - POSTMAN_RUNNER_URL=http://postman:3000 + - TEST_CALLBACK_URL=http://microcks:8080 + - SERVICES_UPDATE_INTERVAL=0 0 0/2 * * * + - KEYCLOAK_ENABLED=false + #- MAX_UPLOAD_FILE_SIZE=3MB + - ASYNC_MINION_URL=http://microcks-async-minion:8081 + - KAFKA_BOOTSTRAP_SERVER=kafka:19092 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + start_period: 35s + interval: 10s + timeout: 3s + retries: 3 + + async-minion: + depends_on: + - app + ports: + - "8081:8081" + image: quay.io/microcks/microcks-async-minion:1.12.1 + container_name: microcks-async-minion + restart: on-failure + volumes: + - "./config:/deployments/config" + environment: + - QUARKUS_PROFILE=docker-compose + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/q/health/ready"] + start_period: 10s + interval: 10s + timeout: 2s + retries: 3 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose-with-proxy.yml b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose-with-proxy.yml new file mode 100644 index 000000000..bd1a90dec --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose-with-proxy.yml @@ -0,0 +1,71 @@ +services: + mongo: + image: mongo:4.4.29 + container_name: microcks-db + volumes: + - "~/tmp/microcks-data:/data/db" + healthcheck: + test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"] + interval: 30s + timeout: 1s + retries: 3 + + keycloak: + image: quay.io/keycloak/keycloak:26.0.0 + container_name: microcks-sso + ports: + - "18080:8080" + environment: + KEYCLOAK_ADMIN: "admin" + KEYCLOAK_ADMIN_PASSWORD: "admin" + volumes: + - "./keycloak-realm/microcks-realm-sample.json:/opt/keycloak/data/import/microcks-realm.json" + command: ["start-dev", "--hostname=http://localhost:18080", "--import-realm", "--health-enabled=true"] + healthcheck: + test: ["CMD", "sh", "-c", "echo -e 'GET /health/live HTTP/1.1\r\nHost: localhost\r\n\r\n' > /dev/tcp/localhost/9000"] + interval: 30s + timeout: 10s + retries: 3 + + postman: + image: quay.io/microcks/microcks-postman-runtime:0.6.0 + container_name: microcks-postman-runtime + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + start_period: 5s + interval: 20s + timeout: 3s + retries: 3 + + app: + depends_on: + - mongo + - keycloak + - postman + image: quay.io/microcks/microcks:1.12.1 + container_name: microcks + ports: + - "8080:8080" + - "9090:9090" + environment: + - SPRING_PROFILES_ACTIVE=prod + - SPRING_DATA_MONGODB_URI=mongodb://mongo:27017 + - SPRING_DATA_MONGODB_DATABASE=microcks + - POSTMAN_RUNNER_URL=http://postman:3000 + - TEST_CALLBACK_URL=http://microcks:8080 + - SERVICES_UPDATE_INTERVAL=0 0 0/2 * * * + - KEYCLOAK_URL=http://keycloak:8080 + - KEYCLOAK_PUBLIC_URL=http://localhost:18080 + - JAVA_OPTIONS=-Dspring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:18080/realms/microcks -Dspring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/realms/microcks/protocol/openid-connect/certs + #- MAX_UPLOAD_FILE_SIZE=3MB + - PROXY_HOST=squiqproxy.example.com + - PROXY_PORT=9876 + - PROXY_USERNAME=proxy_user + - PROXY_PASSWORD=proxy_pass + - PROXY_EXCLUDE=localhost|keycloak|postman + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + start_period: 35s + interval: 10s + timeout: 3s + retries: 3 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose.yml b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose.yml new file mode 100644 index 000000000..3220dcf89 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/docker-compose.yml @@ -0,0 +1,67 @@ +services: + mongo: + image: mongo:4.4.29 + container_name: microcks-db + volumes: + - "~/tmp/microcks-data:/data/db" + healthcheck: + test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"] + interval: 30s + timeout: 1s + retries: 3 + + keycloak: + image: quay.io/keycloak/keycloak:26.0.0 + container_name: microcks-sso + ports: + - "18080:8080" + environment: + KEYCLOAK_ADMIN: "admin" + KEYCLOAK_ADMIN_PASSWORD: "admin" + volumes: + - "./keycloak-realm/microcks-realm-sample.json:/opt/keycloak/data/import/microcks-realm.json" + command: ["start-dev", "--hostname=http://localhost:18080", "--import-realm", "--health-enabled=true"] + healthcheck: + test: ["CMD", "sh", "-c", "echo -e 'GET /health/live HTTP/1.1\r\nHost: localhost\r\n\r\n' > /dev/tcp/localhost/9000"] + start_period: 10s + interval: 10s + timeout: 2s + retries: 3 + + postman: + image: quay.io/microcks/microcks-postman-runtime:0.6.0 + container_name: microcks-postman-runtime + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + start_period: 5s + interval: 20s + timeout: 3s + retries: 3 + + app: + depends_on: + - mongo + - keycloak + - postman + image: quay.io/microcks/microcks:1.12.1 + container_name: microcks + ports: + - "8080:8080" + - "9090:9090" + environment: + - SPRING_PROFILES_ACTIVE=prod + - SPRING_DATA_MONGODB_URI=mongodb://mongo:27017 + - SPRING_DATA_MONGODB_DATABASE=microcks + - POSTMAN_RUNNER_URL=http://postman:3000 + - TEST_CALLBACK_URL=http://microcks:8080 + - SERVICES_UPDATE_INTERVAL=0 0 0/2 * * * + - KEYCLOAK_URL=http://keycloak:8080 + - KEYCLOAK_PUBLIC_URL=http://localhost:18080 + - JAVA_OPTIONS=-Dspring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:18080/realms/microcks -Dspring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/realms/microcks/protocol/openid-connect/certs + #- MAX_UPLOAD_FILE_SIZE=3MB + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + start_period: 35s + interval: 10s + timeout: 3s + retries: 3 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/kafdrop-addon.yml b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/kafdrop-addon.yml new file mode 100644 index 000000000..69ccf8efb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/kafdrop-addon.yml @@ -0,0 +1,17 @@ +services: + kafdrop: + container_name: microcks-kafkadrop + image: obsidiandynamics/kafdrop + restart: "no" + ports: + - "9000:9000" + environment: + KAFKA_BROKERCONNECT: "kafka:19092" + JVM_OPTS: "-Xms16M -Xmx48M -XX:-TieredCompilation -XX:+UseStringDeduplication" + depends_on: + - "kafka" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000"] + interval: 30s + timeout: 10s + retries: 3 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/keycloak-realm/microcks-realm-sample.json b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/keycloak-realm/microcks-realm-sample.json new file mode 100644 index 000000000..6d4aba6de --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/keycloak-realm/microcks-realm-sample.json @@ -0,0 +1,163 @@ +{ + "id": "microcks", + "realm": "microcks", + "displayName": "Microcks", + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "users" : [ + { + "username" : "admin", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "microcks123" } + ], + "realmRoles": [], + "applicationRoles": { + "realm-management": [ "manage-users", "manage-clients" ], + "account": [ "manage-account" ], + "microcks-app": [ "user", "manager", "admin"] + } + }, + { + "username": "service-account-microcks-serviceaccount", + "enabled": true, + "serviceAccountClientId": "microcks-serviceaccount", + "clientRoles": { + "microcks-app": ["manager"] + } + } + ], + "roles": { + "realm": [], + "client": { + "microcks-app": [ + { + "name": "user", + "composite": false, + "clientRole": true, + "containerId": "microcks" + }, + { + "name": "admin", + "composite": false, + "clientRole": true, + "containerId": "microcks" + }, + { + "name": "manager", + "composite": false, + "clientRole": true, + "containerId": "microcks" + } + ] + } + }, + "groups": [ + { + "name": "microcks", + "path": "/microcks", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [ + { + "name": "manager", + "path": "/microcks/manager", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + } + ] + } + ], + "defaultRoles": [], + "requiredCredentials": [ "password" ], + "scopeMappings": [], + "clientScopeMappings": { + "microcks-app": [ + { + "client": "microcks-app-js", + "roles": [ + "manager", + "admin", + "user" + ] + } + ], + "realm-management": [ + { + "client": "microcks-app-js", + "roles": [ + "manage-users", + "manage-clients" + ] + } + ] + }, + "clients": [ + { + "clientId": "microcks-app-js", + "enabled": true, + "publicClient": true, + "redirectUris": [ + "http://localhost:8080/*", + "http://localhost:58085/*" + ], + "webOrigins": [ + "+" + ], + "fullScopeAllowed": false, + "protocolMappers": [ + { + "name": "microcks-group-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "microcks-groups", + "userinfo.token.claim": "true" + } + } + ] + } + ], + "applications": [ + { + "name": "microcks-app", + "enabled": true, + "bearerOnly": true, + "defaultRoles": [ + "user" + ] + }, + { + "name": "microcks-serviceaccount", + "secret": "ab54d329-e435-41ae-a900-ec6b3fe15c54", + "enabled": true, + "bearerOnly": false, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "clientAuthenticatorType": "client-secret" + } + ], + "requiredActions": [ + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": false, + "defaultAction": false, + "priority": 90, + "config": {} + } + ], + "keycloakVersion": "10.0.1" +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/nats-addon.yml b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/nats-addon.yml new file mode 100644 index 000000000..d07ee6a8d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/nats-addon.yml @@ -0,0 +1,13 @@ +services: + nats: + image: nats:2.9.8-alpine3.16 + container_name: microcks-nats + ports: + - 4222:4222 + - 8222:8222 + healthcheck: + test: ["CMD", "nc", "-z", "localhost", "4222"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/rabbitmq-addon.yml b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/rabbitmq-addon.yml new file mode 100644 index 000000000..7abb9beaf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/docker-compose/rabbitmq-addon.yml @@ -0,0 +1,18 @@ +services: + rabbitmq: + image: rabbitmq:3.9.13-management-alpine + container_name: microcks-rabbitmq + environment: + RABBITMQ_ERLANG_COOKIE: "SWQOKODSQALRPCLNMEQG" + RABBITMQ_DEFAULT_USER: microcks + RABBITMQ_DEFAULT_PASS: microcks + RABBITMQ_DEFAULT_VHOST: "/" + ports: + - 5672:5672 + - 15672:15672 + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 30s + timeout: 10s + retries: 5 + start_period: 10s \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/keycloak-microcks-realm-addons.json b/jdk_21_maven/cs/rest-gui/microcks/install/keycloak-microcks-realm-addons.json new file mode 100644 index 000000000..ce07ead63 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/keycloak-microcks-realm-addons.json @@ -0,0 +1,124 @@ +{ + "roles": { + "client": { + "microcks-app": [ + { + "name": "user", + "composite": false, + "clientRole": true + }, + { + "name": "admin", + "composite": false, + "clientRole": true + }, + { + "name": "manager", + "composite": false, + "clientRole": true + } + ] + } + }, + "groups": [ + { + "name": "microcks", + "path": "/microcks", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [ + { + "name": "manager", + "path": "/microcks/manager", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + } + ] + } + ], + "clientScopeMappings": { + "microcks-app": [ + { + "client": "microcks-app-js", + "roles": [ + "manager", + "admin", + "user" + ] + } + ], + "realm-management": [ + { + "client": "microcks-app-js", + "roles": [ + "manage-users", + "manage-clients" + ] + } + ] + }, + "clients": [ + { + "clientId": "microcks-app", + "enabled": true, + "bearerOnly": true, + "defaultRoles": [ + "user" + ] + }, + { + "clientId": "microcks-app-js", + "enabled": true, + "publicClient": true, + "webOrigins": [ + "+" + ], + "redirectUris": [ + "https://localhost:8080/*", + "http://localhost:58085/*" + ], + "fullScopeAllowed": false, + "protocolMappers": [ + { + "name": "microcks-group-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "microcks-groups", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "clientId": "microcks-serviceaccount", + "secret": "ab54d329-e435-41ae-a900-ec6b3fe15c54", + "enabled": true, + "bearerOnly": false, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "clientAuthenticatorType": "client-secret" + } + ], + "requiredActions": [ + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": false, + "defaultAction": false, + "priority": 90, + "config": {} + } + ], + "keycloakVersion": "10.0.1" +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/keycloak-microcks-realm-full.json b/jdk_21_maven/cs/rest-gui/microcks/install/keycloak-microcks-realm-full.json new file mode 100644 index 000000000..bffb45b31 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/keycloak-microcks-realm-full.json @@ -0,0 +1,186 @@ +{ + "id": "microcks", + "realm": "microcks", + "displayName": "Microcks", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "users" : [ + { + "username" : "admin", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "microcks123" } + ], + "requiredActions": [ + "UPDATE_PASSWORD" + ], + "realmRoles": [], + "applicationRoles": { + "realm-management": [ "manage-users", "manage-clients" ], + "account": [ "manage-account" ], + "microcks-app": [ "user", "manager","admin" ] + } + } + ], + "roles": { + "realm": [], + "client": { + "microcks-app": [ + { + "name": "user", + "composite": false, + "clientRole": true, + "containerId": "microcks" + }, + { + "name": "admin", + "composite": false, + "clientRole": true, + "containerId": "microcks" + }, + { + "name": "manager", + "composite": false, + "clientRole": true, + "containerId": "microcks" + } + ] + } + }, + "groups": [ + { + "name": "microcks", + "path": "/microcks", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [ + { + "name": "manager", + "path": "/microcks/manager", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + } + ] + } + ], + "defaultRoles": [], + "requiredCredentials": [ "password" ], + "scopeMappings": [], + "clientScopeMappings": { + "microcks-app": [ + { + "client": "microcks-app-js", + "roles": [ + "manager", + "admin", + "user" + ] + } + ], + "realm-management": [ + { + "client": "microcks-app-js", + "roles": [ + "manage-users", + "manage-clients" + ] + } + ] + }, + "clients": [ + { + "clientId": "microcks-app-js", + "enabled": true, + "publicClient": true, + "redirectUris": [ + "http://localhost:8080/*", + "http://localhost:58085/*" + ], + "webOrigins": [ + "+" + ], + "fullScopeAllowed": false, + "protocolMappers": [ + { + "name": "microcks-group-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "microcks-groups", + "userinfo.token.claim": "true" + } + } + ] + } + ], + "applications": [ + { + "name": "microcks-app", + "enabled": true, + "bearerOnly": true, + "defaultRoles": [ + "user" + ] + }, + { + "name": "microcks-serviceaccount", + "secret": "ab54d329-e435-41ae-a900-ec6b3fe15c54", + "enabled": true, + "bearerOnly": false, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "clientAuthenticatorType": "client-secret" + } + ], + "requiredActions": [ + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": false, + "defaultAction": false, + "priority": 90, + "config": {} + } + ], + "keycloakVersion": "10.0.1" +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/README.md b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/README.md new file mode 100644 index 000000000..c9aa22e96 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/README.md @@ -0,0 +1,398 @@ +# Microcks + +This chart bootstraps a new [Microcks](http://microcks.io) application using the [Helm](https://helm.sh) package manager. + +Resources within this directory should work with Helm version 3+ (which do not need the Tiller server-side component). + +## Installing the Chart + +### Simple install - with no asynchronous mocking + +From the [Helm Hub](https://hub.helm.sh) directly - assuming here for the example, you are running `minikube`: + +```console +$ helm repo add microcks https://microcks.io/helm + +$ kubectl create namespace microcks + +$ helm install microcks microcks/microcks —-version 1.10.0 --namespace microcks --set microcks.url=microcks.$(minikube ip).nip.io --set keycloak.url=keycloak.$(minikube ip).nip.io --set keycloak.privateUrl=http://microcks-keycloak.microcks.svc.cluster.local:8080 + +NAME: microcks +LAST DEPLOYED: Wed Jul 31 17:38:43 2024 +NAMESPACE: microcks +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +Thank you for installing microcks. + +Your release is named microcks. + +To learn more about the release, try: + + $ helm status microcks + $ helm get microcks + +Microcks is available at https://microcks.192.168.64.6.nip.io. + +GRPC mock service is available at "microcks-grpc.192.168.64.6.nip.io". +It has been exposed using TLS passthrough on the Ingress controller, you should extract the certificate for your client using: + + $ kubectl get secret microcks-microcks-grpc-secret -n microcks -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt + +Keycloak has been deployed on https://keycloak.192.168.64.6.nip.io to protect user access. +You may want to configure an Identity Provider or add some users for your Microcks installation by login in using the +username and password found into 'microcks-keycloak-admin' secret. +``` + +From the sources cloned locally: + +```console +$ git clone https://github.com/microcks/microcks + +$ cd install/kubernetes + +$ helm install microcks ./microcks --namespace microcks \ + --set microcks.url=microcks.$(minikube ip).nip.io \ + --set keycloak.url=keycloak.$(minikube ip).nip.io \ + --set keycloak.privateUrl=http://microcks-keycloak.microcks.svc.cluster.local:8080 + +NAME: microcks +LAST DEPLOYED: Wed Jul 31 18:02:12 2024 +NAMESPACE: microcks +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +Thank you for installing microcks. + +Your release is named microcks. + +To learn more about the release, try: + + $ helm status microcks + $ helm get microcks + +Microcks is available at https://microcks.192.168.64.6.nip.io. + +GRPC mock service is available at "microcks-grpc.192.168.64.6.nip.io". +It has been exposed using TLS passthrough on the Ingress controller, you should extract the certificate for your client using: + + $ kubectl get secret microcks-microcks-grpc-secret -n -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt + +Keycloak has been deployed on https://keycloak.192.168.64.6.nip.io to protect user access. +You may want to configure an Identity Provider or add some users for your Microcks installation by login in using the +username and password found into 'microcks-keycloak-admin' secret. +``` + +### Advanced install - with asynchronous mocking + +Microcks supports mocking of event-driven API thanks to [AsyncAPI Spec](https://asyncapi.com). Microcks will take care of publishing sample messages for you on a message broker. You may reuse an existing broker or let Microcks deploy its own (this is the default when turning on this feature). + +To install a Kafka message broker during its deployment, Microcks relies on [Strimzi Operator](https://strimzi.io) and will try to create such custom resources such as `Kafka` and `KafkaTopic`. When using this configuration, you will thus need to install Strimzi Operator cluster-wide or on targeted namespace. + +Here are some commands below on how to do that onto a Minikube instance: + +```console +$ helm repo add strimzi https://strimzi.io/charts/ +$ helm repo add microcks https://microcks.io/helm + +$ kubectl create namespace microcks + +$ helm install strimzi strimzi/strimzi-kafka-operator --namespace microcks + +$ helm install microcks ./microcks --namespace=microcks \ + --set appName=microcks --set features.async.enabled=true \ + --set microcks.url=microcks.$(minikube ip).nip.io \ + --set keycloak.url=keycloak.$(minikube ip).nip.io \ + --set keycloak.privateUrl=http://microcks-keycloak.microcks.svc.cluster.local:8080 \ + --set features.async.kafka.url=$(minikube ip).nip.io + +NAME: microcks +LAST DEPLOYED: Wed Jul 31 18:27:35 2024 +NAMESPACE: microcks +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +Thank you for installing microcks. + +Your release is named microcks. + +To learn more about the release, try: + + $ helm status microcks + $ helm get microcks + +Microcks is available at https://microcks.192.168.64.6.nip.io. + +GRPC mock service is available at "microcks-grpc.192.168.64.6.nip.io". +It has been exposed using TLS passthrough on the Ingress controller, you should extract the certificate for your client using: + + $ kubectl get secret microcks-microcks-grpc-secret -n -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt + +Keycloak has been deployed on https://keycloak.192.168.64.6.nip.io to protect user access. +You may want to configure an Identity Provider or add some users for your Microcks installation by login in using the +username and password found into 'microcks-keycloak-admin' secret. + +Kafka broker has been deployed on microcks-kafka.192.168.64.6.nip.io. +It has been exposed using TLS passthrough of the Ingress controller, you shoud extract the certificate for your client using: + + $ kubectl get secret microcks-kafka-cluster-ca-cert -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt +``` + +[This video](https://www.youtube.com/watch?v=u7SP1bQ8_FE) details the setup. + +## Configuration + +All configurable variables and default values can be seen in `values.yaml`, with reasonable comments. + +Typically, you may want to configure the following blocks and options: + +* Global part is mandatory and contain attributes like `appName` of your install, +* `microcks` part is mandatory and contain attributes like the number of `replicas` and the access `url` if you want some customizations, +* `postman` part is mandatory for the number of `replicas` +* `keycloak` part is optional and allows specifying : + * if you want a new install or reuse an existing instance: `keycloak.install` + * if you want microcks to delegate authentication to keycloak: `keycloak.enabled`. + * Note that `keycloak.enabled=false` forces keycloak not be installed in this release +* `mongodb` part is optional and allows specifying if you want a new install or reuse an existing instance. +* `features` part is optional and allows enabling and configuring opt-in features of Microcks. + +The table below describes all the fields of the `values.yaml`, providing information on what's mandatory and what's optional as well as default values. + +| Section | Property | Description | +|------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `` | `appName` | **Mandatory**. Name of the Microcks deployment (Default to `Microcks`) | +| `` | `ingresses` | **Mandatory**. Enable or Disable ingresses creation and associated secrets (Default to `true`) | +| `` | `gatewayRoutes` | **Mandatory**. Enable or Disable `HTTPRoute` creation instead of `Ingress` (Default to `false`) | +| `` | `gatewayRefName` | **Optional**. Set the name of the `Gateway` to reference when `gatewayRoutes` is enabled (Default to `default`) | +| `` | `gatewayRefNamespace` | **Optional**. Set the name of the `Gateway` to reference when `gatewayRoutes` is enabled (Default to `empty` which means "local namespace") | +| `` | `gatewayRefSectionName` | **Optional**. Set the name of the `Gateway` section/listener to reference from `HTTPRoute` (Default to `https`) | +| `` | `grpcGatewayRefSectionName` | **Optional**. Set the name of the `Gateway` section/listener to reference from `GRPCRoute` (Default to `grpc`) | +| `` | `commonLabels` | **Optional**. Defines the list of labels that will be added to all kubernetes resources (Default to `empty`) | +| `` | `commonAnnotations` | **Optional**. Defines the list of annotations that will be added to all kubernetes resources (Default to `empty`) | +| `microcks` | `url` | **Mandatory**. The URL to use for exposing `Ingress` | +| `microcks` | `ingressSecretRef` | **Optional**. The name of a TLS Secret for securing `Ingress`. If missing, self-signed certificate is generated. | +| `microcks` | `ingressAnnotations` | **Optional**. A map of annotations that will be added to the `Ingress` or `HTTPRoute` for Microcks main pod. If these annotations are triggering a Certificate generation (for example through [cert-mamanger.io](https://cert-manager.io/)). The `generateCert` property should be set to `false`. | +| `microcks` | `generateCert` | **Optional**. Whether to generate self-signed certificate or not if no valid `ingressSecretRef` provided. Default is `true` | +| `microcks` | `gatewayRefName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `gatewayRefNamespace` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `gatewayRefSectionName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `replicas` | **Optional**. The number of replicas for the Microcks main pod. Default is `1`. | +| `microcks` | `image` | **Optional**. The reference of container image used. Chart comes with its default version. | +| `microcks` | `serviceType` | **Optional**. The service type used. Defaults to `ClusterIP`. | +| `microcks` | `grpcEnableTLS` | **Optional**. Flag to disable TLS on gRPC endpoint. Default value is `true`. | +| `microcks` | `grpcSecretRef` | **Optional**. The name of a TLS Secret for securing the gRPC endpoint. If missing, self-signed certificate is generated. | +| `microcks` | `grpcIngressAnnotations` | **Optional**. A map of annotations that will be added to the `Ingress` or `GRPCRoute` for Microcks GRPC mocks. This allows you to specify specific ingress class and GRPC specific settings. | +| `microcks` | `grpcGatewayRefName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `grpcGatewayRefNamespace` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `grpcGatewayRefSectionName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `resources` | **Optional**. Some resources constraints to apply on Microcks pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container). | +| `microcks` | `env` | **Optional**. Some environment variables to add on Microcks container. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/#define-an-environment-variable-for-a-container). | +| `microcks` | `logLevel` | **Optional**. Allows to tune the verbosity level of logs. Default is `INFO`. You can use `DEBUG` for more verbosity or `WARN` for less. | +| `microcks` | `mockInvocationStats` | **Optional**. Allows to disable invocation stats on mocks. Default is `true` (enabled). | +| `microcks` | `extraProperties` | **Optional**. Allows to add yaml extra application configurations. Default is `empty` (disabled). | +| `microcks` | `customSecretRef` | **Optional**. Permit to use a secret (for exemple a keystore). Default is `false` (disabled). | +| `postman` | `replicas` | **Optional**. The number of replicas for the Microcks Postman pod. Default is `1`. | +| `postman` | `image` | **Optional**. The reference of container image used. Chart comes with its default version. | +| `postman` | `resources` | **Optional**. Some resources constraints to apply on Postman pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container).. | +| `keycloak` | `enabled` | **Optional**. Flag for using Keycloak to protect Microcks API. Default is `true`. Set to `false` if you want to bypass the authorization checks, **this is not recommended for production environment**. | +| `keycloak` | `install` | **Optional**. Flag for Keycloak installation. Default is `true`. Set to `false` if you want to reuse an existing Keycloak instance. | +| `keycloak` | `realm` | **Optional**. Name of Keycloak realm to use. Should be setup only if `install` is `false` and you want to reuse an existing realm. Default is `microcks`. | +| `keycloak` | `url` | **Mandatory**. The URL of Keycloak install - indeed just the hostname + port part - if it already exists or the one used for exposing Keycloak `Ingress`. | +| `keycloak` | `privateUrl` | **Optional**. A private URL - a full URL here - used by the Microcks component to internally join Keycloak. This is also known as `backendUrl` in [Keycloak doc](https://www.keycloak.org/docs/latest/server_installation/#_hostname). When specified, the `keycloak.url` is used as `frontendUrl` in Keycloak terms. | +| `keycloak` | `ingressSecretRef` | **Optional**. The name of a TLS Secret for securing `Ingress`. If missing, self-signed certificate is generated. | +| `keycloak` | `ingressAnnotations` | **Optional**. A map of annotations that will be added to the `Ingress` or `HTTPRoute` for Keycloak pod. If these annotations are triggering a Certificate generation (for example through [cert-mamanger.io](https://cert-manager.io/)). The `generateCert` property should be set to `false`. | +| `keycloak` | `generateCert` | **Optional**. Whether to generate self-signed certificate or not if no valid `ingressSecretRef` provided. Default is `true` | +| `keycloak` | `gatewayRefName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `keycloak` | `gatewayRefNamespace` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `keycloak` | `gatewayRefSectionName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `keycloak` | `secretRef` | **Optional**. Reference of a Secret containing credentials for connecting a provided Keycloak instance. Mandatory if `install` is `false`. | +| `keycloak` | `pvcAnnotations` | **Optional**. A map of annotations that will be added to the `pvc` for the Keycloak pod. | +| `keycloak` | `image` | **Optional**. The reference of container image used. Chart comes with its default version. | +| `keycloak` | `serviceType` | **Optional**. The service type used. Defaults to `ClusterIP`. | +| `keycloak` | `resources` | **Optional**. Some resources constraints to apply on Keycloak pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container). | +| `keycloak` | `persistent` | **Optional**. Flag for Keycloak persistence. Default is `true`. Set to `false` if you want an ephemeral Keycloak installation. | +| `keycloak` | `volumeSize` | **Optional**. Size of persistent volume claim for Keycloak. Default is `1Gi`. Not used if not persistent install asked. | +| `keycloak` | `storageClassName` | **Optional**. The cluster storage class to use for persistent volume claim. If not specified, we rely on cluster default storage class. | +| `keycloak` | `postgresImage` | **Optional**. The reference of container image used. Chart comes with its default version. | +| `keycloak` | `postgresResources` | **Optional**. Some resources constraints to apply on Keycloak Postgres pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container). | +| `keycloak` | `serviceAccount` | **Optional**. A service account to create into Microcks Keycloak realm. Default is `microcks-serviceaccount`. | +| `keycloak` | `serviceAccountCredentials` | **Optional**. The credentials of Keycloak realm service account for Microcks. Default is `ab54d329-e435-41ae-a900-ec6b3fe15c54`. | +| `mongodb` | `install` | **Optional**. Flag for MongoDB installation. Default is `true`. Set to `false` if you want to reuse an existing MongoDB instance. | +| `mongodb` | `uri` | **Optional**. MongoDB URI in case you're reusing existing MongoDB instance. Mandatory if `install` is `false`. | +| `mongodb` | `uriParameters` | **Optional**. Allows you to add parameters to the mongodb uri connection string. | +| `mongodb` | `database` | **Optional**. MongoDB database name in case you're reusing existing MongoDB instance. Used if `install` is `false`. Default to `appName`. | +| `mongodb` | `secretRef` | **Optional**. Reference of a Secret containing credentials for connecting a provided MongoDB instance. Mandatory if `install` is `false`. | +| `mongodb` | `pvcAnnotations` | **Optional**. A map of annotations that will be added to the `pvc` for the MongoDB pod. | +| `mongodb` | `resources` | **Optional**. Some resources constraints to apply on MongoDB pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container). | +| `mongodb` | `persistent` | **Optional**. Flag for MongoDB persistence. Default is `true`. Set to `false` if you want an ephemeral MongoDB installation. | +| `mongodb` | `volumeSize` | **Optional**. Size of persistent volume claim for MongoDB. Default is `2Gi`. Not used if not persistent install asked. | +| `mongodb` | `storageClassName` | **Optional**. The cluster storage class to use for persistent volume claim. If not specified, we rely on cluster default storage class. | +| `features` | `repositoryFilter` | **Optional**. Feature allowing to filter API and services on main page. Must be explicitly `enabled`. See [Organizing repository](https://microcks.io/documentation/using/advanced/organizing/#master-level-filter) for more information. | +| `features` | `repositoryTenancy` | **Optional**. Feature allowing to segment and delegate API and services management according the `repositoryFilter` master criteria. Must be explicitly `enabled`. See [Organizing repository](https://microcks.io/documentation/using/advanced/organizing/#rbac-security-segmentation) for more information. | +| `features` | `microcksHub.enabled` | **Optional**. Feature allowing to directly import mocks coming from `hub.microcks.io` marketplace. Default is `true`. See [Micorkcs Hub](https://microcks.io/documentation/using/advanced/microcks-hub) for more information. | +| `features` | `async.enabled` | **Optional**. Feature allowing to mock an tests asynchronous APIs through Events. Enabling it requires an active message broker. Default is `false`. | +| `features` | `async.image` | **Optional**. The reference of container image used for `async-minion` component. Chart comes with its default version. | +| `features` | `async.env` | **Optional**. Some environment variables to add on `async-minion` container. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/#define-an-environment-variable-for-a-container). | +| `features` | `aiCopilot` | **Optional**. Feature allowing to activate AI Copilot. Must be explicitly `enabled`. Default is `false`. | + +### AI Copilot feature + +Here are below the configuration properties of the AI Copilot feature: + +| Section | Property | Description | +|-----------------------------| ------------------ |-------------------------------------------------------------------------------------------------------------------------------------------| +| `features.aiCopilot` | `enabled` | **Optional**. Flag for enabling the feature. Default is `false`. Set to `true` to activate. | +| `features.aiCopilot` | `implementation` | **Optional**. Allow to choose an AI service implementation. Default is `openai` and its the only supported value at the moment. | +| `features.aiCopilot.openai` | `apiKey` | **Madantory** when enabled. Put here your OpenAI API key. | +| `features.aiCopilot.openai` | `timeout` | **Optional**. Allow the customization of the timeout when requesting OpenAI. Default to `20` seconds. | +| `features.aiCopilot.openai` | `model` | **Optional**. Allow the customization of the OpenAI model to use. Default may vary from one Microcks version to another. | +| `features.aiCopilot.openai` | `maxTokens` | **Optional**. Allow the customization of the maximum number of tokens that may be exchanged by OpenAI services. Default to `2000` tokens? | + +### Kafka feature details + +Here are below the configuration properties of the Kafka support feature: + +| Section | Property | Description | +| ------------------------------------- |----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.kafka` | `install` | **Optional**. Flag for Kafka installation. Default is `true` and require Strimzi Operator to be setup. Set to `false` if you want to reuse an existing Kafka instance. | +| `features.async.kafka` | `url` | **Optional**. The URL of Kafka broker if it already exists or the one used for exposing Kafka `Ingress` when we install it. In this later case, it should only be the subdomain part (eg: `apps.example.com`). | +| `features.async.kafka` | `ingressClassName` | **Optional**. The ingress class to use for exposing broker to the outer world when installing it. Default is `nginx`. | +| `features.async.kafka` | `persistent` | **Optional**. Flag for Kafka persistence. Default is `false`. Set to `true` if you want a persistent Kafka installation. | +| `features.async.kafka` | `volumeSize` | **Optional**. Size of persistent volume claim for Kafka. Default is `2Gi`. Not used if not persistent install asked. | +| `features.async.kafka.schemaRegistry` | `url` | **Optional**. The API URL of a Kafka Schema Registry. Used for Avro based serialization | +| `features.async.kafka.schemaRegistry` | `confluent` | **Optional**. Flag for indicating that registry is a Confluent one, or using a Confluent compatibility mode. Default to `true` | +| `features.async.kafka.schemaRegistry` | `username` | **Optional**. Username for connecting to the specified Schema registry. Default to `` | +| `features.async.kafka.schemaRegistry` | `credentialsSource` | **Optional**. Source of the credentials for connecting to the specified Schema registry. Default to `USER_INFO` | +| `features.async.kafka.authentication` | `type` | **Optional**. The type of authentication for connecting to a pre-existing Kafka broker. Supports `SSL` or `SASL_SSL`. Default to `none` | +| `features.async.kafka.authentication` | `truststoreType` | **Optional**. For TLS transport, you'll always need a truststore to hold your cluster certificate. Default to `PKCS12` | +| `features.async.kafka.authentication` | `truststoreSecretRef` | **Optional**. For TLS transport, the reference of a Secret holding truststore and its password. Set `secret`, `storeKey` and `passwordKey` properties | +| `features.async.kafka.authentication` | `keystoreType` | **Optional**. In case of `SSL` type, you'll also need a keystore to hold your user private key for mutual TLS authentication. Default to `PKCS12` | +| `features.async.kafka.authentication` | `keystoreSecretRef` | **Optional**. For mutual TLS authentication, the reference of a Secret holding keystore and its password. Set `secret`, `storeKey` and `passwordKey` properties | +| `features.async.kafka.authentication` | `saslMechanism` | **Optional**. For SASL authentication, you'll have to specify an additional authentication mechanism such as `SCRAM-SHA-512` or `OAUTHBEARER` | +| `features.async.kafka.authentication` | `saslJaasConfig` | **Optional**. For SASL authentication, you'll have to specify a JAAS configuration line with login module, username and password. | +| `features.async.kafka.authentication` | `saslLoginCallbackHandlerClass` | **Optional**. For SASL authentication, you may want to provide a Login Callback Handler implementations. This implementation may be provided by extending the main and `async-minion` images and adding your own libs. | + +#### MQTT feature details + +Here are below the configuration properties of the MQTT support feature: + +| Section | Property | Description | +| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `features.async.mqtt` | `url` | **Optional**. The URL of MQTT broker (eg: `my-mqtt-broker.example.com:1883`). Default is undefined which means that feature is disabled. | +| `features.async.mqtt` | `username` | **Optional**. The username to use for connecting to secured MQTT broker. Default to `microcks`. | +| `features.async.mqtt` | `password` | **Optional**. The password to use for connecting to secured MQTT broker. Default to `microcks`. | + +#### WebSocket feature details + +Here are below the configuration properties of the WebSocket support feature: + +| Section | Property | Description | +| ------------------- |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.ws` | `ingressSecretRef` | **Optional**. The name of a TLS Secret for securing WebSocket `Ingress`. If missing, self-signed certificate is generated. | +| `features.async.ws` | `ingressAnnotations` | **Optional**. A map of annotations that will be added to the `Ingress` for Microcks WebSocket mocks. If these annotations are triggering a Certificate generation (for example through [cert-mamanger.io](https://cert-manager.io/)). The `generateCert` property should be set to `false`.| +| `features.async.ws` | `generateCert` | **Optional**. Whether to generate self-signed certificate or not if no valid `ingressSecretRef` provided. Default is `true` | +| `features.async.ws` | `gatewayRefName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `features.async.ws` | `gatewayRefNamespace` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `features.async.ws` | `gatewayRefSectionName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | + +#### AMQP feature details + +Here are below the configuration properties of the AMQP support feature: + +| Section | Property | Description | +| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `features.async.amqp` | `url` | **Optional**. The URL of AMQP broker (eg: `my-amqp-broker.example.com:5672`). Default is undefined which means that feature is disabled. | +| `features.async.amqp` | `username` | **Optional**. The username to use for connecting to secured AMQP broker. Default to `microcks`. | +| `features.async.amqp` | `password` | **Optional**. The password to use for connecting to secured AMQP broker. Default to `microcks`. | + +#### NATS feature details + +Here are below the configuration properties of the NATS support feature: + +| Section | Property | Description | +| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `features.async.nats` | `url` | **Optional**. The URL of NATS broker (eg: `my-nats-broker.example.com:4222`). Default is undefined which means that feature is disabled. | +| `features.async.nats` | `username` | **Optional**. The username to use for connecting to secured NATS broker. Default to `microcks`. | +| `features.async.nats` | `password` | **Optional**. The password to use for connecting to secured NATS broker. Default to `microcks`. | + +#### Google PubSub feature details + +Here are below the configuration properties of the Google PubSub support feature: + +| Section | Property | Description | +|-------------------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.googlepubsub` | `project` | **Optional**. The GCP project id of PubSub (eg: `my-gcp-project-347219`). Default is undefined which means that feature is disabled. | +| `features.async.googlepubsub` | `serviceAccountSecretRef` | **Optional**. The name of a Generic Secret holding Service Account JSON credentiels. Set `secret` and `fileKey` properties. | + +#### Amazon SQS feature details + +Here are below the configuration properties of the Amazon SQS support feature: + +| Section | Property | Description | +|----------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.sqs` | `region` | **Optional**. The AWS region for connecting SQS service (eg: `eu-west-3`). Default is undefined which means that feature is disabled. | +| `features.async.sqs` | `credentialsType` | **Optional**. The type of credentials we use for authentication. 2 options here `env-variable` or `profile`. Default to `env-variable`. | +| `features.async.sqs` | `credentialsProfile` | **Optional**. When using `profile` authent, name of profile to use for authenticating to SQS. This profile should be present into a credentials file mounted from a Secret (see below). Default to `microcks-sqs-admin`. | +| `features.async.sqs` | `credentialsSecretRef` | **Optional**. The name of a Generic Secret holding either environment variables (set `secret` and `accessKeyIdKey`, `secretAccessKeyKey` and optional `sessionTokenKey` properties) or an AWS credentials file with referenced profile (set `secret` and `fileKey` properties). | +| `features.async.sqs` | `endpointOverride` | **Optional**. The AWS endpoint URI used for API calls. Handy for using SQS via [LocalStack](https://localstack.cloud). | + +#### Amazon SNS feature details + +Here are below the configuration properties of the Amazon SNS support feature: + +| Section | Property | Description | +|----------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.sns` | `region` | **Optional**. The AWS region for connecting SNS service (eg: `eu-west-3`). Default is undefined which means that feature is disabled. | +| `features.async.sns` | `credentialsType` | **Optional**. The type of credentials we use for authentication. 2 options here `env-variable` or `profile`. Default to `env-variable`. | +| `features.async.sns` | `credentialsProfile` | **Optional**. When using `profile` authent, name of profile to use for authenticating to SQS. This profile should be present into a credentials file mounted from a Secret (see below). Default to `microcks-sns-admin`. | +| `features.async.sns` | `credentialsSecretRef` | **Optional**. The name of a Generic Secret holding either environment variables (set `secret` and `accessKeyIdKey`, `secretAccessKeyKey` and optional `sessionTokenKey` properties) or an AWS credentials file with referenced profile (set `secret` and `fileKey` properties). | +| `features.async.sns` | `endpointOverride` | **Optional**. The AWS endpoint URI used for API calls. Handy for using SNS via [LocalStack](https://localstack.cloud). | + +> **Note:** Enabling both SQS and SNS features and using `env-variable` credentials type for both, may lead to collision as both clients rely on the +> same environment variables. So you have to specify `credentialsSecretRef` on only one of those two services and be sure that the access key and secret +> access key mounted refers to a IAM account having write access to both services. + +### Examples + +You may want to launch custom installation with such a command: + + ```console + $ helm install microcks ./microcks --namespace=microcks \ + --set appName=mocks --set mongodb.volumeSize=5Gi \ + --set microcks.url=mocks-microcks.apps.example.com \ + --set keycloak.url=keycloak-microcks.apps.example.com + ``` + +or - with included Kafka for async mocking turned on: + + ```console + $ helm install microcks ./microcks --namespace=microcks \ + --set appName=microcks --set features.async.enabled=true \ + --set microcks.url=microcks.$(minikube ip).nip.io \ + --set keycloak.url=keycloak.$(minikube ip).nip.io \ + --set features.async.kafka.url=$(minikube ip).nip.io + ``` + +## Checking everything is OK + +Just check you've got this 5 running pods: + +```console +$ kubectl get pods -n microcks +NAME READY STATUS RESTARTS AGE +microcks-7f8445887d-f7wt9 1/1 Running 0 39s +microcks-keycloak-bbbfcb-8flrr 1/1 Running 0 39s +microcks-keycloak-postgresql-6dc77c4968-5dcjd 1/1 Running 0 39s +microcks-mongodb-6d558666dc-zdhxl 1/1 Running 0 39s +microcks-postman-runtime-58bf695b59-nm858 1/1 Running 0 39s +``` + +## Deleting the Chart + +```console +helm delete microcks +helm del --purge microcks +``` diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/.helmignore b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/Chart.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/Chart.yaml new file mode 100644 index 000000000..9976665f4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: microcks +version: 1.12.1 +kubeVersion: '>=1.17.0-0' +description: Microcks - API Mocking and Testing +type: application +keywords: + - mocks + - microservice + - api + - test +home: https://microcks.io +sources: + - http://github.com/microcks/microcks/ + - https://microcks.io +maintainers: + - name: Laurent Broudoux + email: laurent.broudoux@gmail.com +icon: https://microcks.io/images/microcks-big.png +appVersion: 1.12.1 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/README.md b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/README.md new file mode 100644 index 000000000..c9aa22e96 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/README.md @@ -0,0 +1,398 @@ +# Microcks + +This chart bootstraps a new [Microcks](http://microcks.io) application using the [Helm](https://helm.sh) package manager. + +Resources within this directory should work with Helm version 3+ (which do not need the Tiller server-side component). + +## Installing the Chart + +### Simple install - with no asynchronous mocking + +From the [Helm Hub](https://hub.helm.sh) directly - assuming here for the example, you are running `minikube`: + +```console +$ helm repo add microcks https://microcks.io/helm + +$ kubectl create namespace microcks + +$ helm install microcks microcks/microcks —-version 1.10.0 --namespace microcks --set microcks.url=microcks.$(minikube ip).nip.io --set keycloak.url=keycloak.$(minikube ip).nip.io --set keycloak.privateUrl=http://microcks-keycloak.microcks.svc.cluster.local:8080 + +NAME: microcks +LAST DEPLOYED: Wed Jul 31 17:38:43 2024 +NAMESPACE: microcks +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +Thank you for installing microcks. + +Your release is named microcks. + +To learn more about the release, try: + + $ helm status microcks + $ helm get microcks + +Microcks is available at https://microcks.192.168.64.6.nip.io. + +GRPC mock service is available at "microcks-grpc.192.168.64.6.nip.io". +It has been exposed using TLS passthrough on the Ingress controller, you should extract the certificate for your client using: + + $ kubectl get secret microcks-microcks-grpc-secret -n microcks -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt + +Keycloak has been deployed on https://keycloak.192.168.64.6.nip.io to protect user access. +You may want to configure an Identity Provider or add some users for your Microcks installation by login in using the +username and password found into 'microcks-keycloak-admin' secret. +``` + +From the sources cloned locally: + +```console +$ git clone https://github.com/microcks/microcks + +$ cd install/kubernetes + +$ helm install microcks ./microcks --namespace microcks \ + --set microcks.url=microcks.$(minikube ip).nip.io \ + --set keycloak.url=keycloak.$(minikube ip).nip.io \ + --set keycloak.privateUrl=http://microcks-keycloak.microcks.svc.cluster.local:8080 + +NAME: microcks +LAST DEPLOYED: Wed Jul 31 18:02:12 2024 +NAMESPACE: microcks +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +Thank you for installing microcks. + +Your release is named microcks. + +To learn more about the release, try: + + $ helm status microcks + $ helm get microcks + +Microcks is available at https://microcks.192.168.64.6.nip.io. + +GRPC mock service is available at "microcks-grpc.192.168.64.6.nip.io". +It has been exposed using TLS passthrough on the Ingress controller, you should extract the certificate for your client using: + + $ kubectl get secret microcks-microcks-grpc-secret -n -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt + +Keycloak has been deployed on https://keycloak.192.168.64.6.nip.io to protect user access. +You may want to configure an Identity Provider or add some users for your Microcks installation by login in using the +username and password found into 'microcks-keycloak-admin' secret. +``` + +### Advanced install - with asynchronous mocking + +Microcks supports mocking of event-driven API thanks to [AsyncAPI Spec](https://asyncapi.com). Microcks will take care of publishing sample messages for you on a message broker. You may reuse an existing broker or let Microcks deploy its own (this is the default when turning on this feature). + +To install a Kafka message broker during its deployment, Microcks relies on [Strimzi Operator](https://strimzi.io) and will try to create such custom resources such as `Kafka` and `KafkaTopic`. When using this configuration, you will thus need to install Strimzi Operator cluster-wide or on targeted namespace. + +Here are some commands below on how to do that onto a Minikube instance: + +```console +$ helm repo add strimzi https://strimzi.io/charts/ +$ helm repo add microcks https://microcks.io/helm + +$ kubectl create namespace microcks + +$ helm install strimzi strimzi/strimzi-kafka-operator --namespace microcks + +$ helm install microcks ./microcks --namespace=microcks \ + --set appName=microcks --set features.async.enabled=true \ + --set microcks.url=microcks.$(minikube ip).nip.io \ + --set keycloak.url=keycloak.$(minikube ip).nip.io \ + --set keycloak.privateUrl=http://microcks-keycloak.microcks.svc.cluster.local:8080 \ + --set features.async.kafka.url=$(minikube ip).nip.io + +NAME: microcks +LAST DEPLOYED: Wed Jul 31 18:27:35 2024 +NAMESPACE: microcks +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +Thank you for installing microcks. + +Your release is named microcks. + +To learn more about the release, try: + + $ helm status microcks + $ helm get microcks + +Microcks is available at https://microcks.192.168.64.6.nip.io. + +GRPC mock service is available at "microcks-grpc.192.168.64.6.nip.io". +It has been exposed using TLS passthrough on the Ingress controller, you should extract the certificate for your client using: + + $ kubectl get secret microcks-microcks-grpc-secret -n -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt + +Keycloak has been deployed on https://keycloak.192.168.64.6.nip.io to protect user access. +You may want to configure an Identity Provider or add some users for your Microcks installation by login in using the +username and password found into 'microcks-keycloak-admin' secret. + +Kafka broker has been deployed on microcks-kafka.192.168.64.6.nip.io. +It has been exposed using TLS passthrough of the Ingress controller, you shoud extract the certificate for your client using: + + $ kubectl get secret microcks-kafka-cluster-ca-cert -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt +``` + +[This video](https://www.youtube.com/watch?v=u7SP1bQ8_FE) details the setup. + +## Configuration + +All configurable variables and default values can be seen in `values.yaml`, with reasonable comments. + +Typically, you may want to configure the following blocks and options: + +* Global part is mandatory and contain attributes like `appName` of your install, +* `microcks` part is mandatory and contain attributes like the number of `replicas` and the access `url` if you want some customizations, +* `postman` part is mandatory for the number of `replicas` +* `keycloak` part is optional and allows specifying : + * if you want a new install or reuse an existing instance: `keycloak.install` + * if you want microcks to delegate authentication to keycloak: `keycloak.enabled`. + * Note that `keycloak.enabled=false` forces keycloak not be installed in this release +* `mongodb` part is optional and allows specifying if you want a new install or reuse an existing instance. +* `features` part is optional and allows enabling and configuring opt-in features of Microcks. + +The table below describes all the fields of the `values.yaml`, providing information on what's mandatory and what's optional as well as default values. + +| Section | Property | Description | +|------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `` | `appName` | **Mandatory**. Name of the Microcks deployment (Default to `Microcks`) | +| `` | `ingresses` | **Mandatory**. Enable or Disable ingresses creation and associated secrets (Default to `true`) | +| `` | `gatewayRoutes` | **Mandatory**. Enable or Disable `HTTPRoute` creation instead of `Ingress` (Default to `false`) | +| `` | `gatewayRefName` | **Optional**. Set the name of the `Gateway` to reference when `gatewayRoutes` is enabled (Default to `default`) | +| `` | `gatewayRefNamespace` | **Optional**. Set the name of the `Gateway` to reference when `gatewayRoutes` is enabled (Default to `empty` which means "local namespace") | +| `` | `gatewayRefSectionName` | **Optional**. Set the name of the `Gateway` section/listener to reference from `HTTPRoute` (Default to `https`) | +| `` | `grpcGatewayRefSectionName` | **Optional**. Set the name of the `Gateway` section/listener to reference from `GRPCRoute` (Default to `grpc`) | +| `` | `commonLabels` | **Optional**. Defines the list of labels that will be added to all kubernetes resources (Default to `empty`) | +| `` | `commonAnnotations` | **Optional**. Defines the list of annotations that will be added to all kubernetes resources (Default to `empty`) | +| `microcks` | `url` | **Mandatory**. The URL to use for exposing `Ingress` | +| `microcks` | `ingressSecretRef` | **Optional**. The name of a TLS Secret for securing `Ingress`. If missing, self-signed certificate is generated. | +| `microcks` | `ingressAnnotations` | **Optional**. A map of annotations that will be added to the `Ingress` or `HTTPRoute` for Microcks main pod. If these annotations are triggering a Certificate generation (for example through [cert-mamanger.io](https://cert-manager.io/)). The `generateCert` property should be set to `false`. | +| `microcks` | `generateCert` | **Optional**. Whether to generate self-signed certificate or not if no valid `ingressSecretRef` provided. Default is `true` | +| `microcks` | `gatewayRefName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `gatewayRefNamespace` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `gatewayRefSectionName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `replicas` | **Optional**. The number of replicas for the Microcks main pod. Default is `1`. | +| `microcks` | `image` | **Optional**. The reference of container image used. Chart comes with its default version. | +| `microcks` | `serviceType` | **Optional**. The service type used. Defaults to `ClusterIP`. | +| `microcks` | `grpcEnableTLS` | **Optional**. Flag to disable TLS on gRPC endpoint. Default value is `true`. | +| `microcks` | `grpcSecretRef` | **Optional**. The name of a TLS Secret for securing the gRPC endpoint. If missing, self-signed certificate is generated. | +| `microcks` | `grpcIngressAnnotations` | **Optional**. A map of annotations that will be added to the `Ingress` or `GRPCRoute` for Microcks GRPC mocks. This allows you to specify specific ingress class and GRPC specific settings. | +| `microcks` | `grpcGatewayRefName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `grpcGatewayRefNamespace` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `grpcGatewayRefSectionName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `microcks` | `resources` | **Optional**. Some resources constraints to apply on Microcks pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container). | +| `microcks` | `env` | **Optional**. Some environment variables to add on Microcks container. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/#define-an-environment-variable-for-a-container). | +| `microcks` | `logLevel` | **Optional**. Allows to tune the verbosity level of logs. Default is `INFO`. You can use `DEBUG` for more verbosity or `WARN` for less. | +| `microcks` | `mockInvocationStats` | **Optional**. Allows to disable invocation stats on mocks. Default is `true` (enabled). | +| `microcks` | `extraProperties` | **Optional**. Allows to add yaml extra application configurations. Default is `empty` (disabled). | +| `microcks` | `customSecretRef` | **Optional**. Permit to use a secret (for exemple a keystore). Default is `false` (disabled). | +| `postman` | `replicas` | **Optional**. The number of replicas for the Microcks Postman pod. Default is `1`. | +| `postman` | `image` | **Optional**. The reference of container image used. Chart comes with its default version. | +| `postman` | `resources` | **Optional**. Some resources constraints to apply on Postman pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container).. | +| `keycloak` | `enabled` | **Optional**. Flag for using Keycloak to protect Microcks API. Default is `true`. Set to `false` if you want to bypass the authorization checks, **this is not recommended for production environment**. | +| `keycloak` | `install` | **Optional**. Flag for Keycloak installation. Default is `true`. Set to `false` if you want to reuse an existing Keycloak instance. | +| `keycloak` | `realm` | **Optional**. Name of Keycloak realm to use. Should be setup only if `install` is `false` and you want to reuse an existing realm. Default is `microcks`. | +| `keycloak` | `url` | **Mandatory**. The URL of Keycloak install - indeed just the hostname + port part - if it already exists or the one used for exposing Keycloak `Ingress`. | +| `keycloak` | `privateUrl` | **Optional**. A private URL - a full URL here - used by the Microcks component to internally join Keycloak. This is also known as `backendUrl` in [Keycloak doc](https://www.keycloak.org/docs/latest/server_installation/#_hostname). When specified, the `keycloak.url` is used as `frontendUrl` in Keycloak terms. | +| `keycloak` | `ingressSecretRef` | **Optional**. The name of a TLS Secret for securing `Ingress`. If missing, self-signed certificate is generated. | +| `keycloak` | `ingressAnnotations` | **Optional**. A map of annotations that will be added to the `Ingress` or `HTTPRoute` for Keycloak pod. If these annotations are triggering a Certificate generation (for example through [cert-mamanger.io](https://cert-manager.io/)). The `generateCert` property should be set to `false`. | +| `keycloak` | `generateCert` | **Optional**. Whether to generate self-signed certificate or not if no valid `ingressSecretRef` provided. Default is `true` | +| `keycloak` | `gatewayRefName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `keycloak` | `gatewayRefNamespace` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `keycloak` | `gatewayRefSectionName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `keycloak` | `secretRef` | **Optional**. Reference of a Secret containing credentials for connecting a provided Keycloak instance. Mandatory if `install` is `false`. | +| `keycloak` | `pvcAnnotations` | **Optional**. A map of annotations that will be added to the `pvc` for the Keycloak pod. | +| `keycloak` | `image` | **Optional**. The reference of container image used. Chart comes with its default version. | +| `keycloak` | `serviceType` | **Optional**. The service type used. Defaults to `ClusterIP`. | +| `keycloak` | `resources` | **Optional**. Some resources constraints to apply on Keycloak pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container). | +| `keycloak` | `persistent` | **Optional**. Flag for Keycloak persistence. Default is `true`. Set to `false` if you want an ephemeral Keycloak installation. | +| `keycloak` | `volumeSize` | **Optional**. Size of persistent volume claim for Keycloak. Default is `1Gi`. Not used if not persistent install asked. | +| `keycloak` | `storageClassName` | **Optional**. The cluster storage class to use for persistent volume claim. If not specified, we rely on cluster default storage class. | +| `keycloak` | `postgresImage` | **Optional**. The reference of container image used. Chart comes with its default version. | +| `keycloak` | `postgresResources` | **Optional**. Some resources constraints to apply on Keycloak Postgres pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container). | +| `keycloak` | `serviceAccount` | **Optional**. A service account to create into Microcks Keycloak realm. Default is `microcks-serviceaccount`. | +| `keycloak` | `serviceAccountCredentials` | **Optional**. The credentials of Keycloak realm service account for Microcks. Default is `ab54d329-e435-41ae-a900-ec6b3fe15c54`. | +| `mongodb` | `install` | **Optional**. Flag for MongoDB installation. Default is `true`. Set to `false` if you want to reuse an existing MongoDB instance. | +| `mongodb` | `uri` | **Optional**. MongoDB URI in case you're reusing existing MongoDB instance. Mandatory if `install` is `false`. | +| `mongodb` | `uriParameters` | **Optional**. Allows you to add parameters to the mongodb uri connection string. | +| `mongodb` | `database` | **Optional**. MongoDB database name in case you're reusing existing MongoDB instance. Used if `install` is `false`. Default to `appName`. | +| `mongodb` | `secretRef` | **Optional**. Reference of a Secret containing credentials for connecting a provided MongoDB instance. Mandatory if `install` is `false`. | +| `mongodb` | `pvcAnnotations` | **Optional**. A map of annotations that will be added to the `pvc` for the MongoDB pod. | +| `mongodb` | `resources` | **Optional**. Some resources constraints to apply on MongoDB pods. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container). | +| `mongodb` | `persistent` | **Optional**. Flag for MongoDB persistence. Default is `true`. Set to `false` if you want an ephemeral MongoDB installation. | +| `mongodb` | `volumeSize` | **Optional**. Size of persistent volume claim for MongoDB. Default is `2Gi`. Not used if not persistent install asked. | +| `mongodb` | `storageClassName` | **Optional**. The cluster storage class to use for persistent volume claim. If not specified, we rely on cluster default storage class. | +| `features` | `repositoryFilter` | **Optional**. Feature allowing to filter API and services on main page. Must be explicitly `enabled`. See [Organizing repository](https://microcks.io/documentation/using/advanced/organizing/#master-level-filter) for more information. | +| `features` | `repositoryTenancy` | **Optional**. Feature allowing to segment and delegate API and services management according the `repositoryFilter` master criteria. Must be explicitly `enabled`. See [Organizing repository](https://microcks.io/documentation/using/advanced/organizing/#rbac-security-segmentation) for more information. | +| `features` | `microcksHub.enabled` | **Optional**. Feature allowing to directly import mocks coming from `hub.microcks.io` marketplace. Default is `true`. See [Micorkcs Hub](https://microcks.io/documentation/using/advanced/microcks-hub) for more information. | +| `features` | `async.enabled` | **Optional**. Feature allowing to mock an tests asynchronous APIs through Events. Enabling it requires an active message broker. Default is `false`. | +| `features` | `async.image` | **Optional**. The reference of container image used for `async-minion` component. Chart comes with its default version. | +| `features` | `async.env` | **Optional**. Some environment variables to add on `async-minion` container. This should be expressed using [Kubernetes syntax](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/#define-an-environment-variable-for-a-container). | +| `features` | `aiCopilot` | **Optional**. Feature allowing to activate AI Copilot. Must be explicitly `enabled`. Default is `false`. | + +### AI Copilot feature + +Here are below the configuration properties of the AI Copilot feature: + +| Section | Property | Description | +|-----------------------------| ------------------ |-------------------------------------------------------------------------------------------------------------------------------------------| +| `features.aiCopilot` | `enabled` | **Optional**. Flag for enabling the feature. Default is `false`. Set to `true` to activate. | +| `features.aiCopilot` | `implementation` | **Optional**. Allow to choose an AI service implementation. Default is `openai` and its the only supported value at the moment. | +| `features.aiCopilot.openai` | `apiKey` | **Madantory** when enabled. Put here your OpenAI API key. | +| `features.aiCopilot.openai` | `timeout` | **Optional**. Allow the customization of the timeout when requesting OpenAI. Default to `20` seconds. | +| `features.aiCopilot.openai` | `model` | **Optional**. Allow the customization of the OpenAI model to use. Default may vary from one Microcks version to another. | +| `features.aiCopilot.openai` | `maxTokens` | **Optional**. Allow the customization of the maximum number of tokens that may be exchanged by OpenAI services. Default to `2000` tokens? | + +### Kafka feature details + +Here are below the configuration properties of the Kafka support feature: + +| Section | Property | Description | +| ------------------------------------- |----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.kafka` | `install` | **Optional**. Flag for Kafka installation. Default is `true` and require Strimzi Operator to be setup. Set to `false` if you want to reuse an existing Kafka instance. | +| `features.async.kafka` | `url` | **Optional**. The URL of Kafka broker if it already exists or the one used for exposing Kafka `Ingress` when we install it. In this later case, it should only be the subdomain part (eg: `apps.example.com`). | +| `features.async.kafka` | `ingressClassName` | **Optional**. The ingress class to use for exposing broker to the outer world when installing it. Default is `nginx`. | +| `features.async.kafka` | `persistent` | **Optional**. Flag for Kafka persistence. Default is `false`. Set to `true` if you want a persistent Kafka installation. | +| `features.async.kafka` | `volumeSize` | **Optional**. Size of persistent volume claim for Kafka. Default is `2Gi`. Not used if not persistent install asked. | +| `features.async.kafka.schemaRegistry` | `url` | **Optional**. The API URL of a Kafka Schema Registry. Used for Avro based serialization | +| `features.async.kafka.schemaRegistry` | `confluent` | **Optional**. Flag for indicating that registry is a Confluent one, or using a Confluent compatibility mode. Default to `true` | +| `features.async.kafka.schemaRegistry` | `username` | **Optional**. Username for connecting to the specified Schema registry. Default to `` | +| `features.async.kafka.schemaRegistry` | `credentialsSource` | **Optional**. Source of the credentials for connecting to the specified Schema registry. Default to `USER_INFO` | +| `features.async.kafka.authentication` | `type` | **Optional**. The type of authentication for connecting to a pre-existing Kafka broker. Supports `SSL` or `SASL_SSL`. Default to `none` | +| `features.async.kafka.authentication` | `truststoreType` | **Optional**. For TLS transport, you'll always need a truststore to hold your cluster certificate. Default to `PKCS12` | +| `features.async.kafka.authentication` | `truststoreSecretRef` | **Optional**. For TLS transport, the reference of a Secret holding truststore and its password. Set `secret`, `storeKey` and `passwordKey` properties | +| `features.async.kafka.authentication` | `keystoreType` | **Optional**. In case of `SSL` type, you'll also need a keystore to hold your user private key for mutual TLS authentication. Default to `PKCS12` | +| `features.async.kafka.authentication` | `keystoreSecretRef` | **Optional**. For mutual TLS authentication, the reference of a Secret holding keystore and its password. Set `secret`, `storeKey` and `passwordKey` properties | +| `features.async.kafka.authentication` | `saslMechanism` | **Optional**. For SASL authentication, you'll have to specify an additional authentication mechanism such as `SCRAM-SHA-512` or `OAUTHBEARER` | +| `features.async.kafka.authentication` | `saslJaasConfig` | **Optional**. For SASL authentication, you'll have to specify a JAAS configuration line with login module, username and password. | +| `features.async.kafka.authentication` | `saslLoginCallbackHandlerClass` | **Optional**. For SASL authentication, you may want to provide a Login Callback Handler implementations. This implementation may be provided by extending the main and `async-minion` images and adding your own libs. | + +#### MQTT feature details + +Here are below the configuration properties of the MQTT support feature: + +| Section | Property | Description | +| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `features.async.mqtt` | `url` | **Optional**. The URL of MQTT broker (eg: `my-mqtt-broker.example.com:1883`). Default is undefined which means that feature is disabled. | +| `features.async.mqtt` | `username` | **Optional**. The username to use for connecting to secured MQTT broker. Default to `microcks`. | +| `features.async.mqtt` | `password` | **Optional**. The password to use for connecting to secured MQTT broker. Default to `microcks`. | + +#### WebSocket feature details + +Here are below the configuration properties of the WebSocket support feature: + +| Section | Property | Description | +| ------------------- |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.ws` | `ingressSecretRef` | **Optional**. The name of a TLS Secret for securing WebSocket `Ingress`. If missing, self-signed certificate is generated. | +| `features.async.ws` | `ingressAnnotations` | **Optional**. A map of annotations that will be added to the `Ingress` for Microcks WebSocket mocks. If these annotations are triggering a Certificate generation (for example through [cert-mamanger.io](https://cert-manager.io/)). The `generateCert` property should be set to `false`.| +| `features.async.ws` | `generateCert` | **Optional**. Whether to generate self-signed certificate or not if no valid `ingressSecretRef` provided. Default is `true` | +| `features.async.ws` | `gatewayRefName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `features.async.ws` | `gatewayRefNamespace` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | +| `features.async.ws` | `gatewayRefSectionName` | **Optional**. Overrides the parameter defined at `` level if defined. Default is `undefined`. | + +#### AMQP feature details + +Here are below the configuration properties of the AMQP support feature: + +| Section | Property | Description | +| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `features.async.amqp` | `url` | **Optional**. The URL of AMQP broker (eg: `my-amqp-broker.example.com:5672`). Default is undefined which means that feature is disabled. | +| `features.async.amqp` | `username` | **Optional**. The username to use for connecting to secured AMQP broker. Default to `microcks`. | +| `features.async.amqp` | `password` | **Optional**. The password to use for connecting to secured AMQP broker. Default to `microcks`. | + +#### NATS feature details + +Here are below the configuration properties of the NATS support feature: + +| Section | Property | Description | +| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `features.async.nats` | `url` | **Optional**. The URL of NATS broker (eg: `my-nats-broker.example.com:4222`). Default is undefined which means that feature is disabled. | +| `features.async.nats` | `username` | **Optional**. The username to use for connecting to secured NATS broker. Default to `microcks`. | +| `features.async.nats` | `password` | **Optional**. The password to use for connecting to secured NATS broker. Default to `microcks`. | + +#### Google PubSub feature details + +Here are below the configuration properties of the Google PubSub support feature: + +| Section | Property | Description | +|-------------------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.googlepubsub` | `project` | **Optional**. The GCP project id of PubSub (eg: `my-gcp-project-347219`). Default is undefined which means that feature is disabled. | +| `features.async.googlepubsub` | `serviceAccountSecretRef` | **Optional**. The name of a Generic Secret holding Service Account JSON credentiels. Set `secret` and `fileKey` properties. | + +#### Amazon SQS feature details + +Here are below the configuration properties of the Amazon SQS support feature: + +| Section | Property | Description | +|----------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.sqs` | `region` | **Optional**. The AWS region for connecting SQS service (eg: `eu-west-3`). Default is undefined which means that feature is disabled. | +| `features.async.sqs` | `credentialsType` | **Optional**. The type of credentials we use for authentication. 2 options here `env-variable` or `profile`. Default to `env-variable`. | +| `features.async.sqs` | `credentialsProfile` | **Optional**. When using `profile` authent, name of profile to use for authenticating to SQS. This profile should be present into a credentials file mounted from a Secret (see below). Default to `microcks-sqs-admin`. | +| `features.async.sqs` | `credentialsSecretRef` | **Optional**. The name of a Generic Secret holding either environment variables (set `secret` and `accessKeyIdKey`, `secretAccessKeyKey` and optional `sessionTokenKey` properties) or an AWS credentials file with referenced profile (set `secret` and `fileKey` properties). | +| `features.async.sqs` | `endpointOverride` | **Optional**. The AWS endpoint URI used for API calls. Handy for using SQS via [LocalStack](https://localstack.cloud). | + +#### Amazon SNS feature details + +Here are below the configuration properties of the Amazon SNS support feature: + +| Section | Property | Description | +|----------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `features.async.sns` | `region` | **Optional**. The AWS region for connecting SNS service (eg: `eu-west-3`). Default is undefined which means that feature is disabled. | +| `features.async.sns` | `credentialsType` | **Optional**. The type of credentials we use for authentication. 2 options here `env-variable` or `profile`. Default to `env-variable`. | +| `features.async.sns` | `credentialsProfile` | **Optional**. When using `profile` authent, name of profile to use for authenticating to SQS. This profile should be present into a credentials file mounted from a Secret (see below). Default to `microcks-sns-admin`. | +| `features.async.sns` | `credentialsSecretRef` | **Optional**. The name of a Generic Secret holding either environment variables (set `secret` and `accessKeyIdKey`, `secretAccessKeyKey` and optional `sessionTokenKey` properties) or an AWS credentials file with referenced profile (set `secret` and `fileKey` properties). | +| `features.async.sns` | `endpointOverride` | **Optional**. The AWS endpoint URI used for API calls. Handy for using SNS via [LocalStack](https://localstack.cloud). | + +> **Note:** Enabling both SQS and SNS features and using `env-variable` credentials type for both, may lead to collision as both clients rely on the +> same environment variables. So you have to specify `credentialsSecretRef` on only one of those two services and be sure that the access key and secret +> access key mounted refers to a IAM account having write access to both services. + +### Examples + +You may want to launch custom installation with such a command: + + ```console + $ helm install microcks ./microcks --namespace=microcks \ + --set appName=mocks --set mongodb.volumeSize=5Gi \ + --set microcks.url=mocks-microcks.apps.example.com \ + --set keycloak.url=keycloak-microcks.apps.example.com + ``` + +or - with included Kafka for async mocking turned on: + + ```console + $ helm install microcks ./microcks --namespace=microcks \ + --set appName=microcks --set features.async.enabled=true \ + --set microcks.url=microcks.$(minikube ip).nip.io \ + --set keycloak.url=keycloak.$(minikube ip).nip.io \ + --set features.async.kafka.url=$(minikube ip).nip.io + ``` + +## Checking everything is OK + +Just check you've got this 5 running pods: + +```console +$ kubectl get pods -n microcks +NAME READY STATUS RESTARTS AGE +microcks-7f8445887d-f7wt9 1/1 Running 0 39s +microcks-keycloak-bbbfcb-8flrr 1/1 Running 0 39s +microcks-keycloak-postgresql-6dc77c4968-5dcjd 1/1 Running 0 39s +microcks-mongodb-6d558666dc-zdhxl 1/1 Running 0 39s +microcks-postman-runtime-58bf695b59-nm858 1/1 Running 0 39s +``` + +## Deleting the Chart + +```console +helm delete microcks +helm del --purge microcks +``` diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/NOTES.txt b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/NOTES.txt new file mode 100644 index 000000000..e891ee5b8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/NOTES.txt @@ -0,0 +1,32 @@ +Thank you for installing {{ .Chart.Name }}. + +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm status {{ .Release.Name }} + $ helm get {{ .Release.Name }} + +Microcks is available at https://{{ .Values.microcks.url }}. + +GRPC mock service is available at {{ ( include "microcks-grpc.url" . ) }}. +{{- if (.Values.microcks.grpcEnableTLS) }} +It has been exposed using TLS passthrough on the Ingress controller, you should extract the certificate for your client using: + + $ kubectl get secret {{ .Values.appName }}-microcks-grpc-secret -n {{ .Release.Namespace }} -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt +{{ end }} + +{{- if and .Values.keycloak.enabled .Values.keycloak.install }} +Keycloak has been deployed on https://{{ .Values.keycloak.url }} to protect user access. +You may want to configure an Identity Provider or add some users for your Microcks installation by login in using the +username and password found into '{{ .Values.appName }}-keycloak-admin' secret. +{{- end }} + +{{- if and .Values.features.async.enabled .Values.features.async.kafka.install }} + +Kafka broker has been deployed on {{ .Values.appName }}-kafka.{{ .Values.features.async.kafka.url }}. +It has been exposed using TLS passthrough on the Ingress controller, you should extract the certificate for your client using: + + $ kubectl get secret {{ .Values.appName }}-kafka-cluster-ca-cert -n {{ .Release.Namespace }} -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt + +{{- end }} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/_helpers.tpl b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/_helpers.tpl new file mode 100644 index 000000000..e848d919e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/_helpers.tpl @@ -0,0 +1,125 @@ +{{/* +Generate certificates for microcks ingress +*/}} +{{- define "microcks-ingress.gen-certs" -}} +{{- $cert := genSelfSignedCert .Values.microcks.url nil (list .Values.microcks.url) 365 -}} +tls.crt: {{ $cert.Cert | b64enc }} +tls.key: {{ $cert.Key | b64enc }} +{{- end -}} + + +{{/* +Produce GRPC Ingress URL +*/}} +{{- define "microcks-grpc.url" -}} +"{{ regexReplaceAll "^([^.-]+)(.*)" .Values.microcks.url "${1}-grpc${2}" }}" +{{- end -}} + + +{{/* +Generate certificates for microcks GRPC service +*/}} +{{- define "microcks-grpc.gen-certs" -}} +{{- $grpcUrl := regexReplaceAll "^([^.-]+)(.*)" .Values.microcks.url "${1}-grpc${2}" -}} +{{- $grpcSvc := print .Values.appName "-grpc." .Release.Namespace ".svc.cluster.local" -}} +{{- $cert := genSelfSignedCert .Values.microcks.url nil (list $grpcUrl $grpcSvc "localhost") 3650 -}} +tls.crt: {{ $cert.Cert | b64enc }} +tls.key: {{ $cert.Key | b64enc }} +{{- end -}} + + +{{/* +Generate certificates for keycloak ingress +*/}} +{{- define "microcks.keycloak-ingress.gen-certs" -}} +{{- $cert := genSelfSignedCert .Values.keycloak.url nil (list .Values.keycloak.url) 365 -}} +tls.crt: {{ $cert.Cert | b64enc }} +tls.key: {{ $cert.Key | b64enc }} +{{- end -}} + + +{{/* +Produce WS Ingress URL +*/}} +{{- define "microcks-ws.url" -}} +{{ regexReplaceAll "^([^.-]+)(.*)" .Values.microcks.url "${1}-ws${2}" }} +{{- end -}} + +{{/* +Generate certificates for microcks WS ingress +*/}} +{{- define "microcks-ws-ingress.gen-certs" -}} +{{- $wsUrl := regexReplaceAll "^([^.-]+)(.*)" .Values.microcks.url "${1}-ws${2}" -}} +{{- $cert := genSelfSignedCert $wsUrl nil (list $wsUrl "localhost") 3650 -}} +tls.crt: {{ $cert.Cert | b64enc }} +tls.key: {{ $cert.Key | b64enc }} +{{- end -}} + +{{/* +Generate common labels +*/}} +{{- define "microcks-common-labels" -}} +{{- range $name, $value := .Values.commonLabels }} +{{ $name }}: {{ $value | quote }} +{{- end -}} +{{- end -}} + +{{/* +Generate common annotations +*/}} +{{- define "microcks-common-annotations" -}} +{{- range $name, $value := .Values.commonAnnotations }} +{{ $name }}: {{ $value | quote }} +{{- end -}} +{{- end -}} + +{{/* +Create image name value +( dict "imageRoot" .Values.path.to.image "context" $ ) +*/}} +{{- define "microcks.renderImage" -}} +{{- $ref := join "/" (compact (list .imageRoot.registry .imageRoot.repository)) -}} +{{- join "" (compact (list $ref (ternary ":" "@" (empty .imageRoot.digest)) (default .imageRoot.tag .imageRoot.digest))) -}} +{{- end -}} + +{{/* +Compute microcks image full name +*/}} +{{- define "microcks.image" -}} +{{- include "microcks.renderImage" ( dict "imageRoot" .Values.microcks.image "context" $ ) -}} +{{- end -}} + +{{/* +Compute postman image full name +*/}} +{{- define "microcks.postman.image" -}} +{{- include "microcks.renderImage" ( dict "imageRoot" .Values.postman.image "context" $ ) -}} +{{- end -}} + +{{/* +Compute mongodb image full name +*/}} +{{- define "microcks.mongodb.image" -}} +{{- include "microcks.renderImage" ( dict "imageRoot" .Values.mongodb.image "context" $ ) -}} +{{- end -}} + +{{/* +Compute keycloak image full name +*/}} +{{- define "microcks.keycloak.image" -}} +{{- include "microcks.renderImage" ( dict "imageRoot" .Values.keycloak.image "context" $ ) -}} +{{- end -}} + +{{/* +Compute keycloak postgres image full name +*/}} +{{- define "microcks.keycloakPostgres.image" -}} +{{- include "microcks.renderImage" ( dict "imageRoot" .Values.keycloak.postgresImage "context" $ ) -}} +{{- end -}} + +{{/* +Compute async-minion image full name +*/}} +{{- define "microcks.async-minion.image" -}} +{{- include "microcks.renderImage" ( dict "imageRoot" .Values.features.async.image "context" $ ) -}} +{{- end -}} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/claim.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/claim.yaml new file mode 100644 index 000000000..6f88586e7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/claim.yaml @@ -0,0 +1,52 @@ +{{- if and .Values.mongodb.install .Values.mongodb.persistent }} +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-mongodb" + labels: + app: "{{ .Values.appName }}" + container: mongodb + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + {{- with .Values.mongodb.pvcAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.mongodb.volumeSize }} + {{- if hasKey .Values.mongodb "storageClassName" }} + storageClassName: {{ .Values.mongodb.storageClassName }} + {{- end }} +{{- end }} +{{- if and .Values.keycloak.enabled .Values.keycloak.install .Values.keycloak.persistent }} +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-keycloak-postgresql" + labels: + app: "{{ .Values.appName }}" + container: keycloak-postgresql + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + {{- with .Values.keycloak.pvcAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.keycloak.volumeSize }} + {{- if hasKey .Values.keycloak "storageClassName" }} + storageClassName: {{ .Values.keycloak.storageClassName }} + {{- end }} +{{- end }} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/configmap.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/configmap.yaml new file mode 100644 index 000000000..4f4acec37 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/configmap.yaml @@ -0,0 +1,612 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-config" + labels: + app: "{{ .Values.appName }}" + container: spring + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +data: + features.properties: |- + features.feature.microcks-hub.enabled={{ .Values.features.microcksHub.enabled }} + features.feature.microcks-hub.endpoint=https://hub.microcks.io/api + features.feature.microcks-hub.allowed-roles={{ .Values.features.microcksHub.allowedRoles }} + + features.feature.repository-filter.enabled={{ .Values.features.repositoryFilter.enabled }} + features.feature.repository-filter.label-key={{ .Values.features.repositoryFilter.labelKey }} + features.feature.repository-filter.label-label={{ .Values.features.repositoryFilter.labelLabel }} + features.feature.repository-filter.label-list={{ .Values.features.repositoryFilter.labelList }} + + features.feature.repository-tenancy.enabled={{ .Values.features.repositoryTenancy.enabled }} + features.feature.repository-tenancy.artifact-import-allowed-roles={{ .Values.features.repositoryTenancy.artifactImportAllowedRoles }} + + features.feature.async-api.enabled={{ .Values.features.async.enabled }} + features.feature.async-api.default-binding={{ .Values.features.async.defaultBinding }} + features.feature.async-api.endpoint-WS={{ ( include "microcks-ws.url" . ) }} + {{- if eq .Values.features.async.kafka.install true }} + features.feature.async-api.endpoint-KAFKA={{ .Values.appName }}-kafka.{{ .Values.features.async.kafka.url }}:443 + {{- else }} + features.feature.async-api.endpoint-KAFKA={{ .Values.features.async.kafka.url }} + {{- end }} + {{- if .Values.features.async.mqtt.url }} + features.feature.async-api.endpoint-MQTT={{ .Values.features.async.mqtt.url }} + {{- end }} + {{- if .Values.features.async.amqp.url }} + features.feature.async-api.endpoint-AMQP={{ .Values.features.async.amqp.url }} + {{- end }} + {{- if .Values.features.async.nats.url }} + features.feature.async-api.endpoint-NATS={{ .Values.features.async.nats.url }} + {{- end }} + {{- if .Values.features.async.googlepubsub.project }} + features.feature.async-api.endpoint-GOOGLEPUBSUB={{ .Values.features.async.googlepubsub.project }} + {{- end }} + {{- if .Values.features.async.sqs.region }} + features.feature.async-api.endpoint-SQS={{ .Values.features.async.sqs.region }}{{ if .Values.features.async.sqs.endpointOverride }} at {{ .Values.features.async.sqs.endpointOverride }}{{ end }} + {{- end }} + {{- if .Values.features.async.sns.region }} + features.feature.async-api.endpoint-SNS={{ .Values.features.async.sns.region }}{{ if .Values.features.async.sns.endpointOverride }} at {{ .Values.features.async.sns.endpointOverride }}{{ end }} + {{- end }} + + features.feature.ai-copilot.enabled={{ .Values.features.aiCopilot.enabled }} + application.properties: |- + # Application configuration properties + tests-callback.url=${TEST_CALLBACK_URL} + postman-runner.url=${POSTMAN_RUNNER_URL} + async-minion.url=${ASYNC_MINION_URL|http://localhost:8081} + + network.username= + network.password= + + validation.resourceUrl=https://{{ .Values.microcks.url }}/api/resources/ + services.update.interval=${SERVICES_UPDATE_INTERVAL:0 0 0/2 * * *} + mocks.rest.enable-cors-policy=${ENABLE_CORS_POLICY:true} + {{- if eq .Values.microcks.mockInvocationStats false }} + mocks.enable-invocation-stats=false + {{- end }} + + # Logging configuration properties + logging.config=/deployments/config/logback.xml + + # Spring Security adapter configuration properties + spring.security.oauth2.client.registration.keycloak.client-id=microcks-app + spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code + spring.security.oauth2.client.registration.keycloak.scope=openid,profile + spring.security.oauth2.client.provider.keycloak.issuer-uri=${KEYCLOAK_URL}/realms/${keycloak.realm} + spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username + spring.security.oauth2.resourceserver.jwt.issuer-uri=${sso.public-url}/realms/${keycloak.realm} + {{- if hasKey .Values.keycloak "privateUrl" }} + spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${KEYCLOAK_URL}/realms/${keycloak.realm}/protocol/openid-connect/certs + {{- end }} + + # Keycloak configuration properties + keycloak.auth-server-url=${KEYCLOAK_URL} + keycloak.realm={{ .Values.keycloak.realm }} + keycloak.resource=microcks-app + keycloak.use-resource-role-mappings=true + keycloak.bearer-only=true + keycloak.ssl-required=external + keycloak.disable-trust-manager=true + + # Keycloak access configuration properties + sso.public-url=${KEYCLOAK_PUBLIC_URL:${keycloak.auth-server-url}} + + # Async mocking support. + async-api.enabled={{ .Values.features.async.enabled }} + async-api.default-binding={{ .Values.features.async.defaultBinding }} + async-api.default-frequency={{ .Values.features.async.defaultFrequency }} + + # Kafka configuration properties + spring.kafka.producer.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVER:localhost:9092} + {{- if eq .Values.features.async.kafka.install false }} + {{- if eq .Values.features.async.kafka.authentication.type "SSL" }} + spring.kafka.producer.properties.security.protocol=SSL + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + spring.kafka.producer.properties.ssl.truststore.location=/deployments/config/kafka/truststore/{{ .Values.features.async.kafka.authentication.truststoreSecretRef.storeKey }} + spring.kafka.producer.properties.ssl.truststore.password=${KAFKA_TRUSTSTORE_PASSWORD} + spring.kafka.producer.properties.ssl.truststore.type={{ .Values.features.async.kafka.authentication.truststoreType }} + {{- end }} + spring.kafka.producer.properties.ssl.keystore.location=/deployments/config/kafka/keystore/{{ .Values.features.async.kafka.authentication.keystoreSecretRef.storeKey }} + spring.kafka.producer.properties.ssl.keystore.password=${KAFKA_KEYSTORE_PASSWORD} + spring.kafka.producer.properties.ssl.keystore.type={{ .Values.features.async.kafka.authentication.keystoreType }} + {{- else if eq .Values.features.async.kafka.authentication.type "SASL_SSL" }} + spring.kafka.producer.properties.security.protocol=SASL_SSL + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + spring.kafka.producer.properties.ssl.truststore.location=/deployments/config/kafka/truststore/{{ .Values.features.async.kafka.authentication.truststoreSecretRef.storeKey }} + spring.kafka.producer.properties.ssl.truststore.password=${KAFKA_TRUSTSTORE_PASSWORD} + spring.kafka.producer.properties.ssl.truststore.type={{ .Values.features.async.kafka.authentication.truststoreType }} + {{- end }} + spring.kafka.producer.properties.sasl.mechanism={{ .Values.features.async.kafka.authentication.saslMechanism }} + spring.kafka.producer.properties.sasl.jaas.config={{ .Values.features.async.kafka.authentication.saslJaasConfig }} + {{- if .Values.features.async.kafka.authentication.saslLoginCallbackHandlerClass }} + spring.kafka.producer.properties.sasl.login.callback.handler.class={{ .Values.features.async.kafka.authentication.saslLoginCallbackHandlerClass }} + {{- end }} + {{- end }} + {{- end }} + + {{- if and (.Values.microcks.grpcEnableTLS) (not .Values.gatewayRoutes) }} + + # Grpc server properties + grpc.server.certChainFilePath=/deployments/config/grpc/tls.crt + grpc.server.privateKeyFilePath=/deployments/config/grpc/tls.key + {{- end }} + + # AI Copilot configuration properties + ai-copilot.enabled={{ .Values.features.aiCopilot.enabled }} + ai-copilot.implementation={{ .Values.features.aiCopilot.implementation }} + {{- if eq .Values.features.aiCopilot.implementation "openai" }} + ai-copilot.openai.api-key={{ .Values.features.aiCopilot.openai.apiKey }} + {{- if .Values.features.aiCopilot.openai.timeout }} + ai-copilot.openai.timeout={{ .Values.features.aiCopilot.openai.timeout }} + {{- end }} + {{- if .Values.features.aiCopilot.openai.model }} + ai-copilot.openai.model={{ .Values.features.aiCopilot.openai.model }} + {{- end }} + {{- if .Values.features.aiCopilot.openai.maxTokens }} + ai-copilot.openai.maxTokens={{ .Values.features.aiCopilot.openai.maxTokens }} + {{- end }} + {{- if .Values.features.aiCopilot.openai.apiUrl }} + ai-copilot.openai.api-url={{ .Values.features.aiCopilot.openai.apiUrl }} + {{- end }} + {{- end }} + logback.xml: |- + + + + + + + + + utf-8 + %d{HH:mm:ss.SSS} [%p] %c - %m%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + +{{- if .Values.microcks.extraProperties }} + application-extra.yaml: |- + {{- with .Values.microcks.extraProperties }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +{{- if (.Values.mongodb.install) }} +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-mongodb-init" + labels: + app: "{{ .Values.appName }}" + container: mongodb + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +data: + create-user.sh: |- + #!/bin/bash + echo "Started Adding the Users..." + mongo admin --eval "db.getSiblingDB('${MONGO_INITDB_DATABASE}').createUser({user: '${MONGODB_USER}', pwd: '${MONGODB_PASSWORD}', roles: [{role: 'readWrite', db: '${MONGO_INITDB_DATABASE}'}]})" + echo "End Adding the User Roles." +{{- end -}} +{{- if and .Values.keycloak.enabled .Values.keycloak.install }} +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-keycloak-config" + labels: + app: "{{ .Values.appName }}" + container: keycloak + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +data: + microcks-realm.json: |- + { + "id": "microcks", + "realm": "microcks", + "displayName": "Microcks", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "users" : [ + { + "username" : "user", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "microcks123" } + ], + "realmRoles": [], + "applicationRoles": { + "microcks-app": [ "user" ] + } + }, + { + "username" : "manager", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "microcks123" } + ], + "realmRoles": [], + "applicationRoles": { + "microcks-app": [ "user", "manager" ] + } + }, + { + "username" : "admin", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "microcks123" } + ], + "realmRoles": [], + "applicationRoles": { + "realm-management": [ "manage-users", "manage-clients" ], + "account": [ "manage-account" ], + "microcks-app": [ "user", "manager", "admin" ] + } + } + ], + "roles": { + "realm": [], + "client": { + "microcks-app": [ + { + "name": "user", + "composite": false, + "clientRole": true, + "containerId": "microcks" + }, + { + "name": "admin", + "composite": false, + "clientRole": true, + "containerId": "microcks" + }, + { + "name": "manager", + "composite": false, + "clientRole": true, + "containerId": "microcks" + } + ] + } + }, + "groups": [ + { + "name": "microcks", + "path": "/microcks", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [ + { + "name": "manager", + "path": "/microcks/manager", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + } + ] + } + ], + "defaultRoles": [ ], + "requiredCredentials": [ "password" ], + "scopeMappings": [], + "clientScopeMappings": { + "microcks-app": [ + { + "client": "microcks-app-js", + "roles": [ + "manager", + "admin", + "user" + ] + } + ], + "realm-management": [ + { + "client": "microcks-app-js", + "roles": [ + "manage-users", + "manage-clients" + ] + } + ] + }, + "clients": [ + { + "clientId": "microcks-app-js", + "enabled": true, + "publicClient": true, + "redirectUris": [ + "https://{{ .Values.microcks.url }}/*", + "http://localhost:58085/*" + ], + "webOrigins": [ + "+" + ], + "fullScopeAllowed": false, + "protocolMappers": [ + { + "name": "microcks-group-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "microcks-groups", + "userinfo.token.claim": "true" + } + } + ] + } + ], + "applications": [ + { + "name": "microcks-app", + "enabled": true, + "bearerOnly": true, + "defaultRoles": [ + "user" + ] + }, + { + "name": "{{ .Values.keycloak.serviceAccount }}", + "secret": "{{ .Values.keycloak.serviceAccountCredentials }}", + "enabled": true, + "bearerOnly": false, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "clientAuthenticatorType": "client-secret" + } + ], + "identityProviders": [ + ], + "requiredActions": [ + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": false, + "defaultAction": false, + "priority": 90, + "config": {} + } + ], + "keycloakVersion": "10.0.1" + } +{{- end }} +{{- if .Values.features.async.enabled }} +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-async-minion-config" + labels: + app: "{{ .Values.appName }}" + container: async-minion + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +data: + application.properties: |- + # Configuration file. + %kube.quarkus.http.port=8080 + + # Configure the log level. + %kube.quarkus.log.level={{ .Values.microcks.logLevel }} + %kube.quarkus.log.console.level={{ .Values.microcks.logLevel }} + + # Access to Microcks API server. + %kube.io.github.microcks.minion.async.client.MicrocksAPIConnector/mp-rest/url=http://{{ .Values.appName }}:8080 + %kube.microcks.serviceaccount={{ .Values.keycloak.serviceAccount }} + %kube.microcks.serviceaccount.credentials={{ .Values.keycloak.serviceAccountCredentials }} + + {{ if hasKey .Values.keycloak "privateUrl" -}} + # Access to Keycloak URL if you override the one coming from Microcks config + keycloak.auth.url={{ .Values.keycloak.privateUrl }} + {{- end }} + + # Access to Kafka broker. + {{- if .Values.features.async.kafka.install }} + %kube.kafka.bootstrap.servers={{ .Values.appName }}-kafka-kafka-bootstrap:9092 + %kube.mp.messaging.incoming.microcks-services-updates.bootstrap.servers={{ .Values.appName }}-kafka-kafka-bootstrap:9092 + {{ else }} + %kube.kafka.bootstrap.servers={{ .Values.features.async.kafka.url }} + %kube.mp.messaging.incoming.microcks-services-updates.bootstrap.servers={{ .Values.features.async.kafka.url }} + + {{- if eq .Values.features.async.kafka.authentication.type "SSL" }} + %kube.kafka.security.protocol=SSL + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + %kube.kafka.ssl.truststore.location=/deployments/config/kafka/truststore/{{ .Values.features.async.kafka.authentication.truststoreSecretRef.storeKey }} + %kube.kafka.ssl.truststore.password=${KAFKA_TRUSTSTORE_PASSWORD} + %kube.kafka.ssl.truststore.type={{ .Values.features.async.kafka.authentication.truststoreType }} + {{- end }} + %kube.kafka.ssl.keystore.location=/deployments/config/kafka/keystore/{{ .Values.features.async.kafka.authentication.keystoreSecretRef.storeKey }} + %kube.kafka.ssl.keystore.password=${KAFKA_KEYSTORE_PASSWORD} + %kube.kafka.ssl.keystore.type={{ .Values.features.async.kafka.authentication.keystoreType }} + + %kube.mp.messaging.incoming.microcks-services-updates.security.protocol=SSL + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.truststore.location=/deployments/config/kafka/truststore/{{ .Values.features.async.kafka.authentication.truststoreSecretRef.storeKey }} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.truststore.password=${KAFKA_TRUSTSTORE_PASSWORD} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.truststore.type={{ .Values.features.async.kafka.authentication.truststoreType }} + {{- end }} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.keystore.location=/deployments/config/kafka/keystore/{{ .Values.features.async.kafka.authentication.keystoreSecretRef.storeKey }} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.keystore.password=${KAFKA_KEYSTORE_PASSWORD} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.keystore.type={{ .Values.features.async.kafka.authentication.keystoreType }} + {{- else if eq .Values.features.async.kafka.authentication.type "SASL_SSL" }} + %kube.kafka.security.protocol=SASL_SSL + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + %kube.kafka.ssl.truststore.location=/deployments/config/kafka/truststore/{{ .Values.features.async.kafka.authentication.truststoreSecretRef.storeKey }} + %kube.kafka.ssl.truststore.password=${KAFKA_TRUSTSTORE_PASSWORD} + %kube.kafka.ssl.truststore.type={{ .Values.features.async.kafka.authentication.truststoreType }} + {{- end }} + %kube.kafka.sasl.mechanism={{ .Values.features.async.kafka.authentication.saslMechanism }} + %kube.kafka.sasl.jaas.config={{ .Values.features.async.kafka.authentication.saslJaasConfig }} + {{- if .Values.features.async.kafka.authentication.saslLoginCallbackHandlerClass }} + %kube.kafka.sasl.login.callback.handler.class={{ .Values.features.async.kafka.authentication.saslLoginCallbackHandlerClass }} + {{- end }} + + %kube.mp.messaging.incoming.microcks-services-updates.security.protocol=SASL_SSL + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.truststore.location=/deployments/config/kafka/truststore/{{ .Values.features.async.kafka.authentication.truststoreSecretRef.storeKey }} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.truststore.password=${KAFKA_TRUSTSTORE_PASSWORD} + %kube.mp.messaging.incoming.microcks-services-updates.ssl.truststore.type={{ .Values.features.async.kafka.authentication.truststoreType }} + {{- end }} + %kube.mp.messaging.incoming.microcks-services-updates.sasl.mechanism={{ .Values.features.async.kafka.authentication.saslMechanism }} + %kube.mp.messaging.incoming.microcks-services-updates.sasl.jaas.config={{ .Values.features.async.kafka.authentication.saslJaasConfig }} + {{- if .Values.features.async.kafka.authentication.saslLoginCallbackHandlerClass }} + %kube.mp.messaging.incoming.microcks-services-updates.sasl.login.callback.handler.class={{ .Values.features.async.kafka.authentication.saslLoginCallbackHandlerClass }} + {{- end }} + {{- end }} + {{ end }} + + {{- if .Values.features.async.kafka.schemaRegistry.url }} + # Access to Kafka schema registry. + %kube.kafka.schema.registry.url={{ .Values.features.async.kafka.schemaRegistry.url }} + %kube.kafka.schema.registry.confluent={{ .Values.features.async.kafka.schemaRegistry.confluent }} + %kube.kafka.schema.registry.username={{ .Values.features.async.kafka.schemaRegistry.username | default "" }} + %kube.kafka.schema.registry.credentials.source={{ .Values.features.async.kafka.schemaRegistry.credentialsSource | default "USER_INFO" }} + {{ end }} + + {{- if .Values.features.async.mqtt.url }} + # Access to MQTT broker. + %kube.mqtt.server={{ .Values.features.async.mqtt.url }} + %kube.mqtt.username={{ .Values.features.async.mqtt.username }} + %kube.mqtt.password={{ .Values.features.async.mqtt.password }} + {{- end }} + + {{- if .Values.features.async.amqp.url }} + # Access to AMQP broker. + %kube.amqp.server={{ .Values.features.async.amqp.url }} + %kube.amqp.username={{ .Values.features.async.amqp.username }} + %kube.amqp.password={{ .Values.features.async.amqp.password }} + {{- end }} + + {{- if .Values.features.async.nats.url }} + # Access to NATS broker. + %kube.nats.server={{ .Values.features.async.nats.url }} + %kube.nats.username={{ .Values.features.async.nats.username }} + %kube.nats.password={{ .Values.features.async.nats.password }} + {{- end }} + + {{- if .Values.features.async.googlepubsub.project }} + # Access to Google PubSub. + %kube.googlepubsub.project={{ .Values.features.async.googlepubsub.project }} + {{- if .Values.features.async.googlepubsub.serviceAccountSecretRef.fileKey }} + %kube.googlepubsub.service-account-location=/deployments/config/googlepubsub/sa/{{ .Values.features.async.googlepubsub.serviceAccountSecretRef.fileKey }} + {{- end }} + {{- end }} + + {{- if .Values.features.async.sqs.region }} + # Access to Amazon SQS. + %kube.amazonsqs.region={{ .Values.features.async.sqs.region }} + %kube.amazonsqs.credentials-type={{ .Values.features.async.sqs.credentialsType }} + {{- if eq .Values.features.async.sqs.credentialsType "profile" }} + %kube.amazonsqs.credentials-profile-name={{ .Values.features.async.sqs.credentialsProfile }} + {{- if .Values.features.async.sqs.credentialsSecretRef.fileKey }} + %kube.amazonsqs.credentials-profile-location=/deployments/config/amazon-sqs/{{ .Values.features.async.sqs.credentialsSecretRef.fileKey }} + {{- end }} + {{- end }} + {{- if .Values.features.async.sqs.endpointOverride }} + %kube.amazonsqs.endpoint-override={{ .Values.features.async.sqs.endpointOverride }} + {{- end }} + {{- end }} + + {{- if .Values.features.async.sns.region }} + # Access to Amazon SNS. + %kube.amazonsns.region={{ .Values.features.async.sns.region }} + %kube.amazonsns.credentials-type={{ .Values.features.async.sns.credentialsType }} + {{- if eq .Values.features.async.sns.credentialsType "profile" }} + %kube.amazonsns.credentials-profile-name={{ .Values.features.async.sns.credentialsProfile }} + {{- if .Values.features.async.sns.credentialsSecretRef.fileKey }} + %kube.amazonsns.credentials-profile-location=/deployments/config/amazon-sns/{{ .Values.features.async.sns.credentialsSecretRef.fileKey }} + {{- end }} + {{- end }} + {{- if .Values.features.async.sns.endpointOverride }} + %kube.amazonsns.endpoint-override={{ .Values.features.async.sns.endpointOverride }} + {{- end }} + {{- end }} + + # Configure the minion own behavioral properties. + %kube.minion.supported-bindings=KAFKA,WS{{ if .Values.features.async.mqtt.url }},MQTT{{ end }}{{ if .Values.features.async.amqp.url }},AMQP{{ end }}{{ if .Values.features.async.nats.url }},NATS{{ end }}{{ if .Values.features.async.googlepubsub.project }},GOOGLEPUBSUB{{ end }}{{ if .Values.features.async.sqs.region }},SQS{{ end }}{{ if .Values.features.async.sns.region }},SNS{{ end }} + %kube.minion.restricted-frequencies=3,10,30 + %kube.minion.default-avro-encoding={{ .Values.features.async.defaultAvroEncoding }} +{{- end -}} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/deployment.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/deployment.yaml new file mode 100644 index 000000000..fd0b0f517 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/deployment.yaml @@ -0,0 +1,886 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Values.appName }}" + labels: + app: "{{ .Values.appName }}" + container: spring + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +spec: + replicas: {{ .Values.microcks.replicas }} + selector: + matchLabels: + app: "{{ .Values.appName }}" + deploymentconfig: "{{ .Values.appName }}" + container: spring + group: microcks + template: + metadata: + labels: + app: "{{ .Values.appName }}" + deploymentconfig: "{{ .Values.appName }}" + container: spring + group: microcks + {{- include "microcks-common-labels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- include "microcks-common-annotations" . | nindent 8 }} + spec: + containers: + - name: spring + image: {{ include "microcks.image" . }} + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + - name: grpc + containerPort: 9090 + protocol: TCP + env: + - name: JAVA_OPTIONS + value: "-XX:+TieredCompilation -XX:TieredStopAtLevel=2" + - name: JAVA_MAJOR_VERSION + value: "11" + - name: SPRING_PROFILES_ACTIVE + value: prod{{ if .Values.microcks.extraProperties }},extra{{ end }} + - name: SPRING_DATA_MONGODB_URI + value: mongodb://${SPRING_DATA_MONGODB_USER}:${SPRING_DATA_MONGODB_PASSWORD}@{{ .Values.mongodb.uri | default (print .Values.appName "-mongodb:27017") }}/${SPRING_DATA_MONGODB_DATABASE}{{ .Values.mongodb.uriParameters }} + - name: SPRING_DATA_MONGODB_USER + valueFrom: + secretKeyRef: + {{- if hasKey .Values.mongodb "secretRef" }} + key: {{ .Values.mongodb.secretRef.usernameKey | default "username" }} + name: {{ .Values.mongodb.secretRef.secret | default (print .Values.appName "-mongodb-connection") }} + {{- else }} + key: username + name: "{{ .Values.appName }}-mongodb-connection" + {{- end }} + - name: SPRING_DATA_MONGODB_PASSWORD + valueFrom: + secretKeyRef: + {{- if hasKey .Values.mongodb "secretRef" }} + key: {{ .Values.mongodb.secretRef.passwordKey | default "password" }} + name: {{ .Values.mongodb.secretRef.secret | default (print .Values.appName "-mongodb-connection") }} + {{- else }} + key: password + name: "{{ .Values.appName }}-mongodb-connection" + {{- end }} + - name: SPRING_DATA_MONGODB_DATABASE + value: {{ .Values.mongodb.database | default .Values.appName }} + - name: POSTMAN_RUNNER_URL + value: http://{{ .Values.appName }}-postman-runtime:8080 + - name: TEST_CALLBACK_URL + value: http://{{ .Values.appName }}:8080 + - name: KEYCLOAK_ENABLED + value: "{{ .Values.keycloak.enabled }}" + {{- if hasKey .Values.keycloak "privateUrl" }} + - name: KEYCLOAK_URL + value: "{{ .Values.keycloak.privateUrl }}" + - name: KEYCLOAK_PUBLIC_URL + value: https://{{ .Values.keycloak.url }} + {{- else }} + - name: KEYCLOAK_URL + value: https://{{ .Values.keycloak.url }} + {{- end }} + {{- if and .Values.features.async.enabled }} + - name: ASYNC_MINION_URL + value: http://{{ .Values.appName }}-async-minion:8080 + {{- end }} + - name: KAFKA_BOOTSTRAP_SERVER + {{- if eq .Values.features.async.kafka.install true }} + value: "{{ .Values.appName }}-kafka-kafka-bootstrap:9092" + {{- else }} + value: "{{ .Values.features.async.kafka.url }}" + {{- end }} + {{- if eq .Values.features.async.kafka.install false }} + {{- if not (eq .Values.features.async.kafka.authentication.type "none") }} + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + - name: KAFKA_TRUSTSTORE_PASSWORD + valueFrom: + secretKeyRef: + key: {{ .Values.features.async.kafka.authentication.truststoreSecretRef.passwordKey }} + name: "{{ .Values.appName }}-kafka-truststore" + {{- end }} + {{- end }} + {{- if eq .Values.features.async.kafka.authentication.type "SSL" }} + - name: KAFKA_KEYSTORE_PASSWORD + valueFrom: + secretKeyRef: + key: {{ .Values.features.async.kafka.authentication.keystoreSecretRef.passwordKey }} + name: "{{ .Values.appName }}-kafka-keystore" + {{- end }} + {{- end }} + {{- toYaml .Values.microcks.env | nindent 10 }} + resources: + {{- toYaml .Values.microcks.resources | nindent 10 }} + livenessProbe: + httpGet: + path: "/api/health" + port: 8080 + scheme: HTTP + initialDelaySeconds: 25 + timeoutSeconds: 3 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/api/health" + port: 8080 + scheme: HTTP + initialDelaySeconds: 35 + timeoutSeconds: 3 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + startupProbe: + httpGet: + path: "/api/health" + port: 8080 + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 3 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 10 + volumeMounts: + - name: "{{ .Values.appName }}-config" + mountPath: "/deployments/config" + {{- if (.Values.microcks.grpcEnableTLS) }} + - name: "{{ .Values.appName }}-grpc-certs" + mountPath: "/deployments/config/grpc" + {{- end }} + {{- if eq .Values.features.async.kafka.install false }} + {{- if not (eq .Values.features.async.kafka.authentication.type "none") }} + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + - name: "{{ .Values.appName }}-kafka-truststore" + mountPath: "/deployments/config/kafka/truststore" + {{- end }} + {{- end }} + {{- if eq .Values.features.async.kafka.authentication.type "SSL" }} + - name: "{{ .Values.appName }}-kafka-keystore" + mountPath: "/deployments/config/kafka/keystore" + {{- end }} + {{- end }} + {{- if .Values.microcks.customSecretRef }} + - name: "{{ .Values.microcks.customSecretRef.secret }}" + mountPath: "/deployments/config/custom/secret" + {{- end}} + terminationMessagePath: "/dev/termination-log" + volumes: + - name: "{{ .Values.appName }}-config" + configMap: + name: "{{ .Values.appName }}-config" + {{- if (.Values.microcks.grpcEnableTLS) }} + - name: "{{ .Values.appName }}-grpc-certs" + secret: + secretName: "{{ .Values.microcks.grpcSecretRef | default (print .Values.appName "-microcks-grpc-secret") }}" + {{- end }} + {{- if eq .Values.features.async.kafka.install false }} + {{- if not (eq .Values.features.async.kafka.authentication.type "none") }} + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + - name: "{{ .Values.appName }}-kafka-truststore" + secret: + secretName: "{{ .Values.features.async.kafka.authentication.truststoreSecretRef.secret }}" + {{- end }} + {{- end }} + {{- if eq .Values.features.async.kafka.authentication.type "SSL" }} + - name: "{{ .Values.appName }}-kafka-keystore" + secret: + secretName: "{{ .Values.features.async.kafka.authentication.keystoreSecretRef.secret }}" + {{- end }} + {{- end }} + {{- if .Values.microcks.customSecretRef }} + - name: "{{ .Values.microcks.customSecretRef.secret }}" + secret: + secretName: "{{ .Values.microcks.customSecretRef.secret }}" + {{- end }} + {{- if .Values.commonAffinities }} + affinity: + {{- .Values.commonAffinities | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.commonTolerations }} + tolerations: + {{- .Values.commonTolerations | toYaml | nindent 8 }} + {{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Values.appName }}-postman-runtime" + labels: + app: "{{ .Values.appName }}" + container: postman-runtime + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + +spec: + replicas: 1 + selector: + matchLabels: + app: "{{ .Values.appName }}" + deploymentconfig: "{{ .Values.appName }}-postman-runtime" + container: postman-runtime + group: microcks + template: + metadata: + labels: + app: "{{ .Values.appName }}" + deploymentconfig: "{{ .Values.appName }}-postman-runtime" + container: postman-runtime + group: microcks + {{- include "microcks-common-labels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- include "microcks-common-annotations" . | nindent 8 }} + spec: + containers: + - name: postman-runtime + image: {{ include "microcks.postman.image" . }} + ports: + - name: http + containerPort: 3000 + protocol: TCP + env: + - name: LOG_LEVEL + value: info + resources: + {{- toYaml .Values.postman.resources | nindent 10 }} + livenessProbe: + httpGet: + path: "/health" + port: 3000 + scheme: HTTP + initialDelaySeconds: 4 + timeoutSeconds: 3 + periodSeconds: 20 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/health" + port: 3000 + scheme: HTTP + initialDelaySeconds: 5 + timeoutSeconds: 3 + periodSeconds: 20 + successThreshold: 1 + failureThreshold: 3 + terminationMessagePath: "/dev/termination-log" + imagePullPolicy: IfNotPresent + restartPolicy: Always + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst + {{- if .Values.commonAffinities }} + affinity: + {{- .Values.commonAffinities | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.commonTolerations }} + tolerations: + {{- .Values.commonTolerations | toYaml | nindent 8 }} + {{- end }} +{{- if and .Values.mongodb.install }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Values.appName }}-mongodb" + labels: + app: "{{ .Values.appName }}" + container: mongodb + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + + creationTimestamp: +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app: "{{ .Values.appName }}" + deploymentconfig: mongodb + container: mongodb + group: microcks + template: + metadata: + labels: + app: "{{ .Values.appName }}" + deploymentconfig: mongodb + container: mongodb + group: microcks + {{- include "microcks-common-labels" . | nindent 8 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 8 }} + spec: + {{- if not (.Capabilities.APIVersions.Has "route.openshift.io/v1/Route") }} + securityContext: + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + {{- end }} + containers: + - name: mongodb + image: {{ include "microcks.mongodb.image" . }} + args: ["--dbpath","/var/lib/mongodb/data"] + ports: + - name: mongodb + containerPort: 27017 + protocol: TCP + readinessProbe: + timeoutSeconds: 1 + initialDelaySeconds: 3 + exec: + command: + - "/bin/sh" + - "-i" + - "-c" + - mongo 127.0.0.1:27017/$MONGO_INITDB_DATABASE -u $MONGODB_USER -p $MONGODB_PASSWORD + --eval="quit()" + livenessProbe: + timeoutSeconds: 1 + initialDelaySeconds: 30 + tcpSocket: + port: 27017 + env: + - name: MONGODB_USER + valueFrom: + secretKeyRef: + key: username + name: "{{ .Values.appName }}-mongodb-connection" + - name: MONGODB_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: "{{ .Values.appName }}-mongodb-connection" + - name: MONGO_INITDB_ROOT_USERNAME + valueFrom: + secretKeyRef: + key: adminUsername + name: "{{ .Values.appName }}-mongodb-connection" + - name: MONGO_INITDB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + key: adminPassword + name: "{{ .Values.appName }}-mongodb-connection" + - name: MONGO_INITDB_DATABASE + value: "{{ .Values.appName }}" + resources: + {{- toYaml .Values.mongodb.resources | nindent 10 }} + volumeMounts: + - name: "{{ .Values.appName }}-mongodb-data" + mountPath: "/var/lib/mongodb/data" + - name: custom-init-scripts + mountPath: /docker-entrypoint-initdb.d + terminationMessagePath: "/dev/termination-log" + imagePullPolicy: IfNotPresent + securityContext: + capabilities: {} + privileged: false + volumes: + - name: "{{ .Values.appName }}-mongodb-data" + {{- if .Values.mongodb.persistent }} + persistentVolumeClaim: + claimName: "{{ .Values.appName }}-mongodb" + {{- else }} + emptyDir: + medium: '' + {{- end }} + - name: custom-init-scripts + configMap: + name: "{{ .Values.appName }}-mongodb-init" + restartPolicy: Always + dnsPolicy: ClusterFirst + {{- if .Values.commonAffinities }} + affinity: + {{- .Values.commonAffinities | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.commonTolerations }} + tolerations: + {{- .Values.commonTolerations | toYaml | nindent 8 }} + {{- end }} +{{- end }} +{{- if and .Values.keycloak.enabled .Values.keycloak.install }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Values.appName }}-keycloak" + labels: + app: "{{ .Values.appName }}" + container: keycloak + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app: "{{ .Values.appName }}" + deploymentconfig: keycloak + container: keycloak + group: microcks + template: + metadata: + labels: + app: "{{ .Values.appName }}" + deploymentconfig: keycloak + container: keycloak + group: microcks + {{- include "microcks-common-labels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- include "microcks-common-annotations" . | nindent 8 }} + spec: + containers: + - name: keycloak-server + image: {{ include "microcks.keycloak.image" . }} + resources: + {{- toYaml .Values.keycloak.resources | nindent 10 }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + - name: jolokia + containerPort: 8778 + protocol: TCP + args: + - 'start' + - '--features=token-exchange' + - '--db=$(KC_DB)' + - '--db-url-host=$(KC_DB_URL_HOST)' + - '--db-username=$(KC_DB_USER)' + - '--db-password=$(KC_DB_PASSWORD)' + {{- if hasKey .Values.keycloak "privateUrl" }} + - '--hostname=https://{{ .Values.keycloak.url }}' + - '--hostname-backchannel-dynamic=true' + {{- else }} + - '--hostname={{ .Values.keycloak.url }}' + {{- end }} + - '--health-enabled=true' + - '--import-realm' + env: + - name: INTERNAL_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: KEYCLOAK_ADMIN + valueFrom: + secretKeyRef: + {{- if hasKey .Values.keycloak "secretRef" }} + key: {{ .Values.keycloak.secretRef.usernameKey | default "username" }} + name: {{ .Values.keycloak.secretRef.secret | default (print .Values.appName "-keycloak-admin") }} + {{- else }} + key: username + name: "{{ .Values.appName }}-keycloak-admin" + {{- end }} + - name: KEYCLOAK_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + {{- if hasKey .Values.keycloak "secretRef" }} + key: {{ .Values.keycloak.secretRef.passwordKey | default "password" }} + name: {{ .Values.keycloak.secretRef.secret | default (print .Values.appName "-keycloak-admin") }} + {{- else }} + key: password + name: "{{ .Values.appName }}-keycloak-admin" + {{- end }} + - name: OPERATING_MODE + value: clustered + - name: KC_DB + value: postgres + - name: KC_DB_USER + valueFrom: + secretKeyRef: + {{- if hasKey .Values.keycloak "secretRef" }} + key: {{ .Values.keycloak.secretRef.postgresUsernameKey | default "postgresUsername" }} + name: {{ .Values.keycloak.secretRef.secret | default (print .Values.appName "-keycloak-admin") }} + {{- else }} + key: postgresUsername + name: "{{ .Values.appName }}-keycloak-admin" + {{- end }} + - name: KC_DB_PASSWORD + valueFrom: + secretKeyRef: + {{- if hasKey .Values.keycloak "secretRef" }} + key: {{ .Values.keycloak.secretRef.postgresPasswordKey | default "postgresPassword" }} + name: {{ .Values.keycloak.secretRef.secret | default (print .Values.appName "-keycloak-admin") }} + {{- else }} + key: postgresPassword + name: "{{ .Values.appName }}-keycloak-admin" + {{- end }} + - name: KC_DB_URL_DATABASE + value: root + - name: KC_DB_URL_HOST + value: "{{ .Values.appName }}-keycloak-postgresql" + - name: KC_PROXY + value: "edge" + livenessProbe: + httpGet: + path: "/health/live" + port: 9000 + scheme: HTTP + initialDelaySeconds: 30 + timeoutSeconds: 2 + periodSeconds: 20 + successThreshold: 1 + failureThreshold: 5 + readinessProbe: + httpGet: + path: "/health/ready" + port: 9000 + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 2 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 5 + volumeMounts: + - name: {{ .Values.appName }}-keycloak-config + mountPath: "/opt/keycloak/data/import" + securityContext: + privileged: false + volumes: + - name: "{{ .Values.appName }}-keycloak-config" + configMap: + name: "{{ .Values.appName }}-keycloak-config" + restartPolicy: Always + dnsPolicy: ClusterFirst + {{- if .Values.commonAffinities }} + affinity: + {{- .Values.commonAffinities | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.commonTolerations }} + tolerations: + {{- .Values.commonTolerations | toYaml | nindent 8 }} + {{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Values.appName }}-keycloak-postgresql" + labels: + app: "{{ .Values.appName }}" + container: keycloak-postgresql + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app: "{{ .Values.appName }}" + deploymentconfig: keycloak-postgresql + container: keycloak-postgresql + group: microcks + template: + metadata: + labels: + app: "{{ .Values.appName }}" + deploymentconfig: keycloak-postgresql + container: keycloak-postgresql + group: microcks + {{- include "microcks-common-labels" . | nindent 8 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 8 }} + spec: + terminationGracePeriodSeconds: 60 + containers: + - name: keycloak-postgresql + image: {{ include "microcks.keycloakPostgres.image" . }} + args: ["-c", "max_connections=100", "-c", "shared_buffers=12MB"] + imagePullPolicy: IfNotPresent + ports: + - name: postgres + containerPort: 5432 + protocol: TCP + readinessProbe: + timeoutSeconds: 1 + initialDelaySeconds: 5 + exec: + command: + - "/bin/sh" + - "-i" + - "-c" + - psql 127.0.0.1 -U ${POSTGRES_USER} -q -d ${POSTGRES_DB} -c 'SELECT 1' + livenessProbe: + timeoutSeconds: 1 + initialDelaySeconds: 30 + tcpSocket: + port: 5432 + resources: + {{- toYaml .Values.keycloak.postgresResources | nindent 10 }} + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + key: postgresUsername + name: "{{ .Values.appName }}-keycloak-admin" + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + key: postgresPassword + name: "{{ .Values.appName }}-keycloak-admin" + - name: POSTGRES_DB + value: root + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: "{{ .Values.appName }}-keycloak-postgresql-data" + mountPath: "/var/lib/postgresql/data" + subPath: pgdata + volumes: + - name: "{{ .Values.appName }}-keycloak-postgresql-data" + {{- if .Values.keycloak.persistent }} + persistentVolumeClaim: + claimName: "{{ .Values.appName }}-keycloak-postgresql" + {{- else }} + emptyDir: + medium: '' + {{- end }} + {{- if .Values.commonAffinities }} + affinity: + {{- .Values.commonAffinities | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.commonTolerations }} + tolerations: + {{- .Values.commonTolerations | toYaml | nindent 8 }} + {{- end }} +{{- end }} +{{- if and .Values.features.async.enabled }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Values.appName }}-async-minion" + labels: + app: "{{ .Values.appName }}" + container: async-minion + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app: "{{ .Values.appName }}" + deploymentconfig: async-minion + container: async-minion + group: microcks + template: + metadata: + labels: + app: "{{ .Values.appName }}" + deploymentconfig: async-minion + container: async-minion + group: microcks + {{- include "microcks-common-labels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- include "microcks-common-annotations" . | nindent 8 }} + spec: + containers: + - name: async-minion + image: {{ include "microcks.async-minion.image" . }} + imagePullPolicy: IfNotPresent + env: + - name: QUARKUS_PROFILE + value: kube + {{- if eq .Values.features.async.kafka.install false }} + {{- if not (eq .Values.features.async.kafka.authentication.type "none") }} + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + - name: KAFKA_TRUSTSTORE_PASSWORD + valueFrom: + secretKeyRef: + key: {{ .Values.features.async.kafka.authentication.truststoreSecretRef.passwordKey }} + name: "{{ .Values.appName }}-kafka-truststore" + {{- end }} + {{- end }} + {{- if eq .Values.features.async.kafka.authentication.type "SSL" }} + - name: KAFKA_KEYSTORE_PASSWORD + valueFrom: + secretKeyRef: + key: {{ .Values.features.async.kafka.authentication.keystoreSecretRef.passwordKey }} + name: "{{ .Values.appName }}-kafka-keystore" + {{- end }} + {{- end }} + {{- if .Values.features.async.sqs.region }} + {{- if eq .Values.features.async.sqs.credentialsType "env-variable" }} + {{- if .Values.features.async.sqs.credentialsSecretRef }} + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Values.features.async.sqs.credentialsSecretRef.secret }} + key: {{ .Values.features.async.sqs.credentialsSecretRef.accessKeyIdKey }} + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.features.async.sqs.credentialsSecretRef.secret }} + key: {{ .Values.features.async.sqs.credentialsSecretRef.secretAccessKeyKey }} + {{- if .Values.features.async.sqs.credentialsSecretRef.sessionTokenKey }} + - name: AWS_SESSION_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.features.async.sqs.credentialsSecretRef.secret }} + key: {{ .Values.features.async.sqs.credentialsSecretRef.sessionTokenKey }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.features.async.sns.region }} + {{- if eq .Values.features.async.sns.credentialsType "env-variable" }} + {{- if .Values.features.async.sns.credentialsSecretRef }} + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Values.features.async.sns.credentialsSecretRef.secret }} + key: {{ .Values.features.async.sns.credentialsSecretRef.accessKeyIdKey }} + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.features.async.sns.credentialsSecretRef.secret }} + key: {{ .Values.features.async.sns.credentialsSecretRef.secretAccessKeyKey }} + {{- if .Values.features.async.sns.credentialsSecretRef.sessionTokenKey }} + - name: AWS_SESSION_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.features.async.sns.credentialsSecretRef.secret }} + key: {{ .Values.features.async.sns.credentialsSecretRef.sessionTokenKey }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.features.async.env }} + {{- toYaml .Values.features.async.env | nindent 10 }} + {{- end }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: "/q/health/live" + port: 8080 + scheme: HTTP + initialDelaySeconds: 5 + timeoutSeconds: 2 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/q/health/ready" + port: 8080 + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 2 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + volumeMounts: + - name: "{{ .Values.appName }}-async-minion-config" + mountPath: "/deployments/config" + {{- if eq .Values.features.async.kafka.install false }} + {{- if not (eq .Values.features.async.kafka.authentication.type "none") }} + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + - name: "{{ .Values.appName }}-kafka-truststore" + mountPath: "/deployments/config/kafka/truststore" + {{- end }} + {{- end }} + {{- if eq .Values.features.async.kafka.authentication.type "SSL" }} + - name: "{{ .Values.appName }}-kafka-keystore" + mountPath: "/deployments/config/kafka/keystore" + {{- end }} + {{- end }} + {{- if .Values.features.async.googlepubsub.project }} + {{- if .Values.features.async.googlepubsub.serviceAccountSecretRef }} + - name: "{{ .Values.appName }}-googlepubsub-sa" + mountPath: "/deployments/config/googlepubsub/sa" + {{- end }} + {{- end }} + {{- if .Values.features.async.sqs.region }} + {{- if .Values.features.async.sqs.credentialsSecretRef }} + {{- if eq .Values.features.async.sqs.credentialsType "profile" }} + - name: "{{ .Values.appName }}-amazonsqs-creds" + mountPath: "/deployments/config/amazon-sqs" + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.features.async.sns.region }} + {{- if .Values.features.async.sns.credentialsSecretRef }} + {{- if eq .Values.features.async.sns.credentialsType "profile" }} + - name: "{{ .Values.appName }}-amazonsns-creds" + mountPath: "/deployments/config/amazon-sns" + {{- end }} + {{- end }} + {{- end }} + terminationMessagePath: "/dev/termination-log" + volumes: + - name: "{{ .Values.appName }}-async-minion-config" + configMap: + name: "{{ .Values.appName }}-async-minion-config" + {{- if eq .Values.features.async.kafka.install false }} + {{- if not (eq .Values.features.async.kafka.authentication.type "none") }} + {{- if .Values.features.async.kafka.authentication.truststoreSecretRef }} + - name: "{{ .Values.appName }}-kafka-truststore" + secret: + secretName: "{{ .Values.features.async.kafka.authentication.truststoreSecretRef.secret }}" + {{- end }} + {{- end }} + {{- if eq .Values.features.async.kafka.authentication.type "SSL" }} + - name: "{{ .Values.appName }}-kafka-keystore" + secret: + secretName: "{{ .Values.features.async.kafka.authentication.keystoreSecretRef.secret }}" + {{- end }} + {{- end }} + {{- if .Values.features.async.googlepubsub.project }} + {{- if .Values.features.async.googlepubsub.serviceAccountSecretRef }} + - name: "{{ .Values.appName }}-googlepubsub-sa" + secret: + secretName: "{{ .Values.features.async.googlepubsub.serviceAccountSecretRef.secret }}" + {{- end }} + {{- end }} + {{- if .Values.features.async.sqs.region }} + {{- if .Values.features.async.sqs.credentialsSecretRef }} + {{- if eq .Values.features.async.sqs.credentialsType "profile" }} + - name: "{{ .Values.appName }}-amazonsqs-creds" + secret: + secretName: "{{ .Values.features.async.sqs.credentialsSecretRef.secret }}" + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.features.async.sns.region }} + {{- if .Values.features.async.sns.credentialsSecretRef }} + {{- if eq .Values.features.async.sns.credentialsType "profile" }} + - name: "{{ .Values.appName }}-amazonsns-creds" + secret: + secretName: "{{ .Values.features.async.sns.credentialsSecretRef.secret }}" + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.commonAffinities }} + affinity: + {{- .Values.commonAffinities | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.commonTolerations }} + tolerations: + {{- .Values.commonTolerations | toYaml | nindent 8 }} + {{- end }} +{{- end }} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/gatewayroute.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/gatewayroute.yaml new file mode 100644 index 000000000..9a6b95307 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/gatewayroute.yaml @@ -0,0 +1,126 @@ +{{- if and (not .Values.ingresses) .Values.gatewayRoutes -}} +--- +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: "{{ .Values.appName }}" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + {{- with .Values.microcks.ingressAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + - name: {{ .Values.microcks.gatewayRefName | default .Values.gatewayRefName }} + {{- if or .Values.microcks.gatewayRefNamespace .Values.gatewayRefNamespace }} + namespace: {{ .Values.microcks.gatewayRefNamespace | default .Values.gatewayRefNamespace }} + {{- end }} + sectionName: {{ .Values.microcks.gatewayRefSectionName | default .Values.gatewayRefSectionName }} + hostnames: + - "{{ .Values.microcks.url }}" + rules: + - matches: + - path: + value: / + type: PathPrefix + backendRefs: + - name: "{{ .Values.appName }}" + port: 8080 +--- +kind: GRPCRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: "{{ .Values.appName }}-grpc" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + {{- with .Values.microcks.grpcIngressAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + - name: {{ .Values.microcks.grpcGatewayRefName | default .Values.gatewayRefName }} + {{- if or .Values.microcks.grpcGatewayRefNamespace .Values.gatewayRefNamespace }} + namespace: {{ .Values.microcks.grpcGatewayRefNamespace | default .Values.gatewayRefNamespace }} + {{- end }} + sectionName: {{ .Values.microcks.grpcGatewayRefSectionName | default .Values.grpcGatewayRefSectionName }} + hostnames: + - {{ ( include "microcks-grpc.url" . ) }} + rules: + - backendRefs: + - name: "{{ .Values.appName }}-grpc" + port: 9090 +{{- if and .Values.keycloak.enabled .Values.keycloak.install }} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: "{{ .Values.appName }}-keycloak" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + {{- with .Values.keycloak.ingressAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + - name: {{ .Values.keycloak.gatewayRefName | default .Values.gatewayRefName }} + {{- if or .Values.keycloak.gatewayRefNamespace .Values.gatewayRefNamespace }} + namespace: {{ .Values.keycloak.gatewayRefNamespace | default .Values.gatewayRefNamespace }} + {{- end }} + sectionName: {{ .Values.keycloak.gatewayRefSectionName | default .Values.gatewayRefSectionName }} + hostnames: + - "{{ .Values.keycloak.url }}" + rules: + - matches: + - path: + value: / + type: PathPrefix + backendRefs: + - name: "{{ .Values.appName }}-keycloak" + port: 8080 +{{- end }} +{{- if and .Values.features.async.enabled }} +--- +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: "{{ .Values.appName }}-ws" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + {{- with .Values.features.async.ws.ingressAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + - name: {{ .Values.features.async.ws.gatewayRefName | default .Values.gatewayRefName }} + {{- if or .Values.features.async.ws.gatewayRefNamespace .Values.gatewayRefNamespace }} + namespace: {{ .Values.features.async.ws.gatewayRefNamespace | default .Values.gatewayRefNamespace }} + {{- end }} + sectionName: {{ .Values.features.async.ws.gatewayRefSectionName | default .Values.gatewayRefSectionName }} + hostnames: + - "{{ ( include "microcks-ws.url" . ) }}" + rules: + - matches: + - path: + value: /api/ws + type: PathPrefix + backendRefs: + - name: "{{ .Values.appName }}-async-minion" + port: 8080 +{{- end }} +{{- end -}} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/ingress.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/ingress.yaml new file mode 100644 index 000000000..a0b034138 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/ingress.yaml @@ -0,0 +1,147 @@ +{{- if .Values.ingresses -}} +--- +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "{{ .Values.appName }}" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + ingress.kubernetes.io/rewrite-target: / + {{- with .Values.microcks.ingressAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.microcks.ingressClassName }} + ingressClassName: {{ .Values.microcks.ingressClassName | quote }} + {{- end }} + tls: + - hosts: + - "{{ .Values.microcks.url }}" + secretName: {{ .Values.microcks.ingressSecretRef | default (print .Values.appName "-microcks-ingress-secret") }} + rules: + - host: "{{ .Values.microcks.url }}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: "{{ .Values.appName }}" + port: + number: 8080 +--- +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "{{ .Values.appName }}-grpc" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/backend-protocol: "GRPC" + {{- if (.Values.microcks.grpcEnableTLS) }} + nginx.ingress.kubernetes.io/ssl-passthrough: "true" + {{- end }} + {{- with .Values.microcks.grpcIngressAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.microcks.grpcIngressClassName }} + ingressClassName: {{ .Values.microcks.grpcIngressClassName | quote }} + {{- end }} + tls: + - hosts: + - {{ ( include "microcks-grpc.url" . ) }} + rules: + - host: {{ ( include "microcks-grpc.url" . ) }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: "{{ .Values.appName }}-grpc" + port: + number: 9090 +{{- if and .Values.keycloak.enabled .Values.keycloak.install }} +--- +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "{{ .Values.appName }}-keycloak" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + ingress.kubernetes.io/rewrite-target: / + {{- with .Values.keycloak.ingressAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.keycloak.ingressClassName }} + ingressClassName: {{ .Values.keycloak.ingressClassName | quote }} + {{- end }} + tls: + - hosts: + - "{{ .Values.keycloak.url }}" + secretName: {{ .Values.keycloak.ingressSecretRef | default (print .Values.appName "-keycloak-ingress-secret") }} + rules: + - host: "{{ .Values.keycloak.url }}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: "{{ .Values.appName }}-keycloak" + port: + number: 8080 +{{- end }} +{{- if and .Values.features.async.enabled }} +--- +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "{{ .Values.appName }}-ws" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/proxy-read-timeout: "3000" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3000" + {{- with .Values.features.async.ws.ingressAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.features.async.ws.ingressClassName }} + ingressClassName: {{ .Values.features.async.ws.ingressClassName | quote }} + {{- end }} + tls: + - hosts: + - "{{ ( include "microcks-ws.url" . ) }}" + secretName: {{ .Values.features.async.ws.ingressSecretRef | default (print .Values.appName "-microcks-ws-ingress-secret") }} + rules: + - host: "{{ ( include "microcks-ws.url" . ) }}" + http: + paths: + - path: /api/ws + pathType: Prefix + backend: + service: + name: "{{ .Values.appName }}-async-minion" + port: + number: 8080 +{{- end }} +{{- end -}} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/kafka-topic.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/kafka-topic.yaml new file mode 100644 index 000000000..982c057c7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/kafka-topic.yaml @@ -0,0 +1,25 @@ +{{- if and .Values.features.async.enabled .Values.features.async.kafka.install }} +{{- if .Capabilities.APIVersions.Has "kafka.strimzi.io/v1beta1/KafkaTopic" }} +apiVersion: kafka.strimzi.io/v1beta1 +kind: KafkaTopic +metadata: + labels: + strimzi.io/cluster: "{{ .Values.appName }}-kafka" + name: "{{ .Values.appName }}-kafka-services-updates" +spec: + partitions: 1 + replicas: 1 + topicName: microcks-services-updates +{{- else if .Capabilities.APIVersions.Has "kafka.strimzi.io/v1beta2/KafkaTopic" }} +apiVersion: kafka.strimzi.io/v1beta2 +kind: KafkaTopic +metadata: + labels: + strimzi.io/cluster: "{{ .Values.appName }}-kafka" + name: "{{ .Values.appName }}-kafka-services-updates" +spec: + partitions: 1 + replicas: 1 + topicName: microcks-services-updates +{{- end }} +{{- end }} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/kafka.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/kafka.yaml new file mode 100644 index 000000000..353a5763e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/kafka.yaml @@ -0,0 +1,85 @@ +{{- if and .Values.features.async.enabled .Values.features.async.kafka.install }} +{{- if .Capabilities.APIVersions.Has "kafka.strimzi.io/v1beta1/Kafka" }} +kind: Kafka +apiVersion: kafka.strimzi.io/v1beta1 +metadata: + name: "{{ .Values.appName }}-kafka" +spec: + entityOperator: + topicOperator: {} + userOperator: {} + kafka: + config: + log.message.format.version: "2.5" + offsets.topic.replication.factor: 1 + transaction.state.log.min.isr: 1 + transaction.state.log.replication.factor: 1 + listeners: + plain: {} + tls: {} + external: + type: ingress + configuration: + bootstrap: + host: "{{ .Values.appName }}-kafka.{{ .Values.features.async.kafka.url }}" + brokers: + - broker: 0 + host: "{{ .Values.appName }}-kafka-0.{{ .Values.features.async.kafka.url }}" + replicas: 1 + storage: + type: ephemeral + resources: + {{- toYaml .Values.features.async.kafka.resources | nindent 6 }} + zookeeper: + replicas: 1 + storage: + type: ephemeral + resources: + {{- toYaml .Values.features.async.kafka.resources | nindent 6 }} +{{- else if .Capabilities.APIVersions.Has "kafka.strimzi.io/v1beta2/Kafka" }} +kind: Kafka +apiVersion: kafka.strimzi.io/v1beta2 +metadata: + name: "{{ .Values.appName }}-kafka" +spec: + entityOperator: + topicOperator: {} + userOperator: {} + kafka: + config: + offsets.topic.replication.factor: 1 + transaction.state.log.replication.factor: 1 + transaction.state.log.min.isr: 1 + listeners: + - name: plain + port: 9092 + type: internal + tls: false + - name: tls + port: 9093 + type: internal + tls: true + - name: external + port: 9094 + tls: true + type: ingress + configuration: + class: "{{ .Values.features.async.kafka.ingressClassName }}" + bootstrap: + host: "{{ .Values.appName }}-kafka.{{ .Values.features.async.kafka.url }}" + brokers: + - broker: 0 + host: "{{ .Values.appName }}-kafka-0.{{ .Values.features.async.kafka.url }}" + replicas: 1 + storage: + type: ephemeral + resources: + {{- toYaml .Values.features.async.kafka.resources | nindent 6 }} + zookeeper: + replicas: 1 + storage: + type: ephemeral + resources: + {{- toYaml .Values.features.async.kafka.resources | nindent 6 }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/secret.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/secret.yaml new file mode 100644 index 000000000..1bad7525d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/secret.yaml @@ -0,0 +1,116 @@ +{{- if and (.Values.microcks.grpcEnableTLS) (not .Values.microcks.grpcSecretRef) }} +kind: Secret +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-microcks-grpc-secret" + labels: + app: "{{ .Values.appName }}" + group: microcks + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +type: kubernetes.io/tls +data: +{{ ( include "microcks-grpc.gen-certs" . ) | indent 2 }} +{{- end }} +{{- if and (.Values.ingresses) (not .Values.microcks.ingressSecretRef) (.Values.microcks.generateCert) }} +--- +kind: Secret +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-microcks-ingress-secret" + labels: + app: "{{ .Values.appName }}" + group: microcks + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +type: kubernetes.io/tls +data: +{{ ( include "microcks-ingress.gen-certs" . ) | indent 2 }} +{{- end }} +{{- if and (.Values.mongodb.install) (not .Values.mongodb.secretRef) }} +--- +kind: Secret +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-mongodb-connection" + labels: + app: "{{ .Values.appName }}" + container: mongodb + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +type: kubernetes.io/basic-auth +stringData: + username: "{{ .Values.mongodb.username }}" +data: + {{- if .Values.mongodb.password }} + password: {{ .Values.mongodb.password | b64enc | quote }} + {{- else }} + password: {{ randAlphaNum 32 | b64enc | quote }} + {{- end }} + adminUsername: {{ randAlphaNum 16 | b64enc | quote }} + adminPassword: {{ randAlphaNum 40 | b64enc | quote }} +{{- end }} +{{- if and .Values.keycloak.enabled .Values.keycloak.install }} +{{- if and (.Values.ingresses) (not .Values.keycloak.ingressSecretRef) (.Values.keycloak.generateCert) }} +--- +kind: Secret +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-keycloak-ingress-secret" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +type: kubernetes.io/tls +data: +{{ ( include "microcks.keycloak-ingress.gen-certs" . ) | indent 2 }} +{{- end }} +{{- if (not .Values.keycloak.secretRef) }} +--- +kind: Secret +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-keycloak-admin" + labels: + app: "{{ .Values.appName }}" + container: keycloak + group: microcks + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +type: kubernetes.io/basic-auth +stringData: + username: "{{ .Values.keycloak.adminUsername }}" + postgresUsername: "{{ .Values.keycloak.username }}" +data: + {{- if .Values.keycloak.adminPassword }} + password: {{ .Values.keycloak.adminPassword | b64enc | quote }} + {{- else }} + password: {{ randAlphaNum 40 | b64enc | quote }} + {{- end }} + {{- if .Values.keycloak.password }} + postgresPassword: {{ .Values.keycloak.password | b64enc | quote }} + {{- else }} + postgresPassword: {{ randAlphaNum 32 | b64enc | quote }} + {{- end }} +{{- end }} +{{- end }} +{{- if and (.Values.ingresses) (.Values.features.async.enabled) (not .Values.features.async.ws.ingressSecretRef) (.Values.features.async.ws.generateCert) }} +--- +kind: Secret +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-microcks-ws-ingress-secret" + labels: + app: "{{ .Values.appName }}" + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +type: kubernetes.io/tls +data: +{{ ( include "microcks-ws-ingress.gen-certs" . ) | indent 2 }} +{{- end }} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/service.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/service.yaml new file mode 100644 index 000000000..bd7c80bdf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/templates/service.yaml @@ -0,0 +1,177 @@ +kind: Service +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}" + labels: + app: "{{ .Values.appName }}" + container: spring + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + +spec: + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + name: spring + type: {{ .Values.microcks.serviceType | default "ClusterIP" | quote }} + sessionAffinity: None + selector: + app: "{{ .Values.appName }}" + container: spring + group: microcks +--- +kind: Service +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-grpc" + labels: + app: "{{ .Values.appName }}" + container: spring + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +spec: + ports: + - protocol: TCP + port: 9090 + targetPort: 9090 + name: spring-grpc + type: ClusterIP + sessionAffinity: None + selector: + app: "{{ .Values.appName }}" + container: spring + group: microcks +--- +kind: Service +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-postman-runtime" + labels: + app: "{{ .Values.appName }}" + container: postman-runtime + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +spec: + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 + name: postman-runtime + type: ClusterIP + sessionAffinity: None + selector: + app: "{{ .Values.appName }}" + container: postman-runtime + group: microcks +{{- if and .Values.mongodb.install }} +--- +kind: Service +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-mongodb" + labels: + app: "{{ .Values.appName }}" + container: mongodb + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +spec: + ports: + - name: mongodb + protocol: TCP + port: 27017 + targetPort: 27017 + nodePort: 0 + selector: + app: "{{ .Values.appName }}" + container: mongodb + group: microcks + type: ClusterIP + sessionAffinity: None +{{- end }} +{{- if and .Values.keycloak.enabled .Values.keycloak.install }} +--- +kind: Service +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-keycloak" + labels: + app: "{{ .Values.appName }}" + container: keycloak + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +spec: + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + name: keycloak + type: {{ .Values.keycloak.serviceType | default "ClusterIP" | quote }} + sessionAffinity: None + selector: + app: "{{ .Values.appName }}" + container: keycloak + group: microcks +--- +kind: Service +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-keycloak-postgresql" + labels: + app: "{{ .Values.appName }}" + container: keycloak-postgresql + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} +spec: + ports: + - name: postgresql + protocol: TCP + port: 5432 + targetPort: 5432 + nodePort: 0 + type: ClusterIP + sessionAffinity: None + selector: + app: "{{ .Values.appName }}" + container: keycloak-postgresql + group: microcks +{{- end }} +{{- if and .Values.features.async.enabled }} +--- +kind: Service +apiVersion: v1 +metadata: + name: "{{ .Values.appName }}-async-minion" + labels: + app: "{{ .Values.appName }}" + container: async-minion + group: microcks + {{- include "microcks-common-labels" . | nindent 4 }} + annotations: + {{- include "microcks-common-annotations" . | nindent 4 }} + +spec: + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + name: async-minion + type: ClusterIP + sessionAffinity: None + selector: + app: "{{ .Values.appName }}" + container: async-minion + group: microcks +{{- end }} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/values.yaml b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/values.yaml new file mode 100644 index 000000000..29df431eb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/kubernetes/microcks/values.yaml @@ -0,0 +1,428 @@ +appName: microcks +ingresses: true + +# You can disable ingresses and use HTTPRoute + GRPCRoutes resources instead. +gatewayRoutes: false +# When using HTTPRoute resources, you can set the Gateway to use. +gatewayRefName: default +# No value for namespace means current namespace. +gatewayRefNamespace: +gatewayRefSectionName: https +grpcGatewayRefSectionName: grpc + +microcks: + url: microcks-microcks.192.168.99.100.nip.io + #url: microcks-microcks.192.168.64.6.nip.io + + #ingressSecretRef: my-secret-for-microcks-ingress + #ingressClassName: nginx + + # Ingress annotations are also used for HTTPRoute resources. + #ingressAnnotations: + #cert-manager.io/issuer: my-cert-issuer + #kubernetes.io/tls-acme: "true" + + # Uncomment to use a custom Gateway for Microcks HTTPRoute. + #gatewayRefName: my-gateway + #gatewayRefNamespace: + #gatewayRefSectionName: https + + grpcEnableTLS: true + #grpcSecretRef: my-secret-for-microcks-grpc + #grpcIngressClassName: nginx + + # gRPC ingress annotations are also used for GRPCRoute resources. + #grpcIngressAnnotations: + #myclass.ingress.kubernetes.io/backend-protocol: "GRPC" + #myclass.ingress.kubernetes.io/ssl-passthrough: "true" + + # Uncomment to use a custom Gateway for Microcks GRPCRoute. + #grpcGatewayRefName: my-gateway + #grpcGatewayRefNamespace: + #grpcGatewayRefSectionName: https + + # Useless if gatewayRoutes are enabled instead of ingresses. + generateCert: true + + image: + registry: quay.io + repository: microcks/microcks + tag: 1.12.1 + digest: + replicas: 1 + + # Uncomment to change Microcks Service to a NodePort + #serviceType: NodePort + + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + #cpu: 500m + memory: 512Mi + + # Enabling by adding yaml extra application configurations + extraProperties: + #server: + #tomcat: + #remoteip: + #internal-proxies: 172.16.0.0/12 + + env: + - name: SERVICES_UPDATE_INTERVAL + value: 0 0 0/2 * * * + #- name: MAX_UPLOAD_FILE_SIZE + # value: 5MB + #- name: CORS_REST_ALLOWED_ORIGINS + # value: my-site.acme.com + #- name: CORS_REST_ALLOW_CREDENTIALS + # value: 'true' + + #logLevel: TRACE | DEBUG | INFO | WARN + logLevel: INFO + + # Enabling/disabling mock invocation stats. + mockInvocationStats: true + + # Custom secret for Microcks + # It can be used to deploy a keystore to add certificate to trust sso connection with keycloak. + # In this case: Use an existing secret with the provided keystore. + # Don't forget to add java options like that if your secret is a keystore: + # JAVA_OPTIONS: "-Djavax.net.ssl.trustStore=/deployments/config/custom/secret/KEYSTORE -Djavax.net.ssl.trustStorePassword=XXXXX" + #customSecretRef: + # secret: microcks-keystore + # key: cacerts + +postman: + image: + registry: quay.io + repository: microcks/microcks-postman-runtime + tag: 0.6.0 + digest: + replicas: 1 + resources: + requests: + memory: 256Mi + limits: + memory: 256Mi + +keycloak: + # Use keycloack in this release + enabled: true + # Install keycloack in this release + install: true + realm: microcks + # Now that we switched to newer version of Keycloak-X, default url does not contain '/auth' path + # If you use an older external Keycloak instance, url must include the '/auth' path + url: keycloak-microcks.192.168.99.100.nip.io + #privateUrl: http://microcks-keycloak.microcks.svc.cluster.local:8080 + + #ingressSecretRef: my-secret-for-keycloak-ingress + #ingressClassName: nginx + + # Ingress annotations are also used for HTTPRoute resources. + #ingressAnnotations: + #cert-manager.io/issuer: my-cert-issuer + #kubernetes.io/tls-acme: "true" + + # Uncomment to use a custom Gateway for Keycloak HTTPRoute. + #gatewayRefName: my-gateway + #gatewayRefNamespace: + #gatewayRefSectionName: https + + # Useless if gatewayRoutes are enabled instead of ingresses. + generateCert: true + + #pvcAnnotations: + #helm.sh/resource-policy: keep + + image: + registry: quay.io + repository: keycloak/keycloak + tag: 26.0.0 + digest: + adminUsername: admin + # Unless you uncomment following line, admin password will be randomly generated. + # Beware that in case of update, new value will be generated and + #adminPassword: 123 + + # Or you can uncomment secretRef block if credentials are provided through a Secret. + #secretRef: + #secret: keycloak + #usernameKey: username + #passwordKey: password + #postgresUsernameKey: postgresUsername + #postgresPasswordKey: postgresPassword + + # Uncomment to change Keycloak Service to a NodePort + #serviceType: NodePort + + resources: + requests: + cpu: 400m + memory: 512Mi + limits: + #cpu: 500m + memory: 512Mi + + persistent: true + volumeSize: 1Gi + # Unless you uncomment following line and set class, persistent volume claim is created + # with no storage class and relies on cluster default one. + #storageClassName: my-awesome-class + + postgresImage: + registry: + repository: library/postgres + tag: 16.3-alpine + + postgresResources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 150m + memory: 192Mi + + username: userP + # Unless you uncomment following line, admin password will be randomly generated. + # Beware that in case of update, new value will be generated and overwrite existing one. + #password: 123 + + serviceAccount: microcks-serviceaccount + serviceAccountCredentials: ab54d329-e435-41ae-a900-ec6b3fe15c54 + +mongodb: + install: true + #uri: mongodb:27017 + #uriParameters: "?ssl=true" + #database: sampledb + image: + registry: + repository: library/mongo + tag: 4.4.29 + digest: + persistent: true + volumeSize: 2Gi + # Unless you uncomment following line and set class, persistent volume claim is created + # with no storage class and relies on cluster default one. + #storageClassName: my-awesome-class + + username: userM + # Unless you uncomment following line, admin password will be randomly generated. + # Beware that in case of update, new value will be generated and overwrite existing one. + #password: 123 + + # Or you can uncomment secretRef block if username and password are provided through a Secret. + #secretRef: + #secret: mongodb + #usernameKey: database-user + #passwordKey: database-password + + #pvcAnnotations: + #helm.sh/resource-policy: keep + + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + #cpu: 500m + memory: 512Mi + +features: + async: + enabled: false + defaultBinding: KAFKA + defaultFrequency: 10 + defaultAvroEncoding: RAW + + image: + registry: quay.io + repository: microcks/microcks-async-minion + tag: 1.12.1 + digest: + + env: + #- name: OAUTH_CLIENT_ID + # value: + #- name: OAUTH_CLIENT_SECRET + # value: + #- name: OAUTH_TOKEN_ENDPOINT_URI + # value: + + kafka: + install: true + url: 192.168.99.100.nip.io + #url: kafka-bootstrap:9092 + # Set this to your own class name if not using bare nginx + ingressClassName: nginx + persistent: false + volumeSize: 2Gi + resources: + #requests: + #cpu: 100m + #memory: 256Mi + limits: + #cpu: 500m + memory: 800Mi + zkResources: + #requests: + #cpu: 100m + #memory: 256Mi + limits: + #cpu: 500m + memory: 800Mi + schemaRegistry: + #url: http://schema-registry.192.168.99.100.nip.io + confluent: true + username: microcks + credentialsSource: USER_INFO + + # If you choose not to install a Kafka broker and reuse on pre-existing, + # you may need to set some authentication parameters. + authentication: + # If not 'none', we support 'SSL' for mutual TLS and 'SASL_SSL' for SASL over TLS. + type: none #SSL #SASL_SSL + # For TLS transport, you'll always need a truststore to hold your cluster certificate. + # You have to setup a truststore type and a secret reference for retrieving content and password. + #truststoreType: PKCS12 + #truststoreSecretRef: + #secret: kafka-cluster-ca-cert + #storeKey: ca.p12 + #passwordKey: ca.password + # For mutual TLS authentication, you'll also need a keystore to hold your user private key. + # You have to setup a keystore type and a secret reference for retrieving content and password. + keystoreType: PKCS12 + keystoreSecretRef: + secret: mtls-user + storeKey: user.p12 + passwordKey: user.password + # For SASL authentication, you'll have to specify an additional authentication mechanism + # as well as a JAAS configuration line with login module, username and password. + #saslMechanism: SCRAM-SHA-512 + #saslJaasConfig: org.apache.kafka.common.security.scram.ScramLoginModule required username="scram-user" password="tDtDCT3pYKE5"; + # or an OAuth configuration (you'll need here to extend the async-minion image with your handler class implementation) + #saslMechanism: OAUTHBEARER + #saslJaasConfig: org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required; + #saslLoginCallbackHandlerClass: io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler + + # Uncomment the mqtt.url and put a valid endpoint address below to enable MQTT support. + mqtt: + #url: artemis:1883 + username: microcks + password: microcks + + # Uncomment the amqp.url and put a valid endpoint address below to enable AMQP support. + amqp: + #url: rabbitmq:5672 + username: microcks + password: microcks + + # Uncomment the nats.url and put a valid endpoint address below to enable NATS support. + nats: + #url: nats:4222 + username: microcks + password: microcks + + # Uncomment the googlepubsub.project and put a valid project id below to enable PubSub support. + googlepubsub: + #project: my-project + # For authentication, we rely on a Google Service Account JSON file. + # You have to setup a secret reference for retrieving this file from secret. + serviceAccountSecretRef: + secret: googlecloud-service-account + fileKey: googlecloud-service-account.json + + # Uncomment the sqs.region and put a valid region below to enable Amazon SQS support. + sqs: + #region: eu-west-3 + # For authentication, we rely on either Environment Variables or an AWS Profile of type credentials (see https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials_profiles.html). + # You can choose between 'env-variable' and 'profile' + credentialsType: env-variable + #credentialsType: profile + # For 'env-variable', you may want to specify a Secret to get environment variables from. + #credentialsSecretRef: + #secret: aws-credentials + #accessKeyIdKey: access_key_id + #secretAccessKeyKey: secret_access_key + #sessionTokenKey: session_token + # For 'profile', you have to provide the profile name and setup a secret reference for retrieving this file from secret. + credentialsProfile: microcks-sqs-admin + #credentialsSecretRef: + #secret: aws-credentials + #fileKey: aws.profile + + # Uncomment the sns.region and put a valid region below to enable Amazon SNS support. + sns: + #region: eu-west-3 + # For authentication, we rely on either Environment Variables or an AWS Profile of type credentials (see https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials_profiles.html). + # You can choose between 'env-variable' and 'profile' + credentialsType: env-variable + #credentialsType: profile + # For 'env-variable', you may want to specify a Secret to get environment variables from. + #credentialsSecretRef: + #secret: aws-credentials + #accessKeyIdKey: access_key_id + #secretAccessKeyKey: secret_access_key + #sessionTokenKey: session_token + # For 'profile', you have to provide the profile name and setup a secret reference for retrieving this file from secret. + credentialsProfile: microcks-sns-admin + #credentialsSecretRef: + #secret: aws-credentials + #fileKey: aws.profile + + ws: + #ingressSecretRef: my-secret-for-microcks-ws-ingress + #ingressClassName: nginx + + # Ingress annotations are also used for HTTPRoute resources. + #ingressAnnotations: + #cert-manager.io/issuer: my-ws-cert-issuer + #kubernetes.io/tls-acme: "true" + + # Uncomment to use a custom Gateway for Async Minion WS HTTPRoute. + #gatewayRefName: my-gateway + #gatewayRefNamespace: + #gatewayRefSectionName: https + + # Useless if gatewayRoutes are enabled instead of ingresses. + generateCert: true + + repositoryFilter: + enabled: false + labelKey: app + labelLabel: Application + labelList: app,status + + repositoryTenancy: + enabled: false + artifactImportAllowedRoles: admin,manager,manager-any + + microcksHub: + enabled: true + allowedRoles: admin,manager,manager-any + + aiCopilot: + enabled: false + implementation: openai + openai: + apiKey: sk-my-openai-api-token + timeout: 20 + #model: gpt-3.5-turbo + #maxTokens: 2000 + +# common labels associated with all resources helm chart +commonLabels: {} + +# common annotations associated with all resource of this helm chart +commonAnnotations: {} + +# common affinities associated with all resource of this helm chart +commonAffinities: {} + +## Node tolerations for server scheduling to nodes with taints +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ +## +commonTolerations: [] diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/.gitignore b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/.gitignore new file mode 100644 index 000000000..b77b5bc9e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/.gitignore @@ -0,0 +1,2 @@ +microcks-data +microcks.yml \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/README.md b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/README.md new file mode 100644 index 000000000..bae907b75 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/README.md @@ -0,0 +1,56 @@ +From Microcks `1.7.1`, we changed the purpose of `run-microcks.sh` script. + +Before `1.7.1`, this file was about adapting compose templates to root or rootless usages. + +However with recent versions of podman for MacOS (and probably M1 architecture as well), +we faced more and more issues for getting podman run on MacOS (see https://github.com/microcks/microcks/issues/568 +for details). + +As a consequence, we now shifted to rootless mode only (safer) and change `run-microcks.sh` to +manage compose files and command lines tweaking depending on your OS. + +### Archived run-microcks.sh + +The old content of `run-microcks.sh` is put below for archive concern: + +```sh +#!/bin/bash + +mkdir -p microcks-data || exit 1 + +# The chosen podman-compose template depends on who is running this script +if [ "$UID" -eq 0 ]; then + # We are root ! + template="microcks-template.yml" + cmd_prefix="sudo" +else + echo "Running rootless containers..." + template="microcks-template-rootless.yml" + cmd_prefix="" +fi + +# Find the host ip address on linux systems +host_ip="$(ip -o route get to 8.8.8.8 2>/dev/null | sed -n 's/.*src \([0-9.]\+\).*/\1/p')" +if [ -z "$host_ip" ]; then + # Fallback method + iface="$(awk -F "\t" '$2 == "00000000" { print $1 }' /proc/net/route 2>/dev/null)" + host_ip="$(ifconfig "$iface" 2>/dev/null |awk '$1 == "inet" { print $2 }')" +fi + +# Generate a podman-compose file from the supplied template +echo "Discovered host IP address: ${host_ip:-none}" +sed "s/__HOST__/$host_ip/" "$template" > microcks.yml || exit 1 + +echo +echo "Starting Microcks using podman-compose ..." +echo "------------------------------------------" +echo "Stop it with: $cmd_prefix podman-compose -f microcks.yml stop" +echo "Re-launch it with: $cmd_prefix podman-compose -f microcks.yml start" +echo "Clean everything with: $cmd_prefix podman-compose -f microcks.yml down" +echo "------------------------------------------" +echo "Go to https://localhost:8080 - first login with admin/microcks123" +echo "Having issues? Check you have changed microcks.yml to your platform" +echo + +podman-compose -f microcks.yml up -d +``` \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/async-addon.yml b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/async-addon.yml new file mode 100644 index 000000000..995396093 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/async-addon.yml @@ -0,0 +1,91 @@ +services: + zookeeper: + image: quay.io/strimzi/kafka:0.43.0-kafka-3.8.0 + container_name: microcks-zookeeper + command: [ + "sh", "-c", "bin/zookeeper-server-start.sh config/zookeeper.properties" + ] + ports: + - "2181:2181" + environment: + LOG_DIR: /tmp/logs + healthcheck: + test: nc -z localhost 2181 || exit -1 + interval: 10s + timeout: 5s + retries: 3 + + kafka: + depends_on: + - zookeeper + image: quay.io/strimzi/kafka:0.43.0-kafka-3.8.0 + container_name: microcks-kafka + command: [ + "sh", "-c", + "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override listener.security.protocol.map=$${KAFKA_LISTENER_SECURITY_PROTOCOL_MAP} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" + ] + ports: + - "9092:9092" + - "19092:19092" + environment: + LOG_DIR: "/tmp/logs" + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:19092,EXTERNAL://localhost:9092 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:19092,EXTERNAL://0.0.0.0:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + healthcheck: + test: ["CMD-SHELL", "timeout 5 bash -c 'echo > /dev/tcp/localhost/19092'"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + + app: + depends_on: + - mongo + - keycloak + - postman + image: quay.io/microcks/microcks:1.12.1 + container_name: microcks + volumes: + - "./config:/deployments/config" + ports: + - "8080:8080" + - "9090:9090" + environment: + - SPRING_PROFILES_ACTIVE=prod + - SPRING_DATA_MONGODB_URI=mongodb://microcks-db:27017 + - SPRING_DATA_MONGODB_DATABASE=microcks + - POSTMAN_RUNNER_URL=http://postman:3000 + - TEST_CALLBACK_URL=http://microcks:8080 + - SERVICES_UPDATE_INTERVAL=0 0 0/2 * * * + - KEYCLOAK_URL=http://keycloak:8080 + - KEYCLOAK_PUBLIC_URL=http://localhost:18080 + - JAVA_OPTIONS=-Dspring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:18080/realms/microcks -Dspring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/realms/microcks/protocol/openid-connect/certs + - ASYNC_MINION_URL=http://microcks-async-minion:8081 + - KAFKA_BOOTSTRAP_SERVER=kafka:19092 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + start_period: 35s + interval: 10s + timeout: 3s + retries: 3 + + async-minion: + depends_on: + - app + ports: + - "8081:8081" + image: quay.io/microcks/microcks-async-minion:1.12.1 + container_name: microcks-async-minion + restart: on-failure + volumes: + - "./config:/deployments/config" + environment: + - QUARKUS_PROFILE=docker-compose + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/q/health/ready"] + start_period: 10s + interval: 10s + timeout: 2s + retries: 3 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/config/application.properties b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/config/application.properties new file mode 100644 index 000000000..dcf21313a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/config/application.properties @@ -0,0 +1,28 @@ +# Async mocking support. +async-api.enabled=true + +# Access to Microcks API server. +%docker-compose.io.github.microcks.minion.async.client.MicrocksAPIConnector/mp-rest/url=http://microcks:8080 + +# Access to Keycloak through docker network +%docker-compose.keycloak.auth.url=http://keycloak:8080 + +# Access to Kafka broker. +%docker-compose.kafka.bootstrap.servers=kafka:19092 + +# Do not save any consumer-offset on the broker as there's a re-sync on each minion startup. +%docker-compose.mp.messaging.incoming.microcks-services-updates.enable.auto.commit=false +%docker-compose.mp.messaging.incoming.microcks-services-updates.bootstrap.servers=kafka:19092 + +# Explicitly telling the minion the protocols we want to support +%docker-compose.minion.supported-bindings=KAFKA,WS + +# %docker-compose.minion.supported-bindings=KAFKA,WS,MQTT,AMQP,NATS + +# %docker-compose.mqtt.server=localhost:1883 +# %docker-compose.mqtt.username=microcks +# %docker-compose.mqtt.password=microcks + +# %docker-compose.amqp.server=localhost:5672 +# %docker-compose.amqp.username=microcks +# %docker-compose.amqp.password=microcks \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/config/features.properties b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/config/features.properties new file mode 100644 index 000000000..b088c9c8f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/config/features.properties @@ -0,0 +1,8 @@ +features.feature.async-api.enabled=true +features.feature.async-api.frequencies=3,10,30 +features.feature.async-api.default-binding=KAFKA +features.feature.async-api.endpoint-KAFKA=localhost:9092 +features.feature.async-api.endpoint-MQTT=my-mqtt-broker.apps.try.microcks.io:1883 +features.feature.async-api.endpoint-AMQP=my-amqp-broker.apps.try.microcks.io:5672 +features.feature.async-api.endpoint-WS=localhost:8081 +features.feature.async-api.endpoint-NATS=localhost:4222 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/keycloak-realm/microcks-realm-sample.json b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/keycloak-realm/microcks-realm-sample.json new file mode 100644 index 000000000..cf99b2fbd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/keycloak-realm/microcks-realm-sample.json @@ -0,0 +1,155 @@ +{ + "id": "microcks", + "realm": "microcks", + "displayName": "Microcks", + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "users" : [ + { + "username" : "admin", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "microcks123" } + ], + "realmRoles": [], + "applicationRoles": { + "realm-management": [ "manage-users", "manage-clients" ], + "account": [ "manage-account" ], + "microcks-app": [ "user", "manager", "admin"] + } + } + ], + "roles": { + "realm": [], + "client": { + "microcks-app": [ + { + "name": "user", + "composite": false, + "clientRole": true, + "containerId": "microcks" + }, + { + "name": "admin", + "composite": false, + "clientRole": true, + "containerId": "microcks" + }, + { + "name": "manager", + "composite": false, + "clientRole": true, + "containerId": "microcks" + } + ] + } + }, + "groups": [ + { + "name": "microcks", + "path": "/microcks", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [ + { + "name": "manager", + "path": "/microcks/manager", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + } + ] + } + ], + "defaultRoles": [], + "requiredCredentials": [ "password" ], + "scopeMappings": [], + "clientScopeMappings": { + "microcks-app": [ + { + "client": "microcks-app-js", + "roles": [ + "manager", + "admin", + "user" + ] + } + ], + "realm-management": [ + { + "client": "microcks-app-js", + "roles": [ + "manage-users", + "manage-clients" + ] + } + ] + }, + "clients": [ + { + "clientId": "microcks-app-js", + "enabled": true, + "publicClient": true, + "redirectUris": [ + "http://localhost:8080/*", + "http://localhost:58085/*" + ], + "webOrigins": [ + "+" + ], + "fullScopeAllowed": false, + "protocolMappers": [ + { + "name": "microcks-group-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "microcks-groups", + "userinfo.token.claim": "true" + } + } + ] + } + ], + "applications": [ + { + "name": "microcks-app", + "enabled": true, + "bearerOnly": true, + "defaultRoles": [ + "user" + ] + }, + { + "name": "microcks-serviceaccount", + "secret": "ab54d329-e435-41ae-a900-ec6b3fe15c54", + "enabled": true, + "bearerOnly": false, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "clientAuthenticatorType": "client-secret" + } + ], + "requiredActions": [ + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": false, + "defaultAction": false, + "priority": 90, + "config": {} + } + ], + "keycloakVersion": "10.0.1" +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/podman-compose-devmode.yml b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/podman-compose-devmode.yml new file mode 100644 index 000000000..09703afb6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/podman-compose-devmode.yml @@ -0,0 +1,51 @@ +services: + mongo: + image: docker.io/library/mongo:4.4.29 + container_name: microcks-db + volumes: + # Podman does not create missing folders, so we need to use an existing one + # Add the ":z" flag to get SELinux configured automatically + - "./microcks-data:/data/db" + #user: "501:1000" + healthcheck: + test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"] + interval: 30s + timeout: 1s + retries: 3 + + postman: + image: quay.io/microcks/microcks-postman-runtime:0.6.0 + container_name: microcks-postman-runtime + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + start_period: 5s + interval: 20s + timeout: 3s + retries: 3 + + app: + depends_on: + - mongo + - postman + image: quay.io/microcks/microcks:1.12.1 + container_name: microcks + volumes: + - "./config:/deployments/config" + ports: + - "8080:8080" + - "9090:9090" + environment: + - SPRING_PROFILES_ACTIVE=prod + - SPRING_DATA_MONGODB_URI=mongodb://microcks-db:27017 + - SPRING_DATA_MONGODB_DATABASE=microcks + - POSTMAN_RUNNER_URL=http://postman:3000 + - TEST_CALLBACK_URL=http://microcks:8080 + - SERVICES_UPDATE_INTERVAL=0 0 0/2 * * * + - KEYCLOAK_ENABLED=false + #- MAX_UPLOAD_FILE_SIZE=3MB + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + start_period: 35s + interval: 10s + timeout: 3s + retries: 3 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/podman-compose.yml b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/podman-compose.yml new file mode 100644 index 000000000..2e55fa15c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/podman-compose.yml @@ -0,0 +1,70 @@ +services: + mongo: + image: docker.io/library/mongo:4.4.29 + container_name: microcks-db + volumes: + # Podman does not create missing folders, so we need to use an existing one + # Add the ":z" flag to get SELinux configured automatically + - "./microcks-data:/data/db" + #user: "501:1000" + healthcheck: + test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"] + interval: 30s + timeout: 1s + retries: 3 + + postman: + image: quay.io/microcks/microcks-postman-runtime:0.6.0 + container_name: microcks-postman-runtime + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + start_period: 5s + interval: 20s + timeout: 3s + retries: 3 + + keycloak: + image: quay.io/keycloak/keycloak:26.0.0 + container_name: microcks-keycloak + ports: + - "18080:8080" + volumes: + - "./keycloak-realm/microcks-realm-sample.json:/opt/keycloak/data/import/microcks-realm.json" + environment: + KEYCLOAK_ADMIN: "admin" + KEYCLOAK_ADMIN_PASSWORD: "admin" + command: ["start-dev", "--hostname=http://localhost:18080", "--import-realm", "--health-enabled=true"] + healthcheck: + test: ["CMD", "sh", "-c", "echo -e 'GET /health/live HTTP/1.1\r\nHost: localhost\r\n\r\n' > /dev/tcp/localhost/9000"] + start_period: 10s + interval: 10s + timeout: 2s + retries: 3 + + app: + depends_on: + - mongo + - postman + - keycloak + image: quay.io/microcks/microcks:1.12.1 + container_name: microcks + ports: + - "8080:8080" + - "9090:9090" + environment: + - SPRING_PROFILES_ACTIVE=prod + - SPRING_DATA_MONGODB_URI=mongodb://microcks-db:27017 + - SPRING_DATA_MONGODB_DATABASE=microcks + - POSTMAN_RUNNER_URL=http://postman:3000 + - TEST_CALLBACK_URL=http://microcks:8080 + - KEYCLOAK_URL=http://keycloak:8080 + - KEYCLOAK_PUBLIC_URL=http://localhost:18080 + - JAVA_OPTIONS=-Dspring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:18080/realms/microcks -Dspring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/realms/microcks/protocol/openid-connect/certs + - SERVICES_UPDATE_INTERVAL=0 0 0/2 * * * + #- MAX_UPLOAD_FILE_SIZE=3MB + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + start_period: 35s + interval: 10s + timeout: 3s + retries: 3 diff --git a/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/run-microcks.sh b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/run-microcks.sh new file mode 100644 index 000000000..fdb995c38 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/install/podman-compose/run-microcks.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# +# Copyright The Microcks Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +mkdir -p microcks-data || exit 1 + +# Machine name on macos. +machine_name="podman-machine-default" + +# Assuming default template, no addon and no command prefix. +template=""; +addon=""; +cmd_prefix=""; + +# Parse argument to adjust template and addon. +if [ -n "$1" ]; then + if [ "$1" == "dev" ]; then + template="-devmode" + elif [ "$1" == "async" ]; then + addon="-f microcks-template-async-addon.yml" + fi +fi + +# Generate a podman-compose file from the supplied template +cp ./microcks-template"$template".yml ./microcks.yml || exit 1; + +if [ "$(uname)" == "Darwin" ]; then + # Change permission on microcks-data + chmod 777 ./microcks-data + + echo + echo "On macos, need to get the userid and groupid from postman machine." + echo "Assuming this machine is named 'podman-machine-default'. Change name in script otherwise." + idStr="$(podman machine ssh $machine_name id)" || exit 1 + + # The above command should give something like: + #idStr="uid=501(core) gid=1000(core) groups=1000(core),4(adm),10(wheel),16(sudo),190(systemd-journal) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023" + + # Parse uid, gid and sed in enable user directive on mongo container. + machine_uid=$(echo $idStr | awk '{split($1,element,"="); split(element[2],part,"("); print part[1]}') + machine_gid=$(echo $idStr | awk '{split($2,element,"="); split(element[2],part,"("); print part[1]}') + sed -i '' 's=#user: \"501:1000\"=user: \"'"$machine_uid"':'"$machine_gid"'\"=g' ./microcks.yml || exit 1 + + cmd_prefix="--podman-run-args='--userns=keep-id:uid=$machine_uid,gid=$machine_gid'" +fi + +echo +echo "Starting Microcks using podman-compose ..." +echo "------------------------------------------" +echo "Stop it with: podman-compose -f microcks.yml $addon $cmd_prefix stop" +echo "Re-launch it with: podman-compose -f microcks.yml $addon $cmd_prefix start" +echo "Clean everything with: podman-compose -f microcks.yml $addon $cmd_prefix down" +echo "------------------------------------------" +echo "Go to https://localhost:8080 - first login with admin/microcks123" +echo "Having issues? Check you have changed microcks.yml to your platform" +echo + +podman-compose -f microcks.yml $addon $cmd_prefix up -d diff --git a/jdk_21_maven/cs/rest-gui/microcks/jreleaser.yml b/jdk_21_maven/cs/rest-gui/microcks/jreleaser.yml new file mode 100644 index 000000000..92f571578 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/jreleaser.yml @@ -0,0 +1,66 @@ +project: + name: Microcks + description: Microcks core project + longDescription: 'Microcks: easy mocks for your microservices' + copyright: The Microcks Authors + java: + version: 21 + +signing: + active: ALWAYS + armored: true + +assemble: + archive: + microcks: + active: ALWAYS + stereotype: NONE + options: + longFileMode: POSIX + formats: + - ZIP + - TGZ + fileSets: + - input: target/staging-deploy + includes: + - '**/*.*' + +files: + active: ALWAYS + artifacts: + - path: 'target/site/microcks-{{projectVersion}}.spdx-sbom.json' + - path: 'commons/model/target/site/microcks-model-{{projectVersion}}.spdx-sbom.json' + - path: 'commons/util/target/site/microcks-util-{{projectVersion}}.spdx-sbom.json' + - path: 'commons/util-el/target/site/microcks-el-{{projectVersion}}.spdx-sbom.json' + - path: 'webapp/target/site/microcks-app-{{projectVersion}}.spdx-sbom.json' + - path: 'minions/async/target/site/microcks-async-minion-{{projectVersion}}.spdx-sbom.json' + - path: 'distro/uber/target/site/microcks-uber-app-{{projectVersion}}.spdx-sbom.json' + - path: 'distro/uber-async-minion/target/site/microcks-uber-async-minion-{{projectVersion}}.spdx-sbom.json' + +deploy: + maven: + mavenCentral: + sonatype: + active: ALWAYS + snapshotSupported: false + url: https://central.sonatype.com/api/v1/publisher + username: wPlqdV4c + namespace: io.github.microcks + applyMavenCentralRules: true + stagingRepositories: + - target/staging-deploy + pomchecker: + failOnWarning: false + failOnError: false + strict: false + +release: + github: + overwrite: true + releaseName: '{{tagName}}' + tagName: '{{projectVersion}}' + changelog: + formatted: ALWAYS + preset: conventional-commits + contributors: + format: '- {{contributorName}}{{#contributorUsernameAsLink}} ({{.}}){{/contributorUsernameAsLink}}' diff --git a/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-async-minion.yml b/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-async-minion.yml new file mode 100644 index 000000000..c44358426 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-async-minion.yml @@ -0,0 +1,15 @@ +# Artifact Hub repository metadata file +# +# Some settings like the verified publisher flag or the ignored packages won't +# be applied until the next time the repository is processed. Please keep in +# mind that the repository won't be processed if it has not changed since the +# last time it was processed. Depending on the repository kind, this is checked +# in a different way. For Helm http based repositories, we consider it has +# changed if the `index.yaml` file changes. For git based repositories, it does +# when the hash of the last commit in the branch you set up changes. This does +# NOT apply to ownership claim operations, which are processed immediately. +# +repositoryID: 6c0278ce-c8be-433d-98f7-eca27868b5d1 +owners: + - name: lbroudoux + email: laurent.broudoux@gmail.com \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-uber-async-minion.yml b/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-uber-async-minion.yml new file mode 100644 index 000000000..eb38e4eff --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-uber-async-minion.yml @@ -0,0 +1,15 @@ +# Artifact Hub repository metadata file +# +# Some settings like the verified publisher flag or the ignored packages won't +# be applied until the next time the repository is processed. Please keep in +# mind that the repository won't be processed if it has not changed since the +# last time it was processed. Depending on the repository kind, this is checked +# in a different way. For Helm http based repositories, we consider it has +# changed if the `index.yaml` file changes. For git based repositories, it does +# when the hash of the last commit in the branch you set up changes. This does +# NOT apply to ownership claim operations, which are processed immediately. +# +repositoryID: 0798fe2d-b881-4cdd-aba9-2d30abdca51b +owners: + - name: lbroudoux + email: laurent.broudoux@gmail.com \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-uber.yml b/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-uber.yml new file mode 100644 index 000000000..c065bf4c2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks-uber.yml @@ -0,0 +1,15 @@ +# Artifact Hub repository metadata file +# +# Some settings like the verified publisher flag or the ignored packages won't +# be applied until the next time the repository is processed. Please keep in +# mind that the repository won't be processed if it has not changed since the +# last time it was processed. Depending on the repository kind, this is checked +# in a different way. For Helm http based repositories, we consider it has +# changed if the `index.yaml` file changes. For git based repositories, it does +# when the hash of the last commit in the branch you set up changes. This does +# NOT apply to ownership claim operations, which are processed immediately. +# +repositoryID: 685e863e-f358-4875-bbb8-7f1607d6b364 +owners: + - name: lbroudoux + email: laurent.broudoux@gmail.com \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks.yml b/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks.yml new file mode 100644 index 000000000..77e496f08 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/metadata/artifacthub-repo-microcks.yml @@ -0,0 +1,15 @@ +# Artifact Hub repository metadata file +# +# Some settings like the verified publisher flag or the ignored packages won't +# be applied until the next time the repository is processed. Please keep in +# mind that the repository won't be processed if it has not changed since the +# last time it was processed. Depending on the repository kind, this is checked +# in a different way. For Helm http based repositories, we consider it has +# changed if the `index.yaml` file changes. For git based repositories, it does +# when the hash of the last commit in the branch you set up changes. This does +# NOT apply to ownership claim operations, which are processed immediately. +# +repositoryID: 0d10e22f-ec36-4fde-8f78-a5d32f827822 +owners: + - name: lbroudoux + email: laurent.broudoux@gmail.com \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/microcks-banner.png b/jdk_21_maven/cs/rest-gui/microcks/microcks-banner.png new file mode 100644 index 000000000..7699cb670 Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/microcks-banner.png differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/apicurio-registry.sh b/jdk_21_maven/cs/rest-gui/microcks/minions/async/apicurio-registry.sh new file mode 100644 index 000000000..986dc8529 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/apicurio-registry.sh @@ -0,0 +1 @@ +docker run -it -p 8888:8080 apicurio/apicurio-registry-mem:2.4.3.Final diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/apicurio-registry.yaml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/apicurio-registry.yaml new file mode 100644 index 000000000..680396ff1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/apicurio-registry.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: apicurio-registry + labels: + app: apicurio-registry +spec: + replicas: 1 + selector: + matchLabels: + app: apicurio-registry + template: + metadata: + labels: + app: apicurio-registry + spec: + containers: + - image: apicurio/apicurio-registry-mem:2.4.3.Final + name: apicurio-registry + imagePullPolicy: Always + resources: + requests: + memory: 300Mi + cpu: 100m + limits: + memory: 400Mi + cpu: 300m + restartPolicy: Always +--- +apiVersion: v1 +kind: Service +metadata: + name: apicurio-registry + labels: + app: apicurio-registry +spec: + selector: + app: apicurio-registry + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP + type: ClusterIP \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis-docker-compose.yml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis-docker-compose.yml new file mode 100644 index 000000000..21dfc5998 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis-docker-compose.yml @@ -0,0 +1,16 @@ +# A docker compose file to start an Artemis AMQP broker +# more details on https://github.com/vromero/activemq-artemis-docker. +version: '2' + +services: + + artemis: + image: vromero/activemq-artemis:2.15.0-alpine + ports: + - "8161:8161" + - "61616:61616" + - "5672:5672" + - "1883:1883" + environment: + ARTEMIS_USERNAME: microcks + ARTEMIS_PASSWORD: microcks \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis.sh b/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis.sh new file mode 100644 index 000000000..969c91b50 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis.sh @@ -0,0 +1 @@ +docker-compose -f artemis-docker-compose.yml up \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis.yaml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis.yaml new file mode 100644 index 000000000..1c30bca4b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/artemis.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: activemq + labels: + app: activemq +spec: + replicas: 1 + selector: + matchLabels: + app: activemq + template: + metadata: + labels: + app: activemq + spec: + containers: + - image: vromero/activemq-artemis:2.15.0-alpine + name: activemq + imagePullPolicy: Always + resources: + requests: + memory: 500Mi + cpu: 200m + limits: + memory: 800Mi + cpu: 400m + env: + - name: ARTEMIS_USERNAME + value: microcks + - name: ARTEMIS_PASSWORD + value: microcks + restartPolicy: Always +--- +apiVersion: v1 +kind: Service +metadata: + name: activemq + labels: + app: activemq +spec: + selector: + app: activemq + ports: + - name: dashboard + port: 8161 + targetPort: 8161 + protocol: TCP + - name: openwire + port: 61616 + targetPort: 61616 + protocol: TCP + - name: amqp + port: 5672 + targetPort: 5672 + protocol: TCP + - name: mqtt + port: 1883 + targetPort: 1883 + protocol: TCP + type: ClusterIP \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-docker-compose-with-registry.yml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-docker-compose-with-registry.yml new file mode 100644 index 000000000..62b01c3b6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-docker-compose-with-registry.yml @@ -0,0 +1,45 @@ +version: '2' + +services: + + zookeeper: + image: quay.io/strimzi/kafka:0.43.0-kafka-3.8.0 + command: [ + "sh", "-c", + "bin/zookeeper-server-start.sh config/zookeeper.properties" + ] + ports: + - "2181:2181" + environment: + LOG_DIR: /tmp/logs + + kafka: + image: quay.io/strimzi/kafka:0.43.0-kafka-3.8.0 + command: [ + "sh", "-c", + "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override listener.security.protocol.map=$${KAFKA_LISTENER_SECURITY_PROTOCOL_MAP} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" + ] + depends_on: + - zookeeper + ports: + - "9092:9092" + - "19092:19092" + environment: + LOG_DIR: "/tmp/logs" + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:19092,EXTERNAL://localhost:9092 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:19092,EXTERNAL://0.0.0.0:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + + schema-registry: + image: confluentinc/cp-schema-registry:7.5.0 + hostname: schema-registry + depends_on: + - zookeeper + - kafka + ports: + - "8889:8889" + environment: + SCHEMA_REGISTRY_HOST_NAME: schema-registry + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: "PLAINTEXT://kafka:19092" + SCHEMA_REGISTRY_LISTENERS: "http://0.0.0.0:8889" diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-docker-compose.yml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-docker-compose.yml new file mode 100644 index 000000000..07c70bb6c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-docker-compose.yml @@ -0,0 +1,30 @@ +version: '2' + +services: + + zookeeper: + image: quay.io/strimzi/kafka:0.30.0-kafka-3.1.0 + command: [ + "sh", "-c", + "bin/zookeeper-server-start.sh config/zookeeper.properties" + ] + ports: + - "2181:2181" + environment: + LOG_DIR: /tmp/logs + + kafka: + image: quay.io/strimzi/kafka:0.30.0-kafka-3.1.0 + command: [ + "sh", "-c", + "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}" + ] + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + LOG_DIR: "/tmp/logs" + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-with-registry.sh b/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-with-registry.sh new file mode 100644 index 000000000..4d970498e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka-with-registry.sh @@ -0,0 +1 @@ +docker compose -f kafka-docker-compose-with-registry.yml up \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka.sh b/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka.sh new file mode 100644 index 000000000..77daa29ff --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/kafka.sh @@ -0,0 +1 @@ +docker-compose -f kafka-docker-compose.yml up diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/nats-docker-compose.yml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/nats-docker-compose.yml new file mode 100644 index 000000000..0147ce2a9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/nats-docker-compose.yml @@ -0,0 +1,10 @@ +version: '2' + +services: + nats: + image: nats:2.9.8-alpine + container_name: nats + ports: + - 4222:4222 + - 6222:6222 + - 8222:8222 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/nats.sh b/jdk_21_maven/cs/rest-gui/microcks/minions/async/nats.sh new file mode 100644 index 000000000..e632309c4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/nats.sh @@ -0,0 +1 @@ +docker-compose -f nats-docker-compose.yml up \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/pom.xml new file mode 100644 index 000000000..e4315ffaf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/pom.xml @@ -0,0 +1,324 @@ + + + + microcks + io.github.microcks + 1.12.2-SNAPSHOT + ../../pom.xml + + 4.0.0 + + Microcks Async Minion + microcks-async-minion + + + ../.. + 3.11.0 + true + 21 + 21 + UTF-8 + UTF-8 + + quarkus-universe-bom + io.quarkus + 3.15.4 + 3.15.4 + + 5.3 + 1.2.5 + 2.21.0 + 5.19.0 + 2.6.8.Final + 6.2.10 + 3.2.5 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + org.testcontainers + testcontainers-bom + 1.19.7 + pom + import + + + + + + + io.quarkus + + quarkus-resteasy-client-jackson + + + io.quarkus + quarkus-resteasy-jackson + + + + com.github.fge + jackson-coreutils + + + + + io.quarkus + quarkus-messaging-kafka + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-quartz + + + io.quarkus + quarkus-websockets + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-registry-prometheus + + + + org.apache.httpcomponents.client5 + httpclient5 + ${apache-httpclient.version} + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + ${eclipse-paho.version} + + + com.rabbitmq + amqp-client + ${rabbitmq.version} + + + + + io.nats + jnats + ${nats.version} + + + + io.quarkiverse.googlecloudservices + quarkus-google-cloud-pubsub + + + + io.quarkiverse.amazonservices + quarkus-amazon-sqs + + + io.quarkiverse.amazonservices + quarkus-amazon-sns + + + software.amazon.awssdk + url-connection-client + + + + io.quarkus + quarkus-apicurio-registry-avro + + + io.apicurio + apicurio-registry-serdes-avro-serde + ${apicurio-registry.version} + + + + io.quarkus + quarkus-confluent-registry-avro + + + io.confluent + kafka-avro-serializer + ${confluent-registry.version} + + + + io.github.microcks + microcks-model + ${project.version} + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + + io.github.microcks + microcks-util + ${project.version} + + + io.github.microcks + microcks-el + ${project.version} + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + rabbitmq + test + + + org.testcontainers + kafka + test + + + + + + confluent + Confluent + https://packages.confluent.io/maven/ + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus-plugin.version} + + + + build + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + + + + + org.jboss.jandex + jandex-maven-plugin + 1.2.3 + + + make-index + + jandex + + + + + + + + + + it + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IT.java + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + native + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/rabbitmq-docker-compose.yml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/rabbitmq-docker-compose.yml new file mode 100644 index 000000000..c46bd56a1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/rabbitmq-docker-compose.yml @@ -0,0 +1,14 @@ +version: '2' + +services: + rabbitmq: + image: rabbitmq:3.9.13-management-alpine + container_name: rabbitmq + environment: + RABBITMQ_ERLANG_COOKIE: "SWQOKODSQALRPCLNMEQG" + RABBITMQ_DEFAULT_USER: microcks + RABBITMQ_DEFAULT_PASS: microcks + RABBITMQ_DEFAULT_VHOST: "/" + ports: + - 5672:5672 + - 15672:15672 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/rabbitmq.sh b/jdk_21_maven/cs/rest-gui/microcks/minions/async/rabbitmq.sh new file mode 100644 index 000000000..9171dce99 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/rabbitmq.sh @@ -0,0 +1 @@ +docker-compose -f rabbitmq-docker-compose.yml up \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/redpanda-docker-compose.yml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/redpanda-docker-compose.yml new file mode 100644 index 000000000..e9bd99762 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/redpanda-docker-compose.yml @@ -0,0 +1,14 @@ +version: '2' + +services: + redpanda: + image: redpandadata/redpanda:v24.3.1 + command: [ + "redpanda", "start", + "--overprovisioned --smp 1 --memory 1G --reserve-memory 0M --node-id 0 --check=false", + "--kafka-addr PLAINTEXT://0.0.0.0:19092,EXTERNAL://0.0.0.0:9092", + "--advertise-kafka-addr PLAINTEXT://kafka:19092,EXTERNAL://localhost:9092" + ] + ports: + - "9092:9092" + - "19092:19092" \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/redpanda.sh b/jdk_21_maven/cs/rest-gui/microcks/minions/async/redpanda.sh new file mode 100644 index 000000000..f14b466ab --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/redpanda.sh @@ -0,0 +1 @@ +docker compose -f redpanda-docker-compose.yml up diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.jvm b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.jvm new file mode 100644 index 000000000..f4e04d410 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.jvm @@ -0,0 +1,59 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the docker image run: +# +# mvn package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t microcks/microcks-async-minion-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 microcks/microcks-async-minion-jvm +# +### +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5-1742914212 + +# Some version information +LABEL maintainer="Laurent Broudoux " \ + org.opencontainers.image.authors="Laurent Broudoux " \ + org.opencontainers.image.title="Microcks Async Minion" \ + org.opencontainers.image.description="Microcks is Open Source cloud-native native tool for API Mocking and Testing" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.documentation="https://github.com/microcks/microcks/tree/master/minions/async" \ + io.artifacthub.package.readme-url="https://raw.githubusercontent.com/microcks/microcks/master/README.md" + +ARG JAVA_PACKAGE=java-21-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl-minimal ca-certificates ${JAVA_PACKAGE} -y \ + && microdnf update -y \ + && microdnf clean all \ + && rm /var/lib/rpm/rpmdb.sqlite \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 550 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +COPY target/quarkus-app/lib/ /deployments/lib/ +COPY target/quarkus-app/*.jar /deployments/ +COPY target/quarkus-app/app/ /deployments/app/ +COPY target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.legacy-jar b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 000000000..2fcc48586 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,60 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the docker image run: +# +# mvn package -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t microcks/microcks-async-minion-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 microcks/microcks-async-minion-legacy-jar +# +### +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5-1742914212 + +# Some version information +LABEL maintainer="Laurent Broudoux " \ + org.opencontainers.image.authors="Laurent Broudoux " \ + org.opencontainers.image.title="Microcks Async Minion" \ + org.opencontainers.image.description="Microcks is Open Source cloud-native native tool for API Mocking and Testing" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.documentation="https://github.com/microcks/microcks/tree/master/minions/async" \ + io.artifacthub.package.readme-url="https://raw.githubusercontent.com/microcks/microcks/master/README.md" + +ARG JAVA_PACKAGE=java-21-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl-minimal ca-certificates ${JAVA_PACKAGE} -y \ + && microdnf update -y \ + && microdnf clean all \ + && rm /var/lib/rpm/rpmdb.sqlite \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 550 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +ENV JAVA_APP_DIR="/deployments" +ENV JAVA_MAIN_CLASS="io.quarkus.runner.GeneratedMain" + +COPY target/lib/ /deployments/lib/ +COPY target/*-runner.jar /deployments/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.native b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.native new file mode 100644 index 000000000..a05b32e2a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/docker/Dockerfile.native @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode +# +# Before building the docker image run: +# +# mvn package -Pnative -Dquarkus.native.container-build=true +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t microcks/microcks-async-minion . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 microcks/microcks-async-minion +# +### +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5-1742914212 +WORKDIR /work/ +COPY --chown=1001:root target/*-runner /work/application + +# set up permissions for user `1001` +RUN chmod 775 /work /work/application \ + && chown -R 1001 /work \ + && chmod -R "g+rwX" /work \ + && chown -R 1001:root /work + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncAPITestManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncAPITestManager.java new file mode 100644 index 000000000..174bb6cfb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncAPITestManager.java @@ -0,0 +1,331 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.minion.async.client.MicrocksAPIConnector; +import io.github.microcks.minion.async.client.dto.TestCaseReturnDTO; +import io.github.microcks.minion.async.consumer.AMQPMessageConsumptionTask; +import io.github.microcks.minion.async.consumer.AmazonSNSMessageConsumptionTask; +import io.github.microcks.minion.async.consumer.AmazonSQSMessageConsumptionTask; +import io.github.microcks.minion.async.consumer.ConsumedMessage; +import io.github.microcks.minion.async.consumer.GooglePubSubMessageConsumptionTask; +import io.github.microcks.minion.async.consumer.KafkaMessageConsumptionTask; +import io.github.microcks.minion.async.consumer.MQTTMessageConsumptionTask; +import io.github.microcks.minion.async.consumer.MessageConsumptionTask; +import io.github.microcks.minion.async.consumer.WebSocketMessageConsumptionTask; +import io.github.microcks.minion.async.consumer.NATSMessageConsumptionTask; +import io.github.microcks.util.SchemaMap; +import io.github.microcks.util.asyncapi.AsyncAPISchemaUtil; +import io.github.microcks.util.asyncapi.AsyncAPISchemaValidator; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Manager that takes care of launching and running an AsyncAPI test from an AsyncTestSpecification. + * @author laurent + */ +@ApplicationScoped +public class AsyncAPITestManager { + + private final MicrocksAPIConnector microcksAPIConnector; + private final SchemaRegistry schemaRegistry; + + @ConfigProperty(name = "io.github.microcks.minion.async.client.MicrocksAPIConnector/mp-rest/url") + String microcksUrl; + + /** + * Create a new AsyncAPITestManager. + * @param microcksAPIConnector The MicrocksAPIConnector to use for reporting test results. + * @param schemaRegistry The SchemaRegistry to use for schema validation. + */ + public AsyncAPITestManager(@RestClient MicrocksAPIConnector microcksAPIConnector, SchemaRegistry schemaRegistry) { + this.microcksAPIConnector = microcksAPIConnector; + this.schemaRegistry = schemaRegistry; + } + + /** + * Launch a new test using this specification. This is a fire and forget operation. Tests results will be reported + * later once finished using a MicrocksAPIConnector. + * @param specification The specification of Async test to launch. + */ + public void launchTest(AsyncTestSpecification specification) { + AsyncAPITestThread thread = new AsyncAPITestThread(specification); + thread.start(); + } + + /** + * Actually run the Async test by instantiating a MessageConsumptionTask, gathering its outputs and + * validating them against an AsyncAPI schema. + */ + class AsyncAPITestThread extends Thread { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private final AsyncTestSpecification specification; + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + private long startTime; + + /** + * Build a new test thread from a test specification. + * @param specification The specification for async test to run. + */ + public AsyncAPITestThread(AsyncTestSpecification specification) { + this.specification = specification; + } + + @Override + public void run() { + logger.infof("Launching a new AsyncAPITestThread for {%s} on {%s}", specification.getTestResultId(), + specification.getEndpointUrl()); + + // Start initialising a TestCaseReturn and build the correct MessageConsumptionTasK. + TestCaseReturnDTO testCaseReturn = new TestCaseReturnDTO(specification.getOperationName()); + MessageConsumptionTask messageConsumptionTask = buildMessageConsumptionTask(specification); + + // Actually start test counter. + startTime = System.currentTimeMillis(); + List outputs = null; + + if (messageConsumptionTask != null) { + try { + logger.debugf("Starting consuming messages for {%d} ms", specification.getTimeoutMS()); + // Adding an extra seconds to allow to close and clean all the machinery ;-) + outputs = executorService.invokeAny(Collections.singletonList(messageConsumptionTask), + specification.getTimeoutMS() + 1000L, TimeUnit.MILLISECONDS); + logger.debugf("Consumption ends and we got {%d} messages to validate", outputs.size()); + } catch (InterruptedException e) { + logger.infof("AsyncAPITestThread for {%s} was interrupted", specification.getTestResultId()); + } catch (ExecutionException e) { + logger.errorf(e, "AsyncAPITestThread for {%s} raise an ExecutionException", + specification.getTestResultId()); + testCaseReturn.addTestReturn(new TestReturn(TestReturn.FAILURE_CODE, specification.getTimeoutMS(), + "ExecutionException: no message received in " + specification.getTimeoutMS() + " ms", null, null)); + } catch (TimeoutException e) { + // Message consumption has timed-out, add an empty test return with failure and message. + logger.infof("AsyncAPITestThread for {%s} was timed-out", specification.getTestResultId()); + testCaseReturn.addTestReturn(new TestReturn(TestReturn.FAILURE_CODE, specification.getTimeoutMS(), + "Timeout: no message received in " + specification.getTimeoutMS() + " ms", null, null)); + } catch (Throwable t) { + // We faced a low-level issue... add an empty test return with failure and message. + logger.error("Caught a low-level throwable", t); + testCaseReturn + .addTestReturn(new TestReturn(TestReturn.FAILURE_CODE, System.currentTimeMillis() - startTime, + "Exception: low-level failure: " + t.getMessage(), null, null)); + } finally { + try { + messageConsumptionTask.close(); + } catch (Throwable t) { + // This was best effort, just ignore the exception... + } + executorService.shutdown(); + } + } else { + logger.errorf("Found no suitable MessageConsumptionTask implementation. {%s} is not a supported endpoint", + specification.getEndpointUrl()); + testCaseReturn.addTestReturn(new TestReturn(TestReturn.FAILURE_CODE, System.currentTimeMillis() - startTime, + "Exception: found no suitable MessageConsumptionTask implementation for endpoint", null, null)); + } + + // Validate all the received outputs if any. + if (outputs != null && !outputs.isEmpty()) { + validateConsumedMessages(testCaseReturn, outputs); + } else { + logger.infof("No consumed message to validate, test {%s} will be marked as timed-out", + specification.getTestResultId()); + } + + // Finally, report the testCase results using Microcks API. + microcksAPIConnector.reportTestCaseResult(specification.getTestResultId(), testCaseReturn); + } + + /** + * Validate consumed messages and complete {@code testCaseReturn} with {@code TestReturn} objects. + * @param testCaseReturn The TestCase to complete with schema validation results + * @param outputs The consumed messages from tested endpoint. + */ + private void validateConsumedMessages(TestCaseReturnDTO testCaseReturn, List outputs) { + JsonNode specificationNode = null; + try { + specificationNode = AsyncAPISchemaValidator.getJsonNodeForSchema(specification.getAsyncAPISpec()); + } catch (IOException e) { + logger.errorf("Retrieval of AsyncAPI schema for validation fails for {%s}", + specification.getTestResultId()); + } + + // Compute message JSON pointer to navigate the spec. + String messagePathPointer = AsyncAPISchemaUtil.findMessagePathPointer(specificationNode, + specification.getOperationName()); + + // Retrieve expected content type from specification and produce a schema registry snapshot. + String expectedContentType = null; + SchemaMap schemaMap = new SchemaMap(); + if (specificationNode != null) { + expectedContentType = getExpectedContentType(specificationNode, messagePathPointer); + if (Constants.AVRO_BINARY_CONTENT_TYPES.contains(expectedContentType)) { + logger.debug("Expected content type is Avro so extracting service resources into a SchemaMap"); + schemaRegistry.updateRegistryForService(specification.getServiceId()); + schemaRegistry.getSchemaEntries(specification.getServiceId()).stream() + .forEach(schemaEntry -> schemaMap.putSchemaEntry(schemaEntry.getPath(), schemaEntry.getContent())); + } + } + + for (int i = 0; i < outputs.size(); i++) { + // Treat each message and compute elapsed time of each. + ConsumedMessage message = outputs.get(i); + long elapsedTime = message.getReceivedAt() - startTime; + + // Initialize an EventMessage with message content. + String responseContent = (message.getPayload() != null) ? new String(message.getPayload()) + : message.getPayloadRecord().toString(); + + EventMessage eventMessage = new EventMessage(); + eventMessage.setName(specification.getTestResultId() + "-" + specification.getOperationName() + "-" + i); + eventMessage.setMediaType(expectedContentType); + eventMessage.setContent(responseContent); + eventMessage.setHeaders(message.getHeaders()); + + TestReturn testReturn; + + if (specificationNode == null) { + logger.infof("AsyncAPI specification cannot be read, so test {%s} cannot be validated", + specification.getTestResultId()); + testReturn = new TestReturn(TestReturn.FAILURE_CODE, elapsedTime, + "AsyncAPI specification cannot be read, thus message cannot be validated", eventMessage); + } else if (expectedContentType == null) { + logger.infof("Expected content-type cannot be determined, so test {%s} cannot be validated", + specification.getTestResultId()); + testReturn = new TestReturn(TestReturn.FAILURE_CODE, elapsedTime, + "Content-Type cannot be determined, thus message cannot be validated", eventMessage); + } else { + // We now should have everything to perform validation, go ahead! + try { + logger.infof("Validating received message {%s} against {%s}", responseContent, messagePathPointer); + List errors = null; + if (Constants.AVRO_BINARY_CONTENT_TYPES.contains(expectedContentType)) { + // Use the correct Avro message validation method depending on what has been read. + if (message.getPayloadRecord() != null) { + errors = AsyncAPISchemaValidator.validateAvroMessage(specificationNode, + message.getPayloadRecord(), messagePathPointer, schemaMap); + } else { + errors = AsyncAPISchemaValidator.validateAvroMessage(specificationNode, message.getPayload(), + messagePathPointer, schemaMap); + } + } else if (expectedContentType.contains("application/json")) { + // Now parse the payloadNode and validate it according the operation message + // found in specificationNode. + JsonNode payloadNode = AsyncAPISchemaValidator.getJsonNode(responseContent); + errors = AsyncAPISchemaValidator.validateJsonMessage(specificationNode, payloadNode, + messagePathPointer, microcksUrl + "/api/resources/"); + } + + if (errors == null || errors.isEmpty()) { + // This is a success. + logger.infof("No errors found while validating message payload! Reporting a success for test {%s}", + specification.getTestResultId()); + testReturn = new TestReturn(TestReturn.SUCCESS_CODE, elapsedTime, eventMessage); + } else { + // This is a failure. Records all errors using \\n as delimiter. + logger.infof("Validation errors found... Reporting a failure for test {%s}", + specification.getTestResultId()); + testReturn = new TestReturn(TestReturn.FAILURE_CODE, elapsedTime, String.join("\\n", errors), + eventMessage); + } + } catch (IOException e) { + logger.error("Exception while parsing the output message", e); + testReturn = new TestReturn(TestReturn.FAILURE_CODE, elapsedTime, + "Message content cannot be parsed as JSON", eventMessage); + } + } + testCaseReturn.addTestReturn(testReturn); + } + } + + /** Retrieve the expected content type for an AsyncAPI message. */ + private String getExpectedContentType(JsonNode specificationNode, String messagePathPointer) { + // Retrieve default content type, defaulting to application/json. + String defaultContentType = specificationNode.path("defaultContentType").asText("application/json"); + + // Get message real content type if defined. + String contentType = defaultContentType; + JsonNode messageNode = specificationNode.at(messagePathPointer); + + // messageNode will be an array of messages + if (messageNode.isArray() && messageNode.size() > 0) { + messageNode = messageNode.get(0); + } + + // If it's a $ref, then navigate to it. + while (messageNode.has("$ref")) { + // $ref: '#/components/messages/lightMeasured' + String ref = messageNode.path("$ref").asText(); + messageNode = specificationNode.at(ref.substring(1)); + } + if (messageNode.has("contentType")) { + contentType = messageNode.path("contentType").asText(); + } + return contentType; + } + + /** Find the appropriate MessageConsumptionTask implementation depending on specification. */ + private MessageConsumptionTask buildMessageConsumptionTask(AsyncTestSpecification testSpecification) { + if (KafkaMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) { + return new KafkaMessageConsumptionTask(testSpecification); + } + if (MQTTMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) { + return new MQTTMessageConsumptionTask(testSpecification); + } + if (NATSMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) { + return new NATSMessageConsumptionTask(testSpecification); + } + if (WebSocketMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) { + return new WebSocketMessageConsumptionTask(testSpecification); + } + if (AMQPMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) { + return new AMQPMessageConsumptionTask(testSpecification); + } + if (GooglePubSubMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) { + return new GooglePubSubMessageConsumptionTask(testSpecification); + } + if (AmazonSQSMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) { + return new AmazonSQSMessageConsumptionTask(testSpecification); + } + if (AmazonSNSMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) { + return new AmazonSNSMessageConsumptionTask(testSpecification); + } + return null; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMinionApp.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMinionApp.java new file mode 100644 index 000000000..d675ccde8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMinionApp.java @@ -0,0 +1,166 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.ServiceView; +import io.github.microcks.domain.UnidirectionalEvent; + +import io.github.microcks.minion.async.client.ConnectorException; +import io.github.microcks.minion.async.client.KeycloakConfig; +import io.github.microcks.minion.async.client.KeycloakConnector; +import io.github.microcks.minion.async.client.MicrocksAPIConnector; + +import io.quarkus.runtime.StartupEvent; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * A Minion App for dealing with Async message mocks. + * @author laurent + */ +@ApplicationScoped +public class AsyncMinionApp { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private static final int SERVICES_FETCH_SIZE = 30; + + @ConfigProperty(name = "minion.supported-bindings") + String[] supportedBindings; + + @ConfigProperty(name = "minion.restricted-frequencies") + Long[] restrictedFrequencies; + + @ConfigProperty(name = "keycloak.auth.url", defaultValue = "") + Optional keycloakAuthURL; + + final MicrocksAPIConnector microcksAPIConnector; + final KeycloakConnector keycloakConnector; + final AsyncMockRepository mockRepository; + final SchemaRegistry schemaRegistry; + final ProducerScheduler producerScheduler; + + /** + * Build a new instance of the AsyncMinionApp with required dependencies. + * @param microcksAPIConnector to access Microcks API + * @param keycloakConnector to access Keycloak server for authentication + * @param mockRepository to store and retrieve mock definitions + * @param schemaRegistry to store and retrieve schema definitions + * @param producerScheduler to initiate producer jobs + */ + public AsyncMinionApp(@RestClient MicrocksAPIConnector microcksAPIConnector, KeycloakConnector keycloakConnector, + AsyncMockRepository mockRepository, SchemaRegistry schemaRegistry, ProducerScheduler producerScheduler) { + this.microcksAPIConnector = microcksAPIConnector; + this.keycloakConnector = keycloakConnector; + this.mockRepository = mockRepository; + this.schemaRegistry = schemaRegistry; + this.producerScheduler = producerScheduler; + } + + + /** Application startup method. */ + void onStart(@Observes StartupEvent ev) { + + // We need to retrieve Keycloak server from Microcks config. + KeycloakConfig config = microcksAPIConnector.getKeycloakConfig(); + logger.infof("Microcks Keycloak server url {%s} and realm {%s}", config.getAuthServerUrl(), config.getRealm()); + + String keycloakEndpoint = config.getAuthServerUrl() + "/realms/" + config.getRealm() + + "/protocol/openid-connect/token"; + if (!keycloakAuthURL.isEmpty() && keycloakAuthURL.get().length() > 0) { + logger.infof("Use locally defined Keycloak Auth URL: %s", keycloakAuthURL); + keycloakEndpoint = keycloakAuthURL.get() + "/realms/" + config.getRealm() + "/protocol/openid-connect/token"; + } + + try { + // First retrieve an authentication token before fetching async messages to publish. + String oauthToken; + if (config.isEnabled()) { + // We've got a full Keycloak config, attempt an authent. + oauthToken = keycloakConnector.connectAndGetOAuthToken(keycloakEndpoint); + logger.info("Authentication to Keycloak server succeed!"); + } else { + // No realm config, probably a dev mode - use a fake token. + oauthToken = ""; + logger.info("Keycloak protection is not enabled, using a fake token"); + } + + int page = 0; + boolean fetchServices = true; + while (fetchServices) { + List services = microcksAPIConnector.listServices("Bearer " + oauthToken, page, + SERVICES_FETCH_SIZE); + for (Service service : services) { + logger.debug("Found service " + service.getName() + " - " + service.getVersion()); + + if (service.getType().equals(ServiceType.EVENT) || service.getType().equals(ServiceType.GENERIC_EVENT)) { + // Find the operations matching this minion constraints.. + List operations = service.getOperations().stream() + .filter(o -> Arrays.asList(restrictedFrequencies).contains(o.getDefaultDelay())).filter(o -> o + .getBindings().keySet().stream().anyMatch(Arrays.asList(supportedBindings)::contains)) + .toList(); + + if (!operations.isEmpty()) { + logger.info("Found " + operations.size() + " candidate operations in " + service.getName() + " - " + + service.getVersion()); + ServiceView serviceView = microcksAPIConnector.getService("Bearer " + oauthToken, service.getId(), + true); + + for (Operation operation : operations) { + AsyncMockDefinition mockDefinition = new AsyncMockDefinition(serviceView.getService(), + operation, + serviceView.getMessagesMap().get(operation.getName()).stream() + .filter(UnidirectionalEvent.class::isInstance) + .map(e -> ((UnidirectionalEvent) e).getEventMessage()).toList()); + mockRepository.storeMockDefinition(mockDefinition); + schemaRegistry.updateRegistryForService(mockDefinition.getOwnerService()); + } + } + } + } + if (services.size() < SERVICES_FETCH_SIZE) { + fetchServices = false; + } + page++; + } + + logger.info("Starting scheduling of all producer jobs..."); + producerScheduler.scheduleAllProducerJobs(); + + } catch (ConnectorException ce) { + logger.error("Cannot authenticate to Keycloak server and thus enable to call Microcks API" + + " to get Async APIs to mocks...", ce); + throw new RuntimeException("Unable to start the Minion due to connection exception"); + } catch (IOException ioe) { + logger.error("IOException while communicating with Keycloak or Microcks API", ioe); + throw new RuntimeException("Unable to start the Minion due to IO exception"); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockDefinition.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockDefinition.java new file mode 100644 index 000000000..94fa40fcd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockDefinition.java @@ -0,0 +1,80 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; + +import java.util.List; +import java.util.Objects; + +/** + * This is a wrapper bean that holds needed elements for async API operation mocking. + * @author laurent + */ +public class AsyncMockDefinition { + + private Service ownerService; + private Operation operation; + private List eventMessages; + + public AsyncMockDefinition(Service ownerService, Operation operation, List eventMessages) { + this.ownerService = ownerService; + this.operation = operation; + this.eventMessages = eventMessages; + } + + public Service getOwnerService() { + return ownerService; + } + + public void setOwnerService(Service ownerService) { + this.ownerService = ownerService; + } + + public Operation getOperation() { + return operation; + } + + public void setOperation(Operation operation) { + this.operation = operation; + } + + public List getEventMessages() { + return eventMessages; + } + + public void setEventMessages(List eventMessages) { + this.eventMessages = eventMessages; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + AsyncMockDefinition that = (AsyncMockDefinition) o; + return ownerService.getId().equals(that.ownerService.getId()) + && operation.getName().equals(that.operation.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(ownerService.getId(), operation.getName()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockDefinitionUpdater.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockDefinitionUpdater.java new file mode 100644 index 000000000..b11bea46d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockDefinitionUpdater.java @@ -0,0 +1,115 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.ServiceView; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.event.ChangeType; +import io.github.microcks.event.ServiceViewChangeEvent; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.jboss.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; +import java.util.Arrays; + +/** + * This bean is responsible for listening the incoming ServiceViewChangeEvent on + * microcks-services-updates Kafka topic and updating the AsyncMockRepository accordingly. + * @author laurent + */ +@ApplicationScoped +public class AsyncMockDefinitionUpdater { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + @ConfigProperty(name = "minion.supported-bindings") + String[] supportedBindings; + + @ConfigProperty(name = "minion.restricted-frequencies") + Long[] restrictedFrequencies; + + final AsyncMockRepository mockRepository; + final SchemaRegistry schemaRegistry; + + /** + * Create a new AsyncMockDefinitionUpdater with required dependencies. + * @param mockRepository The repository for mock definitions + * @param schemaRegistry The registry for schema definitions + */ + public AsyncMockDefinitionUpdater(AsyncMockRepository mockRepository, SchemaRegistry schemaRegistry) { + this.mockRepository = mockRepository; + this.schemaRegistry = schemaRegistry; + } + + @Incoming("microcks-services-updates") + public void onServiceUpdate(ServiceViewChangeEvent serviceViewChangeEvent) { + logger.infof("Received a new change event [%s] for '%s', at %d", serviceViewChangeEvent.getChangeType(), + serviceViewChangeEvent.getServiceId(), serviceViewChangeEvent.getTimestamp()); + + applyServiceChangeEvent(serviceViewChangeEvent); + } + + public void applyServiceChangeEvent(ServiceViewChangeEvent serviceViewChangeEvent) { + // Remove existing definitions or add/update existing for EVENT services. + if (serviceViewChangeEvent.getChangeType().equals(ChangeType.DELETED)) { + logger.infof("Removing mock definitions for %s", serviceViewChangeEvent.getServiceId()); + mockRepository.removeMockDefinitions(serviceViewChangeEvent.getServiceId()); + schemaRegistry.clearRegistryForService(serviceViewChangeEvent.getServiceId()); + } else { + // Only deal with service of type EVENT... + if (serviceViewChangeEvent.getServiceView() != null && (serviceViewChangeEvent.getServiceView().getService() + .getType().equals(ServiceType.EVENT) + || serviceViewChangeEvent.getServiceView().getService().getType().equals(ServiceType.GENERIC_EVENT))) { + + // Browse and check operation regarding restricted frequencies and supported bindings. + boolean scheduled = scheduleOperations(serviceViewChangeEvent.getServiceView()); + + if (!scheduled) { + logger.infof("Ensure to un-schedule %s on this minion. Removing definitions.", + serviceViewChangeEvent.getServiceId()); + mockRepository.removeMockDefinitions(serviceViewChangeEvent.getServiceId()); + schemaRegistry.clearRegistryForService(serviceViewChangeEvent.getServiceId()); + } + } + } + } + + /** Browse and check operation regarding restricted frequencies and supported bindings. */ + private boolean scheduleOperations(ServiceView serviceView) { + boolean scheduled = false; + for (Operation operation : serviceView.getService().getOperations()) { + if (Arrays.asList(restrictedFrequencies).contains(operation.getDefaultDelay()) + && operation.getBindings().keySet().stream().anyMatch(Arrays.asList(supportedBindings)::contains)) { + + logger.info("Found '" + operation.getName() + "' as a candidate for async message mocking"); + // Build an Async mock definition and store it into repository. + AsyncMockDefinition mockDefinition = new AsyncMockDefinition(serviceView.getService(), operation, + serviceView.getMessagesMap().get(operation.getName()).stream() + .filter(UnidirectionalEvent.class::isInstance) + .map(e -> ((UnidirectionalEvent) e).getEventMessage()).toList()); + mockRepository.storeMockDefinition(mockDefinition); + schemaRegistry.updateRegistryForService(mockDefinition.getOwnerService()); + scheduled = true; + } + } + return scheduled; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockRepository.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockRepository.java new file mode 100644 index 000000000..a77a449f9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncMockRepository.java @@ -0,0 +1,92 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +@ApplicationScoped +/** + * Repository for AsyncMockDefinitions. Used as a backend storage for jobs that have to publish event messages at + * specified frequencies. Has to be initialized at application startup and regularly keep-in-sync with Microcks server. + * @author laurent + */ +public class AsyncMockRepository { + + private Set mockDefinitions = new HashSet<>(); + + /** + * Retrieve the AsyncMockDefinitions present in store. + * @return A set of AsyncMockDefinitions + */ + public Set getMocksDefinitions() { + return mockDefinitions; + } + + /** + * Store a new or update an existing AsyncMockDefinition in store. + * @param mockDefinition Definition to store or update in store + */ + public void storeMockDefinition(AsyncMockDefinition mockDefinition) { + if (mockDefinitions.contains(mockDefinition)) { + mockDefinitions.remove(mockDefinition); + } + mockDefinitions.add(mockDefinition); + } + + /** + * Remove the AsyncMockDefinitions corresponding to owner Service id. + * @param serviceId The identifier of owner Service. + */ + public void removeMockDefinitions(String serviceId) { + Set serviceDefs = mockDefinitions.stream() + .filter(d -> d.getOwnerService().getId().equals(serviceId)).collect(Collectors.toSet()); + mockDefinitions.removeAll(serviceDefs); + } + + /** + * Retrieve the set of frequencies of Operation found within definitions in store. + * @return A set of frequencies for definitions operations + */ + public Set getMockDefinitionsFrequencies() { + return mockDefinitions.stream().map(d -> d.getOperation().getDefaultDelay()).collect(Collectors.toSet()); + } + + /** + * Retrieve all the AsyncMockDefinition corresponding to a specified operation frequency. + * @param frequency The operation frequency to get definitions for + * @return A set of AsyncMockDefinition having specified operation frequency + */ + public Set getMockDefinitionsByFrequency(Long frequency) { + return mockDefinitions.stream().filter(d -> d.getOperation().getDefaultDelay().equals(frequency)) + .collect(Collectors.toSet()); + } + + /** + * Retrieve the AsyncMockDefinition corresponding to a specified service and version. + * @param serviceName The service name to get definition for + * @param version The service version to get definition for + * @return Should return an empty of 1 element set only. + */ + public Set getMockDefinitionsByServiceAndVersion(String serviceName, String version) { + return mockDefinitions.stream().filter( + d -> d.getOwnerService().getName().equals(serviceName) && d.getOwnerService().getVersion().equals(version)) + .collect(Collectors.toSet()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncTestSpecification.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncTestSpecification.java new file mode 100644 index 000000000..49882d4a3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/AsyncTestSpecification.java @@ -0,0 +1,102 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.TestRunnerType; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * This is a bean representing the specification of an Asynchronous test using AsyncAPI specification elements. + * @author laurent + */ +@RegisterForReflection +public class AsyncTestSpecification { + + private TestRunnerType runnerType; + private String testResultId; + private String serviceId; + private String operationName; + private String endpointUrl; + private String asyncAPISpec; + private Secret secret; + private Long timeoutMS; + + public TestRunnerType getRunnerType() { + return runnerType; + } + + public void setRunnerType(TestRunnerType runnerType) { + this.runnerType = runnerType; + } + + public String getTestResultId() { + return testResultId; + } + + public void setTestResultId(String testResultId) { + this.testResultId = testResultId; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getOperationName() { + return operationName; + } + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public String getEndpointUrl() { + return endpointUrl; + } + + public void setEndpointUrl(String endpointUrl) { + this.endpointUrl = endpointUrl; + } + + public String getAsyncAPISpec() { + return asyncAPISpec; + } + + public void setAsyncAPISpec(String asyncAPISpec) { + this.asyncAPISpec = asyncAPISpec; + } + + public Secret getSecret() { + return secret; + } + + public void setSecret(Secret secret) { + this.secret = secret; + } + + public Long getTimeoutMS() { + return timeoutMS; + } + + public void setTimeoutMS(Long timeoutMS) { + this.timeoutMS = timeoutMS; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/Constants.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/Constants.java new file mode 100644 index 000000000..0e5aee02c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/Constants.java @@ -0,0 +1,39 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import java.util.Arrays; +import java.util.List; + +/** + * Some constants related to asynchronous messages used by many classes (producers, consumers, clients) + * @author laurent + */ +public class Constants { + + /** Setup parameter telling that we have to connect to a Schema Registry for writing Avro schemas used on mocking. */ + public static final String REGISTRY_AVRO_ENCODING = "REGISTRY"; + + /** Supported content-types representing Avro binary encoded content. */ + public static final List AVRO_BINARY_CONTENT_TYPES = Arrays.asList("avro/binary", "application/octet-stream", + "application/avro"); + + /** Constant identifying that AMQP destination is a queue. */ + public static final String AMQP_QUEUE_TYPE = "queue"; + + /** Constants identifying that AMQP destination is an exchange. */ + public static final List AMQP_EXCHANGE_TYPES = Arrays.asList("direct", "topic", "fanout", "headers"); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/ProducerScheduler.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/ProducerScheduler.java new file mode 100644 index 000000000..8caee1316 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/ProducerScheduler.java @@ -0,0 +1,124 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.minion.async.producer.ProducerManager; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.annotations.RegisterForReflection; +import io.quarkus.scheduler.Scheduled; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.ArrayList; +import java.util.List; + +/** + * Bean responsible for Async mock messages producers scheduling. + * @author laurent + */ +@ApplicationScoped +public class ProducerScheduler { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private final List triggerKeys = new ArrayList<>(); + + final Scheduler quartz; + final AsyncMockRepository mockRepository; + + @ConfigProperty(name = "minion.restricted-frequencies") + Long[] restrictedFrequencies; + + /** + * Create a new ProducerScheduler with required dependencies. + * @param quartz The Quartz scheduler + * @param mockRepository The repository for mock definitions + */ + public ProducerScheduler(Scheduler quartz, AsyncMockRepository mockRepository) { + this.quartz = quartz; + this.mockRepository = mockRepository; + } + + + /** Perform a dummy action. This one is actually necessary to activate the injection of Quartz scheduler. */ + @Scheduled(every = "24h") + public void performAction() { + logger.debug("Performing dummy action each day to allow Quartz activation"); + } + + + /** Schedule all the producer jobs for configured frequencies. */ + public void scheduleAllProducerJobs() { + for (Long frequency : restrictedFrequencies) { + scheduleProducerForFrequency(frequency); + } + } + + /** Unschedule all producer jobs for configured frequencies. */ + public void unscheduleAllProducerJobs() { + try { + quartz.unscheduleJobs(triggerKeys); + triggerKeys.clear(); + } catch (SchedulerException e) { + logger.error("Failure while unscheduling all producer jobs", e); + } + } + + /** Inner class implementing Quartz Job and deleting job execution to ProducerManager bean. */ + @RegisterForReflection + public static class AsyncMockProducerJob implements Job { + public void execute(JobExecutionContext context) throws JobExecutionException { + Long frequency = context.getJobDetail().getJobDataMap().getLong("frequency"); + Arc.container().instance(ProducerManager.class).get().produceAsyncMockMessagesAt(frequency); + } + } + + /** Schedule a Quartz Job and associate trigger for specified frewqency. */ + private void scheduleProducerForFrequency(Long frequency) { + logger.info("Scheduling a new Producer Job at frequency " + frequency); + + // Create a job detail which is holding its frequency as a param. + JobDetail jobDetail = JobBuilder.newJob(AsyncMockProducerJob.class).usingJobData("frequency", frequency).build(); + + // Create a trigger for this job, executing each 'frequency' seconds. + Trigger trigger = TriggerBuilder.newTrigger().forJob(jobDetail).withIdentity(String.valueOf(frequency)).startNow() + .withSchedule( + SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds((int) (long) frequency).repeatForever()) + .build(); + + try { + quartz.scheduleJob(jobDetail, trigger); + triggerKeys.add(trigger.getKey()); + } catch (SchedulerException e) { + logger.error("Failure while scheduling producer job for frequency " + frequency, e); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/SchemaRegistry.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/SchemaRegistry.java new file mode 100644 index 000000000..eec939157 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/SchemaRegistry.java @@ -0,0 +1,193 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.minion.async.client.MicrocksAPIConnector; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This is a simple and local registry for Async APIs schemas. It is used as a local proxy to Services Resources that + * are available within Microcks instance. This registry uses the MicrocksAPIConnector to retrieve + * resources/schemas from instance. + * @author laurent + */ +@ApplicationScoped +public class SchemaRegistry { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private final MicrocksAPIConnector microcksAPIConnector; + + /** + * Create a new SchemaRegistry instance. + * @param microcksAPIConnector The MicrocksAPIConnector to use for retrieving resources + */ + public SchemaRegistry(@RestClient MicrocksAPIConnector microcksAPIConnector) { + this.microcksAPIConnector = microcksAPIConnector; + } + + /** The internal map backing schemas storage. This is an in-memory map. */ + private Map> schemaEntries = new HashMap<>(); + + /** + * Updating the registry with resources attached to specified service. + * @param service The Service to retrieve resources for and update registry with + */ + public void updateRegistryForService(Service service) { + clearRegistryForService(service); + List resources = microcksAPIConnector.getResources(service.getId()); + logger.infof("Updating schema registry for '%s - %s' with %d entries", service.getName(), service.getVersion(), + resources.size()); + schemaEntries.put(service.getId(), resources.stream().map(SchemaEntry::new).toList()); + } + + /** + * Updating the registry with resources attached to specified service. + * @param serviceId The Id of service to retrieve resources for and update registry with + */ + public void updateRegistryForService(String serviceId) { + clearRegistryForService(serviceId); + List resources = microcksAPIConnector.getResources(serviceId); + logger.infof("Updating schema registry for Service '%s' with %d entries", serviceId, resources.size()); + schemaEntries.put(serviceId, resources.stream().map(SchemaEntry::new).toList()); + } + + /** + * Retrieve the schema entries for a specified Service. + * @param service The Service to get schema entries for + * @return A list of {@code SchemaEntry} + */ + public List getSchemaEntries(Service service) { + return getSchemaEntries(service.getId()); + } + + /** + * Retrieve the schema entries for a specified Service. + * @param serviceId The Id of service to get schema entries for + * @return A list of {@code SchemaEntry} + */ + public List getSchemaEntries(String serviceId) { + // Do we have local entries already ? If not, retrieve them. + List entries = schemaEntries.get(serviceId); + if (entries == null) { + updateRegistryForService(serviceId); + entries = schemaEntries.get(serviceId); + } + return entries; + } + + /** + * Get the content (ie. actual schema specification) of a Schema entry in the scope of a specified service. + * @param service The Service to get schema for + * @param schemaName The name of the Schema entry to retrieve content for + * @return The schema entry content or null if not found. + */ + public String getSchemaEntryContent(Service service, String schemaName) { + // Do we have local entries already ? If not, retrieve them. + List entries = schemaEntries.get(service.getId()); + if (entries == null) { + updateRegistryForService(service); + entries = schemaEntries.get(service.getId()); + } + // We have to look for entry matching schemaName. + SchemaEntry correspondingEntry = null; + for (SchemaEntry entry : entries) { + if (entry.getName().equals(schemaName)) { + correspondingEntry = entry; + break; + } + } + // Return the entry content if found. + if (correspondingEntry != null) { + return correspondingEntry.getContent(); + } + return null; + } + + /** + * Clear the registry for specified service. + * @param service The Service whose schema entries should be removed from registry. + */ + public void clearRegistryForService(Service service) { + schemaEntries.remove(service.getId()); + } + + /** + * Clear the registry for specified service. + * @param serviceId The identifier of Service whose schema entries should be removed from registry. + */ + public void clearRegistryForService(String serviceId) { + schemaEntries.remove(serviceId); + } + + /** + * SchemaEntry represents an image of remote resources and allow access to content. For now, content remains + * in-memory but a future direction for handling load could be to manage overflow to local files. + */ + public class SchemaEntry { + private String name; + private String serviceId; + private String path; + private ResourceType type; + private String content; + private Set operations; + + public SchemaEntry(Resource resource) { + this.name = resource.getName(); + this.serviceId = resource.getServiceId(); + this.path = resource.getPath(); + this.type = resource.getType(); + this.content = resource.getContent(); + this.operations = resource.getOperations(); + } + + public String getName() { + return name; + } + + public String getServiceId() { + return serviceId; + } + + public String getPath() { + return path; + } + + public ResourceType getType() { + return type; + } + + public String getContent() { + return content; + } + + public Set getOperations() { + return operations; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/TestResource.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/TestResource.java new file mode 100644 index 000000000..47d02bb2c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/TestResource.java @@ -0,0 +1,54 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.domain.TestRunnerType; + +import org.jboss.logging.Logger; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * Endpoint for tests launching API. + * @author laurent + */ +@Path("/api/tests") +public class TestResource { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + @Inject + AsyncAPITestManager asyncAPITestManager; + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response launchTestCandidate(AsyncTestSpecification specification) { + logger.debugf("Test AsyncAPI Spec: " + specification.getAsyncAPISpec()); + if (specification.getRunnerType() == TestRunnerType.ASYNC_API_SCHEMA) { + logger.info("Accepting an ASYNC_API_SCHEMA test on endpoint " + specification.getEndpointUrl()); + asyncAPITestManager.launchTest(specification); + return Response.accepted().build(); + } + logger.errorf("Found no suitable test runner for {%S}, returning 204", specification.getRunnerType()); + return Response.noContent().build(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/ConnectorException.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/ConnectorException.java new file mode 100644 index 000000000..3f17731b8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/ConnectorException.java @@ -0,0 +1,33 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.client; + +/** + * Exception related to Microcks or Keycloak backend connnection. + * @author laurent + */ +public class ConnectorException extends Exception { + + private static final long serialVersionUID = 527259357275701107L; + + /** + * Cerate a ConnectorException with text message. + * @param message This exception text message + */ + public ConnectorException(String message) { + super(message); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/KeycloakConfig.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/KeycloakConfig.java new file mode 100644 index 000000000..2bdef0133 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/KeycloakConfig.java @@ -0,0 +1,89 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.client; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Simple bean representing the Keycloak configuration returned by the Microcks API config endpoint. + * @author laurent + */ +public class KeycloakConfig { + + private boolean enabled = true; + + private String realm; + + @JsonProperty("auth-server-url") + private String authServerUrl; + + @JsonProperty("ssl-required") + private String sslRequired; + + @JsonProperty("public-client") + private boolean publicClient; + + private String resource; + + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getRealm() { + return realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } + + public String getAuthServerUrl() { + return authServerUrl; + } + + public void setAuthServerUrl(String authServerUrl) { + this.authServerUrl = authServerUrl; + } + + public String getSslRequired() { + return sslRequired; + } + + public void setSslRequired(String sslRequired) { + this.sslRequired = sslRequired; + } + + public boolean isPublicClient() { + return publicClient; + } + + public void setPublicClient(boolean publicClient) { + this.publicClient = publicClient; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/KeycloakConnector.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/KeycloakConnector.java new file mode 100644 index 000000000..01775a00c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/KeycloakConnector.java @@ -0,0 +1,129 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.client; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.ssl.TrustStrategy; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Base64; + +/** + * Connector to the Keycloak server configured for Microcks API server. This connector allows to retrieved valid OAuth / + * JWT token for configured service account. + * @author laurent + */ +@ApplicationScoped +public class KeycloakConnector { + + private final Logger logger = Logger.getLogger(getClass()); + + @ConfigProperty(name = "microcks.serviceaccount") + String serviceAccount; + + @ConfigProperty(name = "microcks.serviceaccount.credentials") + String saCredentials; + + /** + * Connect to the given OIDC endpoint on a Keycloak server, realize a client_credentials grant type with configured + * account and credentials to finally return the OAuth accesc token. + * @param tokenEndpoint The OIDC endpoint on which to authenticate + * @return The OAuth access token after successful authentication + * @throws ConnectorException If connection faild (bad endpoint) or authentication failed (bad account or + * credentials) + * @throws IOException In case of communication exception + */ + public String connectAndGetOAuthToken(String tokenEndpoint) throws ConnectorException, IOException { + CloseableHttpClient httpClient = null; + + try { + // Start creating a SSL Context that accepts all because we may have self-signed certs. + TrustStrategy acceptingTrustStrategy = (cert, authType) -> true; + SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); + + // Configuring a httpClient that disables host name validation for certificates. + final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, + new String[] { "TLSv1.2", "TLSv1.3" }, null, NoopHostnameVerifier.INSTANCE); + + // BasicHttpClientConnectionManager was facing issues detecting close connections and re-creating new ones. + // Switching to PoolingHttpClientConnectionManager for HC 5.2 solves this issue. + final PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslsf).build(); + + httpClient = HttpClients.custom().setConnectionManager(connectionManager).build(); + } catch (GeneralSecurityException gse) { + logger.error("Caught a SecurityException when building the SSL Context", gse); + throw new ConnectorException("SSLContext cannot be created to reach Keycloak endpoint: " + gse.getMessage()); + } + + try { + // Prepare a post request with grant_type client credentials flow. + HttpPost tokenRequest = new HttpPost(tokenEndpoint); + tokenRequest.addHeader("Content-Type", "application/x-www-form-urlencoded"); + tokenRequest.addHeader("Accept", "application/json"); + tokenRequest.addHeader("Authorization", "Basic " + Base64.getEncoder() + .encodeToString((serviceAccount + ":" + saCredentials).getBytes(StandardCharsets.UTF_8))); + tokenRequest.setEntity(new StringEntity("grant_type=client_credentials")); + + // Execute request and retrieve content as string. + CloseableHttpResponse tokenResponse = httpClient.execute(tokenRequest); + + if (tokenResponse.getCode() != 200) { + logger.error( + "OAuth token cannot be retrieved for Keycloak server, check microcks.serviceaccount configuration"); + logger.errorf(" tokenResponse.statusCode: %d", tokenResponse.getCode()); + throw new ConnectorException( + "OAuth token cannot be retrieved for Microcks. Check microcks.serviceaccount."); + } + + String result = EntityUtils.toString(tokenResponse.getEntity()); + logger.debugf("Result: %s", result); + + // This should be a JSON token so parse it with Jackson. + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonToken = mapper.readTree(result); + + // Retrieve and return access_token. + String accessToken = jsonToken.path("access_token").asText(); + logger.debugf("Got an access token: %s", accessToken); + return accessToken; + } catch (ParseException e) { + logger.errorf("Caught a ParseException when parsing token response: %s", e.getMessage()); + throw new IOException(e); + } finally { + httpClient.close(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/MicrocksAPIConnector.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/MicrocksAPIConnector.java new file mode 100644 index 000000000..da20fbef3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/MicrocksAPIConnector.java @@ -0,0 +1,101 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.client; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceView; +import io.github.microcks.domain.TestCaseResult; + +import io.github.microcks.minion.async.client.dto.TestCaseReturnDTO; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; + +import java.util.List; + +@Path("/api") +@RegisterRestClient +@RegisterClientHeaders +/** + * REST Client interface for calling various Microcks APIs. + * @author laurent + */ +public interface MicrocksAPIConnector { + + /** + * Retrieve the Keycloak server configuration for the Microcks instance. + * @return The Keycloak config object + */ + @GET + @Path("/keycloak/config") + @Produces("application/json") + KeycloakConfig getKeycloakConfig(); + + /** + * Retrieve a list of services from Microcks APIs. + * @param authorization The Authorization header containing the OAuth access token for this API call + * @param page The page of service list to request + * @param size The size of the page to fetch + * @return A list of Service + */ + @GET + @Path("/services") + @Produces("application/json") + List listServices(@HeaderParam("Authorization") String authorization, @QueryParam("page") int page, + @QueryParam("size") int size); + + /** + * Retrieve the complete ServiceView for a Service that may contain messages definitions. + * @param authorization The Authorization header containing the OAuth access token for this API call + * @param serviceId The unique identifier of Service to get the view for + * @param messages Whether to include full descriptions of operations messages + * @return The complete ServiceView for Service + */ + @GET + @Path("/services/{id}") + @Produces("application/json") + ServiceView getService(@HeaderParam("Authorization") String authorization, @PathParam("id") String serviceId, + @QueryParam("messages") boolean messages); + + /** + * Retrieve the list of contract resources for a Service. + * @param serviceId The unique identifier of Service to get resources for + * @return A list of Resources associated to Service + */ + @GET + @Path("/resources/service/{id}") + @Produces("application/json") + List getResources(@PathParam("id") String serviceId); + + /** + * Report a TestCaseResult associated to a TestResult. + * @param testResultId The unique identifier of TestResult we want to report a result for + * @param testCaseReturn A Test Case return data object for this TestResult + * @return The created TestCaseResult following reporting + */ + @POST + @Path("/tests/{id}/testCaseResult") + @Produces("application/json") + TestCaseResult reportTestCaseResult(@PathParam("id") String testResultId, TestCaseReturnDTO testCaseReturn); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/ServiceViewChangeEventDeserializer.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/ServiceViewChangeEventDeserializer.java new file mode 100644 index 000000000..1b740a566 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/ServiceViewChangeEventDeserializer.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.client; + +import io.github.microcks.event.ServiceViewChangeEvent; + +import io.quarkus.kafka.client.serialization.ObjectMapperDeserializer; + +/** + * A Kafka deserializer for ServiceViewChangeEvent using Jackson ObjectMapper. + * @author laurent + */ +public class ServiceViewChangeEventDeserializer extends ObjectMapperDeserializer { + + public ServiceViewChangeEventDeserializer() { + // Pass the class to the parent. + super(ServiceViewChangeEvent.class); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/dto/TestCaseReturnDTO.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/dto/TestCaseReturnDTO.java new file mode 100644 index 000000000..3ca968728 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/client/dto/TestCaseReturnDTO.java @@ -0,0 +1,68 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.client.dto; + +import io.github.microcks.domain.TestReturn; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data Transfer object for grouping base information to report a test case run (and thus create a TestCaseResult). + * @author laurent + */ +@RegisterForReflection +public class TestCaseReturnDTO { + + private String operationName; + private List testReturns; + + public TestCaseReturnDTO() { + } + + /** + * Create a new TestCaseReturnDTo corresponding to an operation. + * @param operationName The name of the operation this Test case return relates to + */ + public TestCaseReturnDTO(String operationName) { + this.operationName = operationName; + } + + public String getOperationName() { + return operationName; + } + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public List getTestReturns() { + return testReturns; + } + + public void setTestReturns(List testReturns) { + this.testReturns = testReturns; + } + + public void addTestReturn(TestReturn testReturn) { + if (testReturns == null) { + testReturns = new ArrayList<>(); + } + testReturns.add(testReturn); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTask.java new file mode 100644 index 000000000..f79a92243 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTask.java @@ -0,0 +1,292 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncTestSpecification; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import org.jboss.logging.Logger; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An implementation of MessageConsumptionTask that consumes a queue on an RabbitMQ 3.x Server. Endpoint + * URL should be specified using the following form: + * amqp://{brokerhost[:port]}[/{virtualHost}]/{type}/{destination}[?option1=value1&option2=value2] + * @author laurent + */ +public class AMQPMessageConsumptionTask implements MessageConsumptionTask { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + /** The string for Regular Expression that helps validating acceptable endpoints. */ + public static final String ENDPOINT_PATTERN_STRING = "amqp://(?[^:]+(:\\d+)?)/(?[a-zA-Z0-9-_]+/)?(?[q|d|f|t|h])/(?[^?]+)(\\?(?.+))?"; + /** The Pattern for matching groups within the endpoint regular expression. */ + public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING); + + /** Constant representing a queue destination type in endpoint URL. */ + public static final String QUEUE_TYPE = "q"; + /** Constant representing a direct exchange destination type in endpoint URL. */ + public static final String DIRECT_TYPE = "d"; + /** Constant representing a fanout exchange destination type in endpoint URL. */ + public static final String FANOUT_TYPE = "f"; + /** Constant representing a topic exchange destination type in endpoint URL. */ + public static final String TOPIC_TYPE = "t"; + /** Constant representing a headers exchange destination type in endpoint URL. */ + public static final String HEADERS_TYPE = "h"; + + /** The endpoint URL option representing routing key. */ + public static final String ROUTING_KEY_OPTION = "routingKey"; + /** The endpoint URL option representing durable property. */ + public static final String DURABLE_OPTION = "durable"; + + private File trustStore; + + private final AsyncTestSpecification specification; + + protected Map optionsMap; + + private Connection connection; + + private String virtualHost; + + private String destinationType; + + private String destinationName; + + private String options; + + /** + * Create a new consumption task from an Async test specification. + * @param testSpecification The specification holding endpointURL and timeout. + */ + public AMQPMessageConsumptionTask(AsyncTestSpecification testSpecification) { + this.specification = testSpecification; + } + + /** + * Convenient static method for checking if this implementation will accept endpoint. + * @param endpointUrl The endpoint URL to validate + * @return True if endpointUrl can be used for connecting and consuming on endpoint + */ + public static boolean acceptEndpoint(String endpointUrl) { + return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING); + } + + @Override + public List call() throws Exception { + if (connection == null) { + initializeAMQPConnection(); + } + List messages = new ArrayList<>(); + + try (Channel channel = connection.createChannel()) { + String queueName = destinationName; + + if (!destinationType.equals(QUEUE_TYPE)) { + boolean durable = false; + if (optionsMap != null && optionsMap.containsKey(DURABLE_OPTION)) { + durable = Boolean.parseBoolean(optionsMap.get(DURABLE_OPTION)); + } + String routingKey = "#"; + if (optionsMap != null && optionsMap.containsKey(ROUTING_KEY_OPTION)) { + routingKey = optionsMap.get(ROUTING_KEY_OPTION); + } + + switch (destinationType) { + case DIRECT_TYPE: + channel.exchangeDeclare(destinationName, "direct", durable); + queueName = channel.queueDeclare().getQueue(); + channel.queueBind(queueName, destinationName, routingKey); + break; + case HEADERS_TYPE: + channel.exchangeDeclare(destinationName, "headers", durable); + queueName = channel.queueDeclare().getQueue(); + // Specify any header if specified otherwise default to routing key. + Map bindingArgs = buildHeaderArgs(); + if (bindingArgs != null && !bindingArgs.isEmpty()) { + bindingArgs.put("x-match", "any"); + channel.queueBind(queueName, destinationName, "", bindingArgs); + } else { + channel.queueBind(queueName, destinationName, routingKey); + } + break; + case FANOUT_TYPE: + channel.exchangeDeclare(destinationName, "fanout", durable); + queueName = channel.queueDeclare().getQueue(); + channel.queueBind(queueName, destinationName, ""); + break; + case TOPIC_TYPE: + default: + channel.exchangeDeclare(destinationName, "topic", durable); + queueName = channel.queueDeclare().getQueue(); + channel.queueBind(queueName, destinationName, routingKey); + break; + } + } + + String consumerTag = channel.basicConsume(queueName, false, new DefaultConsumer(channel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, + byte[] body) throws IOException { + logger.infof("Received a new AMQP Message: %s", new String(body)); + // Build a ConsumedMessage from AMQP message. + ConsumedMessage message = new ConsumedMessage(); + message.setReceivedAt(System.currentTimeMillis()); + message.setHeaders(buildHeaders(properties.getHeaders())); + message.setPayload(body); + messages.add(message); + + channel.basicAck(envelope.getDeliveryTag(), false); + } + }); + + Thread.sleep(specification.getTimeoutMS()); + + channel.basicCancel(consumerTag); + } + + return messages; + } + + /** + * Close the resources used by this task. Namely the AMQP connection and the optionally created truststore holding + * server client SSL credentials. + * @throws IOException should not happen. + */ + @Override + public void close() throws IOException { + if (connection != null) { + try { + connection.close(); + } catch (IOException ioe) { + logger.warn("Closing AMQP connection raised an exception", ioe); + } + } + if (trustStore != null && trustStore.exists()) { + Files.delete(trustStore.toPath()); + } + } + + /** Initialize AMQP connection from test properties. */ + private void initializeAMQPConnection() throws Exception { + Matcher matcher = ENDPOINT_PATTERN.matcher(specification.getEndpointUrl().trim()); + // Call matcher.find() to be able to use named expressions. + matcher.find(); + String endpointBrokerUrl = matcher.group("brokerUrl"); + virtualHost = matcher.group("virtualHost"); + destinationType = matcher.group("type"); + destinationName = matcher.group("destination"); + options = matcher.group("options"); + + // Parse options if specified. + if (options != null && !options.isBlank()) { + optionsMap = ConsumptionTaskCommons.initializeOptionsMap(options); + } + + ConnectionFactory factory = new ConnectionFactory(); + if (endpointBrokerUrl.contains(":")) { + String[] serverAndPort = endpointBrokerUrl.split(":"); + factory.setHost(serverAndPort[0]); + factory.setPort(Integer.parseInt(serverAndPort[1])); + } else { + factory.setHost(endpointBrokerUrl); + } + if (virtualHost != null && !virtualHost.isEmpty()) { + factory.setVirtualHost(virtualHost); + } + + if (specification.getSecret() != null) { + if (specification.getSecret().getUsername() != null && specification.getSecret().getPassword() != null) { + logger.debug("Adding username/password authentication from secret " + specification.getSecret().getName()); + factory.setUsername(specification.getSecret().getUsername()); + factory.setPassword(specification.getSecret().getPassword()); + } + + if (specification.getSecret().getCaCertPem() != null) { + logger.debug("Installing a broker certificate from secret " + specification.getSecret().getName()); + trustStore = ConsumptionTaskCommons.installBrokerCertificate(specification); + + // Load the truststore into a ssl context as explained here: + // https://www.rabbitmq.com/ssl.html#java-client + KeyStore tks = KeyStore.getInstance("JKS"); + tks.load(new FileInputStream(trustStore), ConsumptionTaskCommons.TRUSTSTORE_PASSWORD.toCharArray()); + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(tks); + + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null, tmf.getTrustManagers(), null); + + factory.useSslProtocol(sslContext); + factory.enableHostnameVerification(); + } + } + connection = factory.newConnection(); + } + + /** Build the map of headers as binding arguments for headers type exchange. */ + private Map buildHeaderArgs() { + if (optionsMap != null) { + Map results = new HashMap<>(); + // ?h.header1=value1&h.header2=value2 + for (String option : optionsMap.keySet()) { + if (option.startsWith(HEADERS_TYPE + ".")) { + String headerName = option.substring(HEADERS_TYPE.length() + 1); + String headerValue = optionsMap.get(option); + results.put(headerName, headerValue); + } + } + return results; + } + return null; + } + + /** Build set of Microcks headers from RabbitMQ headers. */ + private Set
buildHeaders(Map headers) { + if (headers == null || headers.isEmpty()) { + return null; + } + Set
results = new HashSet<>(); + for (String key : headers.keySet()) { + Header result = new Header(); + result.setName(key); + result.setValues(Set.of(headers.get(key).toString())); + results.add(result); + } + return results; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AmazonSNSMessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AmazonSNSMessageConsumptionTask.java new file mode 100644 index 000000000..bef71d666 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AmazonSNSMessageConsumptionTask.java @@ -0,0 +1,290 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncTestSpecification; + +import org.jboss.logging.Logger; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sns.SnsClientBuilder; +import software.amazon.awssdk.services.sns.model.ListTopicsRequest; +import software.amazon.awssdk.services.sns.model.ListTopicsResponse; +import software.amazon.awssdk.services.sns.model.SubscribeRequest; +import software.amazon.awssdk.services.sns.model.Topic; +import software.amazon.awssdk.services.sns.model.UnsubscribeRequest; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.SqsClientBuilder; +import software.amazon.awssdk.services.sqs.model.CreateQueueRequest; +import software.amazon.awssdk.services.sqs.model.DeleteQueueRequest; +import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest; +import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse; +import software.amazon.awssdk.services.sqs.model.Message; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; +import software.amazon.awssdk.services.sqs.model.QueueAttributeName; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; +import software.amazon.awssdk.services.sqs.model.SetQueueAttributesRequest; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An implementation of MessageConsumptionTask that consumes a queue on Amazon Simple Notification Service + * (SNS). Endpoint URL should be specified using the following form: + * sns://{region}/{topic}[?option1=value1&option2=value2] + * @author laurent + */ +public class AmazonSNSMessageConsumptionTask implements MessageConsumptionTask { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + /** The string for Regular Expression that helps validating acceptable endpoints. */ + public static final String ENDPOINT_PATTERN_STRING = "sns://(?[a-zA-Z0-9-]+)/(?[a-zA-Z0-9-_]+)(\\?(?.+))?"; + /** The Pattern for matching groups within the endpoint regular expression. */ + public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING); + + /** The endpoint URL option representing AWS endpoint override URL. */ + public static final String OVERRIDE_URL_OPTION = "overrideUrl"; + + private static final String SUBSCRIPTION_PREFIX = "-microcks-test"; + + private AsyncTestSpecification specification; + + protected String topic; + + protected Map optionsMap; + + private AwsCredentialsProvider credentialsProvider; + + private SnsClient snsClient; + + private SqsClient sqsClient; + + private QueueCoordinates queue; + + private String subscriptionArn; + + /** + * Creates a new consumption task from an Async test specification. + * @param testSpecification The specification holding endpointURL and timeout. + */ + public AmazonSNSMessageConsumptionTask(AsyncTestSpecification testSpecification) { + this.specification = testSpecification; + } + + /** + * Convenient static method for checking if this implementation will accept endpoint. + * @param endpointUrl The endpoint URL to validate + * @return True if endpointUrl can be used for connecting and consuming on endpoint + */ + public static boolean acceptEndpoint(String endpointUrl) { + return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING); + } + + @Override + public List call() throws Exception { + // As initialization can be long (circa 1 sec), we have to remove this time from wait time. + long startTime = System.currentTimeMillis(); + if (snsClient == null) { + initializeSubscription(); + } + List messages = new ArrayList<>(); + + long timeoutTime = startTime + specification.getTimeoutMS(); + while (System.currentTimeMillis() - startTime < specification.getTimeoutMS()) { + // Start polling/receiving messages with a max wait time and a max number. + ReceiveMessageRequest messageRequest = ReceiveMessageRequest.builder().queueUrl(queue.url()) + .maxNumberOfMessages(10).waitTimeSeconds((int) (timeoutTime - System.currentTimeMillis()) / 1000) + .build(); + + List receivedMessages = sqsClient.receiveMessage(messageRequest).messages(); + + for (Message receivedMessage : receivedMessages) { + // Build a ConsumedMessage from SQS message. + ConsumedMessage message = new ConsumedMessage(); + message.setReceivedAt(System.currentTimeMillis()); + message.setHeaders(buildHeaders(receivedMessage.messageAttributes())); + message.setPayload(receivedMessage.body().getBytes(StandardCharsets.UTF_8)); + messages.add(message); + } + } + return messages; + } + + @Override + public void close() throws IOException { + // Remove subscription on SNS side, then delete SQS queue. + snsClient.unsubscribe(UnsubscribeRequest.builder().subscriptionArn(subscriptionArn).build()); + sqsClient.deleteQueue(DeleteQueueRequest.builder().queueUrl(queue.url()).build()); + // Close both clients. + if (snsClient != null) { + snsClient.close(); + } + if (sqsClient != null) { + sqsClient.close(); + } + } + + /** Initialize Amazon SNS/SQS clients and SQS subscription from test properties. */ + private void initializeSubscription() throws Exception { + Matcher matcher = ENDPOINT_PATTERN.matcher(specification.getEndpointUrl().trim()); + // Call matcher.find() to be able to use named expressions. + matcher.find(); + + String region = matcher.group("region"); + topic = matcher.group("topic"); + String options = matcher.group("options"); + + // Parse options if specified. + if (options != null && !options.isBlank()) { + optionsMap = ConsumptionTaskCommons.initializeOptionsMap(options); + } + + // Build credential provider from secret username and password if any. + if (specification.getSecret() != null && specification.getSecret().getUsername() != null + && specification.getSecret().getPassword() != null) { + String accessKeyId = specification.getSecret().getUsername(); + String secretKeyId = specification.getSecret().getPassword(); + credentialsProvider = StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKeyId, secretKeyId)); + } else { + credentialsProvider = DefaultCredentialsProvider.create(); + } + + // Build the SNS client with provided region and credentials. + SnsClientBuilder snsClientBuilder = SnsClient.builder().region(Region.of(region)) + .credentialsProvider(credentialsProvider); + + // Build the SQS client with provided region and credentials. + SqsClientBuilder sqsClientBuilder = SqsClient.builder().region(Region.of(region)) + .credentialsProvider(credentialsProvider); + + // Override endpoint urls if provided. + if (hasOption(OVERRIDE_URL_OPTION)) { + String endpointOverride = optionsMap.get(OVERRIDE_URL_OPTION); + if (endpointOverride.startsWith("http")) { + URI endpointOverrideURI = new URI(endpointOverride); + snsClientBuilder.endpointOverride(endpointOverrideURI); + sqsClientBuilder.endpointOverride(endpointOverrideURI); + } + } + + snsClient = snsClientBuilder.build(); + sqsClient = sqsClientBuilder.build(); + + // Ensure connection is possible and subscription of SQS endpoint exists. + String topicArn = retrieveTopicArn(); + + // Create a temporary subscription Queue and get its ARN. + String subscriptionQueueName = topic + SUBSCRIPTION_PREFIX + "-" + specification.getTestResultId(); + queue = createQueue(subscriptionQueueName, topicArn); + + SubscribeRequest subscribeRequest = SubscribeRequest.builder().protocol("sqs").endpoint(queue.arn()) + .topicArn(topicArn).attributes(Map.of("RawMessageDelivery", "true")).returnSubscriptionArn(true).build(); + + subscriptionArn = snsClient.subscribe(subscribeRequest).subscriptionArn(); + } + + /** + * Safe method for checking if an option has been set. + * @param optionKey Check if that option is available in options map. + * @return true if option is present, false if undefined. + */ + protected boolean hasOption(String optionKey) { + if (optionsMap != null) { + return optionsMap.containsKey(optionKey); + } + return false; + } + + /** + * Retrieve a topic ARN using its name (from internal {@code topic} property). + * @return The topic ARN or null if not found. + */ + private String retrieveTopicArn() { + ListTopicsRequest listRequest = ListTopicsRequest.builder().build(); + ListTopicsResponse listResponse = snsClient.listTopics(listRequest); + + if (listResponse.hasTopics() && listResponse.topics().size() > 0) { + for (Topic topicTopic : listResponse.topics()) { + logger.infof("Found AWS SNS topic: %s", topicTopic.toString()); + if (topicTopic.topicArn().endsWith(":" + topic)) { + return topicTopic.topicArn(); + } + } + } + return null; + } + + /** + * Create a SQS Queue and retrieve its ARN. + * @param queueName The name of queue to create + * @return The unique ARN of the newly created queue + */ + private QueueCoordinates createQueue(String queueName, String topicArn) { + // 3 steps operation: 1st create a queue. + CreateQueueRequest createQueueRequest = CreateQueueRequest.builder().queueName(queueName).build(); + String queueURL = sqsClient.createQueue(createQueueRequest).queueUrl(); + + // Now read its attributes, requesting the ARN only. + GetQueueAttributesResponse queueAttributes = sqsClient.getQueueAttributes(GetQueueAttributesRequest.builder() + .queueUrl(queueURL).attributeNames(QueueAttributeName.QUEUE_ARN).build()); + String queueArn = queueAttributes.attributes().get(QueueAttributeName.QUEUE_ARN); + + // Finally set the queue policy to allow SNS topic to fanout to queue. + sqsClient.setQueueAttributes(SetQueueAttributesRequest.builder().queueUrl(queueURL) + .attributes(Map.of(QueueAttributeName.POLICY, + "{\n" + " \"Statement\": [\n" + " {\n" + " \"Effect\": \"Allow\",\n" + + " \"Principal\": {\n" + " \"Service\": \"sns.amazonaws.com\"\n" + " },\n" + + " \"Action\": \"sqs:SendMessage\",\n" + " \"Resource\": \"" + queueArn + "\",\n" + + " \"Condition\": {\n" + " \"ArnEquals\": {\n" + " \"aws:SourceArn\": \"" + + topicArn + "\"\n" + " }\n" + " }\n" + " }\n" + " ]\n" + "}")) + .build()); + + return new QueueCoordinates(queueName, queueURL, queueArn); + } + + /** Build set of Microcks headers from SQS headers. */ + private Set
buildHeaders(Map messageAttributes) { + if (messageAttributes == null || messageAttributes.isEmpty()) { + return null; + } + Set
results = new HashSet<>(); + for (Map.Entry attributeEntry : messageAttributes.entrySet()) { + Header result = new Header(); + result.setName(attributeEntry.getKey()); + result.setValues(Set.of(attributeEntry.getValue().stringValue())); + results.add(result); + } + return results; + } + + protected record QueueCoordinates(String name, String url, String arn) { + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AmazonSQSMessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AmazonSQSMessageConsumptionTask.java new file mode 100644 index 000000000..310b7b578 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/AmazonSQSMessageConsumptionTask.java @@ -0,0 +1,216 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncTestSpecification; + +import org.jboss.logging.Logger; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.SqsClientBuilder; +import software.amazon.awssdk.services.sqs.model.ListQueuesRequest; +import software.amazon.awssdk.services.sqs.model.ListQueuesResponse; +import software.amazon.awssdk.services.sqs.model.Message; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An implementation of MessageConsumptionTask that consumes a queue on Amazon Simple Queue Service (SQS). + * Endpoint URL should be specified using the following form: + * sqs://{region}/{queue}[?option1=value1&option2=value2] + * @author laurent + */ +public class AmazonSQSMessageConsumptionTask implements MessageConsumptionTask { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + /** The string for Regular Expression that helps validating acceptable endpoints. */ + public static final String ENDPOINT_PATTERN_STRING = "sqs://(?[a-zA-Z0-9-]+)/(?[a-zA-Z0-9-_]+)(\\?(?.+))?"; + /** The Pattern for matching groups within the endpoint regular expression. */ + public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING); + + /** The endpoint URL option representing AWS endpoint override URL. */ + public static final String OVERRIDE_URL_OPTION = "overrideUrl"; + + private AsyncTestSpecification specification; + + protected String queue; + + protected Map optionsMap; + + private AwsCredentialsProvider credentialsProvider; + + private SqsClient client; + + /** + * Creates a new consumption task from an Async test specification. + * @param testSpecification The specification holding endpointURL and timeout. + */ + public AmazonSQSMessageConsumptionTask(AsyncTestSpecification testSpecification) { + this.specification = testSpecification; + } + + /** + * Convenient static method for checking if this implementation will accept endpoint. + * @param endpointUrl The endpoint URL to validate + * @return True if endpointUrl can be used for connecting and consuming on endpoint + */ + public static boolean acceptEndpoint(String endpointUrl) { + return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING); + } + + @Override + public List call() throws Exception { + // As initialization can be long (circa 500 ms), we have to remove this time from wait time. + long startTime = System.currentTimeMillis(); + if (client == null) { + initializeSQSClient(); + } + List messages = new ArrayList<>(); + + // Find the correct queue url. + String queueUrl = retrieveQueueURL(); + if (queueUrl == null) { + logger.errorf("Unable to find the SQS queue URL for queue named '%s'", queue); + throw new IOException("Unable to find the SQS queue URL for queue " + queue); + } + + long timeoutTime = startTime + specification.getTimeoutMS(); + while (System.currentTimeMillis() - startTime < specification.getTimeoutMS()) { + // Start polling/receiving messages with a max wait time and a max number. + ReceiveMessageRequest messageRequest = ReceiveMessageRequest.builder().queueUrl(queueUrl) + .maxNumberOfMessages(10).waitTimeSeconds((int) (timeoutTime - System.currentTimeMillis()) / 1000) + .build(); + + List receivedMessages = client.receiveMessage(messageRequest).messages(); + + for (Message receivedMessage : receivedMessages) { + // Build a ConsumedMessage from SQS message. + ConsumedMessage message = new ConsumedMessage(); + message.setReceivedAt(System.currentTimeMillis()); + message.setHeaders(buildHeaders(receivedMessage.messageAttributes())); + message.setPayload(receivedMessage.body().getBytes(StandardCharsets.UTF_8)); + messages.add(message); + } + } + client.close(); + return messages; + } + + @Override + public void close() throws IOException { + if (client != null) { + client.close(); + } + } + + /** Initialize Amazon SQS client from test properties. */ + private void initializeSQSClient() throws Exception { + Matcher matcher = ENDPOINT_PATTERN.matcher(specification.getEndpointUrl().trim()); + // Call matcher.find() to be able to use named expressions. + matcher.find(); + + String region = matcher.group("region"); + queue = matcher.group("queue"); + String options = matcher.group("options"); + + // Parse options if specified. + if (options != null && !options.isBlank()) { + optionsMap = ConsumptionTaskCommons.initializeOptionsMap(options); + } + + // Build credential provider from secret username and password if any. + if (specification.getSecret() != null && specification.getSecret().getUsername() != null + && specification.getSecret().getPassword() != null) { + String accessKeyId = specification.getSecret().getUsername(); + String secretKeyId = specification.getSecret().getPassword(); + credentialsProvider = StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKeyId, secretKeyId)); + } else { + credentialsProvider = DefaultCredentialsProvider.create(); + } + + // Build the SQS client with provided region and credentials. + SqsClientBuilder builder = SqsClient.builder().region(Region.of(region)).credentialsProvider(credentialsProvider); + + if (hasOption(OVERRIDE_URL_OPTION)) { + String endpointOverride = optionsMap.get(OVERRIDE_URL_OPTION); + if (endpointOverride.startsWith("http")) { + builder.endpointOverride(new URI(endpointOverride)); + } + } + + client = builder.build(); + } + + /** + * Safe method for checking if an option has been set. + * @param optionKey Check if that option is available in options map. + * @return true if option is present, false if undefined. + */ + protected boolean hasOption(String optionKey) { + if (optionsMap != null) { + return optionsMap.containsKey(optionKey); + } + return false; + } + + /** + * Retrieve a queue URL using its name (from internal {@code queue} property). + * @return The queue URL or null if not found. + */ + private String retrieveQueueURL() { + ListQueuesRequest listRequest = ListQueuesRequest.builder().queueNamePrefix(queue).maxResults(1).build(); + ListQueuesResponse listResponse = client.listQueues(listRequest); + + if (listResponse.hasQueueUrls()) { + logger.infof("Found AWS SQS queue: %s", listResponse.queueUrls().get(0)); + return listResponse.queueUrls().get(0); + } + return null; + } + + /** Build set of Microcks headers from SQS headers. */ + private Set
buildHeaders(Map messageAttributes) { + if (messageAttributes == null || messageAttributes.isEmpty()) { + return null; + } + Set
results = new HashSet<>(); + for (Map.Entry attributeEntry : messageAttributes.entrySet()) { + Header result = new Header(); + result.setName(attributeEntry.getKey()); + result.setValues(Set.of(attributeEntry.getValue().stringValue())); + results.add(result); + } + return results; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/ConsumedMessage.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/ConsumedMessage.java new file mode 100644 index 000000000..b2e29b143 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/ConsumedMessage.java @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.domain.Header; +import org.apache.avro.generic.GenericRecord; + +import java.util.Set; + +/** + * This is a simple wrapper allowing to track a consumed messages from endpoint. + * @author laurent + */ +public class ConsumedMessage { + + private long receivedAt; + private byte[] payload; + private GenericRecord payloadRecord; + private Set
headers; + + public long getReceivedAt() { + return receivedAt; + } + + public void setReceivedAt(long receivedAt) { + this.receivedAt = receivedAt; + } + + public byte[] getPayload() { + return payload; + } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + + public GenericRecord getPayloadRecord() { + return payloadRecord; + } + + public void setPayloadRecord(GenericRecord payloadRecord) { + this.payloadRecord = payloadRecord; + } + + public Set
getHeaders() { + return headers; + } + + public void setHeaders(Set
headers) { + this.headers = headers; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/ConsumptionTaskCommons.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/ConsumptionTaskCommons.java new file mode 100644 index 000000000..0176e25d0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/ConsumptionTaskCommons.java @@ -0,0 +1,95 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.minion.async.AsyncTestSpecification; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +/** + * This is a utility class holding commons routines for MessageConsumptionTask implementations. + * @author laurent + */ +public class ConsumptionTaskCommons { + + /** Constant representing the header line in a custom CA Cert in PEM format. */ + private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; + /** Constant representing the footer line in a custom CA Cert in PEM format. */ + private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; + /** The password that is used when generating a custom truststore. */ + public static final String TRUSTSTORE_PASSWORD = "password"; + + private ConsumptionTaskCommons() { + // Private constructor to hide the implicit public one. + } + + /** + * Install broker custom certificate into a truststore file. + * @param specification The specification holding secret information + * @return A newly created trustStore file created as temporary file. + * @throws Exception in case of IO exception while decoding secret or writing the truststore file. + */ + public static File installBrokerCertificate(AsyncTestSpecification specification) throws Exception { + String caCertPem = specification.getSecret().getCaCertPem(); + + // First compute a stripped PEM certificate and decode it from base64. + String strippedPem = caCertPem.replaceAll(BEGIN_CERTIFICATE, "").replaceAll(END_CERTIFICATE, ""); + InputStream is = new ByteArrayInputStream(org.apache.commons.codec.binary.Base64.decodeBase64(strippedPem)); + + // Generate a new x509 certificate from the stripped decoded pem. + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate caCert = (X509Certificate) cf.generateCertificate(is); + + // Create a new TrustStore using KeyStore API. + char[] password = TRUSTSTORE_PASSWORD.toCharArray(); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null, password); + ks.setCertificateEntry("root", caCert); + + File trustStore = File.createTempFile("microcks-truststore-" + System.currentTimeMillis(), ".jks"); + + try (FileOutputStream fos = new FileOutputStream(trustStore)) { + ks.store(fos, password); + } + + return trustStore; + } + + /** + * Initialize options map from options string found in Endpoint URL. + * @param options A string of options having the form: option1=value1&option2=value2 + * @return A Map of options supplied in endpoint url. + */ + public static Map initializeOptionsMap(String options) { + Map optionsMap = new HashMap<>(); + String[] keyValuePairs = options.split("&"); + for (String keyValuePair : keyValuePairs) { + String[] keyValue = keyValuePair.split("="); + if (keyValue.length > 1) { + optionsMap.put(keyValue[0], keyValue[1]); + } + } + return optionsMap; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/GooglePubSubMessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/GooglePubSubMessageConsumptionTask.java new file mode 100644 index 000000000..06e4e9836 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/GooglePubSubMessageConsumptionTask.java @@ -0,0 +1,182 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import com.google.protobuf.Duration; +import com.google.pubsub.v1.*; +import io.github.microcks.minion.async.AsyncTestSpecification; + +import com.google.api.gax.rpc.NotFoundException; +import com.google.api.gax.core.CredentialsProvider; +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.pubsub.v1.MessageReceiver; +import com.google.cloud.pubsub.v1.Subscriber; +import com.google.cloud.pubsub.v1.SubscriptionAdminClient; +import com.google.cloud.pubsub.v1.SubscriptionAdminSettings; +import org.jboss.logging.Logger; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An implementation of MessageConsumptionTask that consumes a queue on Google Cloud PubSub. Endpoint URL + * should be specified using the following form: + * googlepubsub://{projectId}/{topic}[?option1=value1&option2=value2] + * @author laurent + */ +public class GooglePubSubMessageConsumptionTask implements MessageConsumptionTask { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + /** The string for Regular Expression that helps validating acceptable endpoints. */ + public static final String ENDPOINT_PATTERN_STRING = "googlepubsub://(?[a-zA-Z0-9-_]+)/(?[a-zA-Z0-9-_\\.]+)(\\?(?.+))?"; + /** The Pattern for matching groups within the endpoint regular expression. */ + public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING); + + private static final String CLOUD_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; + + private static final String SUBSCRIPTION_PREFIX = "-microcks-test"; + + private AsyncTestSpecification specification; + + protected Map optionsMap; + + private CredentialsProvider credentialsProvider; + + private SubscriptionName subscriptionName; + + private Subscriber subscriber; + + /** + * Creates a new consumption task from an Async test specification. + * @param testSpecification The specification holding endpointURL and timeout. + */ + public GooglePubSubMessageConsumptionTask(AsyncTestSpecification testSpecification) { + this.specification = testSpecification; + } + + /** + * Convenient static method for checking if this implementation will accept endpoint. + * @param endpointUrl The endpoint URL to validate + * @return True if endpointUrl can be used for connecting and consuming on endpoint + */ + public static boolean acceptEndpoint(String endpointUrl) { + return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING); + } + + @Override + public List call() throws Exception { + // As initialization can be long (circa 1 sec), we have to remove this time from wait time. + long start = System.currentTimeMillis(); + if (subscriber == null) { + initializePubSubSubscriber(); + } + List messages = new ArrayList<>(); + + // Subscribe to PubSub. + MessageReceiver receiver = (message, consumer) -> { + logger.info("Received a new PubSub Message: " + message.getData().toStringUtf8()); + // Build a ConsumedMessage from PubSub message. + ConsumedMessage consumedMessage = new ConsumedMessage(); + consumedMessage.setReceivedAt(System.currentTimeMillis()); + consumedMessage.setPayload(message.getData().toByteArray()); + messages.add(consumedMessage); + + consumer.ack(); + }; + + // Create a new subscriber for subscription. + subscriber = Subscriber + .newBuilder(ProjectSubscriptionName.of(subscriptionName.getProject(), subscriptionName.getSubscription()), + receiver) + .setCredentialsProvider(credentialsProvider).build(); + subscriber.startAsync().awaitRunning(); + + // Wait and stop async receiver. + Thread.sleep(specification.getTimeoutMS() - (System.currentTimeMillis() - start)); + subscriber.stopAsync(); + + return messages; + } + + @Override + public void close() throws IOException { + if (subscriber != null && subscriber.isRunning()) { + subscriber.stopAsync(); + } + } + + /** Initialize Google Pub Sub consumer from test properties. */ + private void initializePubSubSubscriber() throws Exception { + Matcher matcher = ENDPOINT_PATTERN.matcher(specification.getEndpointUrl().trim()); + // Call matcher.find() to be able to use named expressions. + matcher.find(); + + String projectId = matcher.group("projectId"); + String topic = matcher.group("topic"); + String options = matcher.group("options"); + + // Parse options if specified. + if (options != null && !options.isBlank()) { + optionsMap = ConsumptionTaskCommons.initializeOptionsMap(options); + } + + // Build credential provider from secret token. + if (specification.getSecret() != null && specification.getSecret().getToken() != null) { + byte[] decode = Base64.getDecoder().decode(specification.getSecret().getToken()); + ByteArrayInputStream is = new ByteArrayInputStream(decode); + credentialsProvider = FixedCredentialsProvider + .create(GoogleCredentials.fromStream(is).createScoped(CLOUD_OAUTH_SCOPE)); + } else { + credentialsProvider = NoCredentialsProvider.create(); + } + + // Ensure connection is possible and subscription exists. + TopicName topicName = TopicName.of(projectId, topic); + subscriptionName = SubscriptionName.of(projectId, topic + SUBSCRIPTION_PREFIX); + + SubscriptionAdminSettings subscriptionAdminSettings = SubscriptionAdminSettings.newBuilder() + .setCredentialsProvider(credentialsProvider).build(); + SubscriptionAdminClient subscriptionAdminClient = SubscriptionAdminClient.create(subscriptionAdminSettings); + + try { + subscriptionAdminClient.getSubscription(subscriptionName); + } catch (NotFoundException nfe) { + logger.infof("Subscription {%s} does not exist yet, creating it", subscriptionName); + + // Customize subscription to avoid retention and let google auto-cleanup it. + // Put the durations to the minimum values accepted by Google cloud. + Subscription subscriptionRequest = Subscription.newBuilder().setName(subscriptionName.toString()) + .setTopic(topicName.toString()).setPushConfig(PushConfig.getDefaultInstance()).setAckDeadlineSeconds(10) + .setRetainAckedMessages(false).setMessageRetentionDuration(Duration.newBuilder().setSeconds(600).build()) + .setExpirationPolicy( + ExpirationPolicy.newBuilder().setTtl(Duration.newBuilder().setSeconds(24 * 3600L)).build()) + .setEnableMessageOrdering(false).setEnableExactlyOnceDelivery(false).build(); + subscriptionAdminClient.createSubscription(subscriptionRequest); + } finally { + subscriptionAdminClient.close(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTask.java new file mode 100644 index 000000000..78771ad67 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTask.java @@ -0,0 +1,335 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; +import io.confluent.kafka.serializers.KafkaAvroDeserializer; + +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncTestSpecification; + +import org.apache.avro.generic.GenericRecord; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.serialization.ByteArrayDeserializer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.jboss.logging.Logger; + +import java.io.*; +import java.nio.file.Files; +import java.time.Duration; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An implementation of MessageConsumptionTask that consumes a topic on an Apache Kafka Broker. Endpoint + * URL should be specified using the following form: + * kafka://{brokerhost[:port]}/{topic}[?option1=value1&option2=value2] + * @author laurent + */ +public class KafkaMessageConsumptionTask implements MessageConsumptionTask { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + /** The string for Regular Expression that helps validating acceptable endpoints. */ + public static final String ENDPOINT_PATTERN_STRING = "kafka://(?[^:]+(:\\d+)?)/(?[a-zA-Z0-9-_\\.]+)(\\?(?.+))?"; + /** The Pattern for matching groups within the endpoint regular expression. */ + public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING); + + /** The endpoint URL option representing schema registry URL. */ + public static final String REGISTRY_URL_OPTION = "registryUrl"; + /** The endpoint URL option representing schema registry username. */ + public static final String REGISTRY_USERNAME_OPTION = "registryUsername"; + /** The endpoint URL option representing schema registry auth credentials source. */ + public static final String REGISTRY_AUTH_CREDENTIALS_SOURCE = "registryAuthCredSource"; + + /** The endpoint URL option representing the offset we should start consume from. */ + public static final String START_OFFSET = "startOffset"; + /** The endpoint URL option representing the offset whe should end consumer to. */ + public static final String END_OFFSET = "endOffset"; + + private File trustStore; + + private AsyncTestSpecification specification; + + protected Map optionsMap; + + protected KafkaConsumer consumer; + + protected KafkaConsumer avroConsumer; + + protected Long startOffset; + protected Long endOffset; + + + /** + * Create a new consumption task from an Async test specification. + * @param testSpecification The specification holding endpointURL and timeout. + */ + public KafkaMessageConsumptionTask(AsyncTestSpecification testSpecification) { + this.specification = testSpecification; + } + + /** + * Convenient static method for checking if this implementation will accept endpoint. + * @param endpointUrl The endpoint URL to validate + * @return True if endpointUrl can be used for connecting and consuming on endpoint + */ + public static boolean acceptEndpoint(String endpointUrl) { + return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING); + } + + @Override + public List call() throws Exception { + if (consumer == null && avroConsumer == null) { + initializeKafkaConsumer(); + } + List messages = new ArrayList<>(); + + // Start polling with appropriate consumer for records. + // Do not forget to close the consumer before returning results. + if (consumer != null) { + consumeByteArray(messages); + consumer.close(); + } else { + consumeAvro(messages); + avroConsumer.close(); + } + return messages; + } + + /** + * Close the resources used by this task. Namely the Kafka consumer(s) and the optionally created truststore holding + * Kafka client SSL credentials. + * @throws IOException should not happen. + */ + @Override + public void close() throws IOException { + if (consumer != null) { + consumer.close(); + } + if (avroConsumer != null) { + avroConsumer.close(); + } + if (trustStore != null && trustStore.exists()) { + Files.delete(trustStore.toPath()); + } + } + + /** Initialize Kafka consumer from built properties and subscribe to target topic. */ + private void initializeKafkaConsumer() { + Matcher matcher = ENDPOINT_PATTERN.matcher(specification.getEndpointUrl().trim()); + // Call matcher.find() to be able to use named expressions. + matcher.find(); + String endpointBrokerUrl = matcher.group("brokerUrl"); + String endpointTopic = matcher.group("topic"); + String options = matcher.group("options"); + + // Parse options if specified. + if (options != null && !options.isBlank()) { + optionsMap = ConsumptionTaskCommons.initializeOptionsMap(options); + } + + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, endpointBrokerUrl); + + // Generate a unique GroupID for no collision with previous or other consumers. + props.put(ConsumerConfig.GROUP_ID_CONFIG, specification.getTestResultId() + "-" + System.currentTimeMillis()); + props.put(ConsumerConfig.CLIENT_ID_CONFIG, "microcks-async-minion-test"); + + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + + // Value deserializer depends on schema registry presence. + if (hasOption(REGISTRY_URL_OPTION)) { + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, KafkaAvroDeserializer.class.getName()); + props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, optionsMap.get(REGISTRY_URL_OPTION)); + // Configure schema registry credentials if any. + if (hasOption(REGISTRY_USERNAME_OPTION) || hasOption(REGISTRY_AUTH_CREDENTIALS_SOURCE)) { + props.put(AbstractKafkaSchemaSerDeConfig.USER_INFO_CONFIG, optionsMap.get(REGISTRY_USERNAME_OPTION)); + props.put(AbstractKafkaSchemaSerDeConfig.BASIC_AUTH_CREDENTIALS_SOURCE, + optionsMap.get(REGISTRY_AUTH_CREDENTIALS_SOURCE)); + } + } else { + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName()); + } + + // Only retrieve incoming messages and do not persist offset. + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); + + if (specification.getSecret() != null && specification.getSecret().getCaCertPem() != null) { + try { + // Because Kafka Java client does not support any other sources for SSL configuration, + // we need to create a Truststore holding the secret certificate and credentials. See below: + // https://cwiki.apache.org/confluence/display/KAFKA/KIP-486%3A+Support+custom+way+to+load+KeyStore+and+TrustStore + trustStore = ConsumptionTaskCommons.installBrokerCertificate(specification); + + // Then we have to add SSL specific properties. + props.put("security.protocol", "SSL"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, trustStore.getAbsolutePath()); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, ConsumptionTaskCommons.TRUSTSTORE_PASSWORD); + } catch (Exception e) { + logger.error("Exception while installing custom truststore: " + e.getMessage()); + } + } + + instanciateKafkaConsumer(props, endpointTopic); + } + + private void instanciateKafkaConsumer(Properties props, String endpointTopic) { + if (hasOption(START_OFFSET)) { + try { + startOffset = Long.parseLong(optionsMap.get(START_OFFSET)); + } catch (NumberFormatException nfe) { + logger.warnf("The startOffset endpoint option {%s} is not a Long", optionsMap.get(START_OFFSET)); + } + } + if (hasOption(END_OFFSET)) { + try { + endOffset = Long.parseLong(optionsMap.get(END_OFFSET)); + } catch (NumberFormatException nfe) { + logger.warnf("The endOffset endpoint option {%s} is not a Long", optionsMap.get(END_OFFSET)); + } + } + + // Create the consumer from properties and subscribe to given topic. + if (hasOption(REGISTRY_URL_OPTION)) { + avroConsumer = new KafkaConsumer<>(props); + avroConsumer.subscribe(Arrays.asList(endpointTopic), new StartOffsetSeeker()); + } else { + consumer = new KafkaConsumer<>(props); + consumer.subscribe(Arrays.asList(endpointTopic), new StartOffsetSeeker()); + } + } + + /** + * Safe method for checking if an option has been set. + * @param optionKey Check if that option is available in options map. + * @return true if option is present, false if undefined. + */ + protected boolean hasOption(String optionKey) { + if (optionsMap != null) { + return optionsMap.containsKey(optionKey); + } + return false; + } + + /** Consume simple byte[] on default consumer. Fill messages array. */ + private void consumeByteArray(List messages) { + long startTime = System.currentTimeMillis(); + long timeoutTime = startTime + specification.getTimeoutMS(); + + while (System.currentTimeMillis() - startTime < specification.getTimeoutMS()) { + ConsumerRecords records = consumer + .poll(Duration.ofMillis(timeoutTime - System.currentTimeMillis())); + + boolean oufOfOffsetRange = false; + for (ConsumerRecord consumerRecord : records) { + // Check current offset if a limit was set. + if (endOffset != null && consumerRecord.offset() > endOffset) { + oufOfOffsetRange = true; + break; + } + // Build a ConsumedMessage from Kafka record. + ConsumedMessage message = new ConsumedMessage(); + message.setReceivedAt(System.currentTimeMillis()); + message.setHeaders(buildHeaders(consumerRecord.headers())); + message.setPayload(consumerRecord.value()); + messages.add(message); + } + // Exit main waiting loop if we reach the end. + if (oufOfOffsetRange) { + break; + } + } + } + + /** Consumer avro records when connected to registry. Fill messages array. */ + private void consumeAvro(List messages) { + long startTime = System.currentTimeMillis(); + long timeoutTime = startTime + specification.getTimeoutMS(); + + while (System.currentTimeMillis() - startTime < specification.getTimeoutMS()) { + ConsumerRecords records = avroConsumer + .poll(Duration.ofMillis(timeoutTime - System.currentTimeMillis())); + + boolean oufOfOffsetRange = false; + for (ConsumerRecord consumerRecord : records) { + // Check current offset if a limit was set. + if (endOffset != null && consumerRecord.offset() > endOffset) { + oufOfOffsetRange = true; + break; + } + // Build a ConsumedMessage from Kafka record. + ConsumedMessage message = new ConsumedMessage(); + message.setReceivedAt(System.currentTimeMillis()); + message.setHeaders(buildHeaders(consumerRecord.headers())); + message.setPayloadRecord(consumerRecord.value()); + messages.add(message); + } + // Exit main waiting loop if we reach the end. + if (oufOfOffsetRange) { + break; + } + } + } + + /** Build set of Microcks headers from Kafka headers. */ + private Set
buildHeaders(Headers headers) { + Set
results = new HashSet<>(); + if (headers == null || !headers.iterator().hasNext()) { + return results; + } + Iterator headersIterator = headers.iterator(); + while (headersIterator.hasNext()) { + org.apache.kafka.common.header.Header header = headersIterator.next(); + Header result = new Header(); + result.setName(header.key()); + result.setValues(Set.of(new String(header.value()))); + results.add(result); + } + return results; + } + + private class StartOffsetSeeker implements ConsumerRebalanceListener { + @Override + public void onPartitionsRevoked(Collection partitions) { + // Nothing to do on revocation. + } + + @Override + public void onPartitionsAssigned(Collection partitions) { + if (startOffset != null) { + partitions.forEach(p -> { + if (consumer != null) { + consumer.seek(p, startOffset); + } + if (avroConsumer != null) { + avroConsumer.seek(p, startOffset); + } + }); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/MQTTMessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/MQTTMessageConsumptionTask.java new file mode 100644 index 000000000..0f5b18a3b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/MQTTMessageConsumptionTask.java @@ -0,0 +1,167 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.minion.async.AsyncTestSpecification; +import org.eclipse.paho.client.mqttv3.IMqttClient; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.jboss.logging.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An implementation of MessageConsumptionTask that consumes a topic on an MQTT 3.1 Server. Endpoint URL + * should be specified using the following form: + * mqtt://{brokerhost[:port]}/{topic}[?option1=value1&option2=value2] + * @author laurent + */ +public class MQTTMessageConsumptionTask implements MessageConsumptionTask { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + /** The string for Regular Expression that helps validating acceptable endpoints. */ + public static final String ENDPOINT_PATTERN_STRING = "mqtt://(?[^:]+(:\\d+)?)/(?.+)(\\?(?.+))?"; + /** The Pattern for matching groups within the endpoint regular expression. */ + public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING); + + private File trustStore; + + private final AsyncTestSpecification specification; + + private IMqttClient subscriber; + + private String endpointTopic; + + private String options; + + /** + * Create a new consumption task from an Async test specification. + * @param testSpecification The specification holding endpointURL and timeout. + */ + public MQTTMessageConsumptionTask(AsyncTestSpecification testSpecification) { + this.specification = testSpecification; + } + + /** + * Convenient static method for checking if this implementation will accept endpoint. + * @param endpointUrl The endpoint URL to validate + * @return True if endpointUrl can be used for connecting and consuming on endpoint + */ + public static boolean acceptEndpoint(String endpointUrl) { + return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING); + } + + @Override + public List call() throws Exception { + if (subscriber == null) { + intializeMQTTClient(); + } + List messages = new ArrayList<>(); + + // Start subscribing to the server endpoint topic. + subscriber.subscribe(endpointTopic, (topic, mqttMessage) -> { + logger.info("Received a new MQTT Message: " + new String(mqttMessage.getPayload())); + // Build a ConsumedMessage from MQTT message. + ConsumedMessage message = new ConsumedMessage(); + message.setReceivedAt(System.currentTimeMillis()); + message.setPayload(mqttMessage.getPayload()); + messages.add(message); + }); + + Thread.sleep(specification.getTimeoutMS()); + + // Disconnect the subscriber before returning results. + subscriber.disconnect(); + return messages; + } + + /** + * Close the resources used by this task. Namely the MQTT subscriber and the optionally created truststore holding + * server client SSL credentials. + * @throws IOException should not happen. + */ + @Override + public void close() throws IOException { + if (subscriber != null) { + try { + subscriber.close(); + } catch (MqttException e) { + logger.warn("Closing MQTT subscriber raised an exception", e); + } + } + if (trustStore != null && trustStore.exists()) { + Files.delete(trustStore.toPath()); + } + } + + /** */ + private void intializeMQTTClient() throws Exception { + Matcher matcher = ENDPOINT_PATTERN.matcher(specification.getEndpointUrl().trim()); + // Call matcher.find() to be able to use named expressions. + matcher.find(); + String endpointBrokerUrl = matcher.group("brokerUrl"); + endpointTopic = matcher.group("topic"); + options = matcher.group("options"); + + MqttConnectOptions connectOptions = new MqttConnectOptions(); + connectOptions.setAutomaticReconnect(false); + connectOptions.setCleanSession(true); + connectOptions.setConnectionTimeout(10); + + // Initialize default protocol pragma for connection string. + String protocolPragma = "tcp://"; + + if (specification.getSecret() != null) { + if (specification.getSecret().getUsername() != null && specification.getSecret().getPassword() != null) { + logger.debug("Adding username/password authentication from secret " + specification.getSecret().getName()); + connectOptions.setUserName(specification.getSecret().getUsername()); + connectOptions.setPassword(specification.getSecret().getPassword().toCharArray()); + } + + if (specification.getSecret().getCaCertPem() != null) { + logger.debug("Installing a broker certificate from secret " + specification.getSecret().getName()); + trustStore = ConsumptionTaskCommons.installBrokerCertificate(specification); + + // Find the list of SSL properties here: + // https://www.eclipse.org/paho/files/javadoc/org/eclipse/paho/client/mqttv3/MqttConnectOptions.html#setSSLProperties-java.util.Properties- + Properties sslProperties = new Properties(); + sslProperties.put("com.ibm.ssl.trustStore", trustStore.getAbsolutePath()); + sslProperties.put("com.ibm.ssl.trustStorePassword", ConsumptionTaskCommons.TRUSTSTORE_PASSWORD); + sslProperties.put("com.ibm.ssl.trustStoreType", "JKS"); + connectOptions.setSSLProperties(sslProperties); + + // We also have to change the prococolPragma to ssl:// + protocolPragma = "ssl://"; + } + } + + // Create the subscriber and connect it to server using the properties. + // Generate a unique clientId for each publication to avoid collisions (cleanSession is true). + subscriber = new MqttClient(protocolPragma + endpointBrokerUrl, + "microcks-async-minion-test-" + System.currentTimeMillis()); + subscriber.connect(connectOptions); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/MessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/MessageConsumptionTask.java new file mode 100644 index 000000000..d4ff61625 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/MessageConsumptionTask.java @@ -0,0 +1,28 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import java.io.Closeable; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Interface definition for Task consuming messages on a remote broker. + * @author laurent + */ +public interface MessageConsumptionTask extends Callable>, Closeable { + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/NATSMessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/NATSMessageConsumptionTask.java new file mode 100644 index 000000000..a0e00c390 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/NATSMessageConsumptionTask.java @@ -0,0 +1,163 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncTestSpecification; + +import io.nats.client.Connection; +import io.nats.client.Dispatcher; +import io.nats.client.Nats; +import io.nats.client.Options; +import io.nats.client.impl.Headers; +import org.jboss.logging.Logger; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An implementation of MessageConsumptionTask that consumes a topic on an NATS. Endpoint URL should be + * specified using the following form: nats://{brokerhost[:port]} + * @author laurent + */ +public class NATSMessageConsumptionTask implements MessageConsumptionTask { + + /** + * Get a JBoss logging logger. + */ + private final Logger logger = Logger.getLogger(getClass()); + + /** + * The string for Regular Expression that helps validating acceptable endpoints. + */ + public static final String ENDPOINT_PATTERN_STRING = "nats://(?[^:]+(:\\d+)?)/(?.+)"; + + /** + * The Pattern for matching groups within the endpoint regular expression. + */ + public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING); + + private AsyncTestSpecification specification; + + private Connection subscriber; + + private String endpointTopic; + + private String options; + + /** + * Create a new consumption task from an Async test specification. + * @param testSpecification The specification holding endpointURL and timeout. + */ + public NATSMessageConsumptionTask(AsyncTestSpecification testSpecification) { + this.specification = testSpecification; + } + + /** + * Convenient static method for checking if this implementation will accept endpoint. + * @param endpointUrl The endpoint URL to validate + * @return True if endpointUrl can be used for connecting and consuming on endpoint + */ + public static boolean acceptEndpoint(String endpointUrl) { + return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING); + } + + @Override + public List call() throws Exception { + if (subscriber == null) { + intializeNATSClient(); + } + List messages = new ArrayList<>(); + + // Start subscribing to the broker endpoint topic. + Dispatcher d = subscriber.createDispatcher((msg) -> { + String msgString = new String(msg.getData(), StandardCharsets.UTF_8); + logger.info("Received a new NATS Message: " + msgString); + // Build a ConsumedMessage from NATS message. + ConsumedMessage message = new ConsumedMessage(); + message.setReceivedAt(System.currentTimeMillis()); + message.setHeaders(buildHeaders(msg.getHeaders())); + message.setPayload(msg.getData()); + messages.add(message); + }); + + d.subscribe(endpointTopic); + + Thread.sleep(specification.getTimeoutMS()); + + // Disconnect the subscriber before returning results. + subscriber.close(); + return messages; + } + + /** Build set of Microcks headers from NATS headers. */ + private Set
buildHeaders(Headers headers) { + if (headers == null || headers.isEmpty()) { + return null; + } + Set
results = new HashSet<>(); + for (Map.Entry> entry : headers.entrySet()) { + Header result = new Header(); + result.setName(entry.getKey()); + result.setValues(new HashSet<>(entry.getValue())); + results.add(result); + } + return results; + } + + /** + * Close the resources used by this task. Namely the NATS subscriber and the option truststore. + * @throws IOException should not happen. + */ + @Override + public void close() throws IOException { + if (subscriber != null) { + try { + subscriber.close(); + } catch (InterruptedException e) { + logger.warn("Closing NATS subscriber raised an exception", e); + } + } + } + + private void intializeNATSClient() throws Exception { + Matcher matcher = ENDPOINT_PATTERN.matcher(specification.getEndpointUrl().trim()); + // Call matcher.find() to be able to use named expressions. + matcher.find(); + String endpointBrokerUrl = matcher.group("brokerUrl"); + endpointTopic = matcher.group("topic"); + + Options.Builder optionsBuilder = new Options.Builder().server(endpointBrokerUrl).maxReconnects(10); + + if (specification.getSecret() != null) { + if (specification.getSecret().getUsername() != null && specification.getSecret().getPassword() != null) { + logger.debug("Adding username/password authentication from secret " + specification.getSecret().getName()); + optionsBuilder.userInfo(specification.getSecret().getUsername(), specification.getSecret().getPassword()); + } else if (specification.getSecret().getToken() != null) { + logger.debug("Adding token authentication from secret " + specification.getSecret().getName()); + optionsBuilder.token(specification.getSecret().getToken().toCharArray()); + } + } + subscriber = Nats.connect(optionsBuilder.build()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/WebSocketClient.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/WebSocketClient.java new file mode 100644 index 000000000..692201cff --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/WebSocketClient.java @@ -0,0 +1,84 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@ClientEndpoint +/** + * A simple WebSocket client that stores messages as ConsumedMessages. It supports both the + * {@class jakarta.websocket.ClientEndpoint} annotation and the generic {@class jakarta.websocket.Endpoint} interface, + * registering itself as a new message handler. + * @author laurent + */ +public class WebSocketClient extends Endpoint { + + private List messages = new ArrayList<>(); + + /** + * Create a new WebSocket Client endpoint. + */ + public WebSocketClient() { + } + + @Override + public void onOpen(Session session, EndpointConfig endpointConfig) { + session.addMessageHandler(new MessageHandler.Whole() { + public void onMessage(String messagePayload) { + storeMessage(messagePayload); + } + }); + } + + @OnOpen + public void onOpen(Session session) { + // Nothing to do on session opening. + } + + @OnMessage + public void onMessage(String messagePayload) { + storeMessage(messagePayload); + } + + /** + * Get the list of consumed messages. + * @return The consumed message during client connection time. + */ + public List getMessages() { + return messages; + } + + /** + * Store after having transformed into a ConsumedMessage. + * @param messagePayload The payload to store + */ + protected void storeMessage(String messagePayload) { + // Build a ConsumedMessage from Kafka record. + ConsumedMessage message = new ConsumedMessage(); + message.setReceivedAt(System.currentTimeMillis()); + message.setPayload(messagePayload.getBytes(StandardCharsets.UTF_8)); + messages.add(message); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/WebSocketMessageConsumptionTask.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/WebSocketMessageConsumptionTask.java new file mode 100644 index 000000000..64a2e453a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/consumer/WebSocketMessageConsumptionTask.java @@ -0,0 +1,131 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.minion.async.AsyncTestSpecification; +import org.apache.http.ssl.SSLContexts; +import org.jboss.logging.Logger; + +import javax.net.ssl.SSLContext; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.regex.Pattern; + +/** + * An implementation of MessageConsumptionTask that consumes a WebSocket endpoint. Endpoint URL should be + * specified using the following form: ws://{wsHost[:port]}/{channel}[?option1=value1&option2=value2]. + * Channel may be empty if connecting to the root context of the WebSocket server. + * @author laurent + */ +public class WebSocketMessageConsumptionTask implements MessageConsumptionTask { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + /** The string for Regular Expression that helps validating acceptable endpoints. */ + public static final String ENDPOINT_PATTERN_STRING = "ws://(?[^:]+(:\\d+)?)/(?.*)(\\?(?.+))?"; + /** The Pattern for matching groups within the endpoint regular expression. */ + public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING); + + private File trustStore; + + private AsyncTestSpecification specification; + + private Session session; + + /** + * Create a new consumption task from an Async test specification. + * @param testSpecification The specification holding endpointURL and timeout. + */ + public WebSocketMessageConsumptionTask(AsyncTestSpecification testSpecification) { + this.specification = testSpecification; + } + + /** + * Convenient static method for checking if this implementation will accept endpoint. + * @param endpointUrl The endpoint URL to validate + * @return True if endpointUrl can be used for connecting and consuming on endpoint + */ + public static boolean acceptEndpoint(String endpointUrl) { + return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING); + } + + @Override + public List call() throws Exception { + WebSocketClient client = new WebSocketClient(); + try { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + + if (specification.getSecret() != null && specification.getSecret().getCaCertPem() != null) { + // We neet to create a Truststore holding secret certificxate. + trustStore = ConsumptionTaskCommons.installBrokerCertificate(specification); + + SSLContext sslContext = SSLContexts.custom() + .loadTrustMaterial(trustStore, ConsumptionTaskCommons.TRUSTSTORE_PASSWORD.toCharArray(), null) + .build(); + + // Then configure the Client Endpoint using a property specific to Undertow impl (from Quarkus). + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + config.getUserProperties().put("io.undertow.websocket.SSL_CONTEXT", sslContext); + + // Then connect replacing ws:// by wss:// + session = container.connectToServer(client, config, + URI.create(specification.getEndpointUrl().replace("ws://", "wss://"))); + } else { + // Simply connect to plain text WebSocket... + session = container.connectToServer(client, URI.create(specification.getEndpointUrl())); + } + } catch (Exception e) { + logger.errorf("Connection error while try to reach {%s}", specification.getEndpointUrl()); + throw e; + } + + Thread.sleep(specification.getTimeoutMS()); + if (session != null) { + try { + session.close(); + } catch (Exception e) { + logger.info("Exception while closing the WebSocket session: " + e.getMessage()); + } + } + return client.getMessages(); + } + + /** + * Close the resources used by this task. Namely the WS client session and the optionally created truststore holding + * server client SSL credentials. + * @throws IOException should not happen. + */ + @Override + public void close() throws IOException { + if (session != null) { + try { + session.close(); + } catch (Exception e) { + logger.warn("Closing WebSocket session raised an exception", e); + } + } + if (trustStore != null && trustStore.exists()) { + trustStore.delete(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AMQPProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AMQPProducerManager.java new file mode 100644 index 000000000..bd1fa4650 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AMQPProducerManager.java @@ -0,0 +1,185 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.util.el.TemplateEngine; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeoutException; + +/** + * AMQP 0.9.1 (ie. RabbitMQ) implementation of producer for async event messages. + * + * @author laurent + */ +@ApplicationScoped +public class AMQPProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private Connection amqpConnection; + + @ConfigProperty(name = "amqp.server") + String amqpServer; + + @ConfigProperty(name = "amqp.clientid", defaultValue = "microcks-async-minion") + String amqpClientId; + + @ConfigProperty(name = "amqp.username") + String amqpUsername; + + @ConfigProperty(name = "amqp.password") + String amqpPassword; + + /** + * Initialize the AMQP connection post construction. + * + * @throws Exception If connection to AMQP Broker cannot be done. + */ + @PostConstruct + public void create() throws Exception { + try { + amqpConnection = createConnection(); + } catch (Exception e) { + logger.errorf("Cannot connect to AMQP broker %s", amqpServer); + logger.errorf("Connection exception: %s", e.getMessage()); + throw e; + } + } + + /** + * @return A newly created connection to configured broker + * @throws Exception in case of connection failure + */ + protected Connection createConnection() throws Exception { + ConnectionFactory factory = new ConnectionFactory(); + factory.setUri("amqp://" + amqpServer); + + if (amqpUsername != null && amqpUsername.length() > 0 && amqpPassword != null && amqpPassword.length() > 0) { + logger.infof("Connecting to AMQP broker with user '%s'", amqpUsername); + factory.setUsername(amqpUsername); + factory.setPassword(amqpPassword); + } + factory.setAutomaticRecoveryEnabled(true); + return factory.newConnection(amqpClientId); + } + + /** + * Publish a message on specified destination. + * + * @param destinationType The type of destination (queue, topic, fanout, ...) + * @param destinationName The name of destination + * @param value The message payload + * @param headers A set of headers if any (maybe null or empty) + */ + public void publishMessage(String destinationType, String destinationName, String value, Set
headers) { + logger.infof("Publishing on destination {%s}, message: %s ", destinationName, value); + try (Channel channel = amqpConnection.createChannel()) { + channel.exchangeDeclare(destinationName, destinationType); + + AMQP.BasicProperties properties = null; + // Adding headers to properties if provided. + if (headers != null && !headers.isEmpty()) { + Map amqpHeaders = new HashMap<>(); + for (Header header : headers) { + amqpHeaders.put(header.getName(), header.getValues().toArray()[0]); + } + properties = new AMQP.BasicProperties.Builder().headers(amqpHeaders).build(); + } + channel.basicPublish(destinationName, "", properties, value.getBytes(StandardCharsets.UTF_8)); + } catch (IOException | TimeoutException ioe) { + logger.warnf("Message %s sending has thrown an exception", ioe); + } + } + + public String getDestinationName(AsyncMockDefinition definition, EventMessage eventMessage) { + // Produce service name part of topic name. + String serviceName = definition.getOwnerService().getName().replace(" ", ""); + serviceName = serviceName.replace("-", ""); + + // Produce version name part of topic name. + String versionName = definition.getOwnerService().getVersion().replace(" ", ""); + + // Produce operation name part of topic name. + String operationName = ProducerManager.getDestinationOperationPart(definition.getOperation(), eventMessage); + + // Aggregate the 3 parts using '_' as delimiter. + return serviceName + "-" + versionName + "-" + operationName; + } + + /** + * Render Microcks headers using the template engine. + * + * @param engine The template engine to reuse (because we do not want to initialize and manage a context at the + * KafkaProducerManager level.) + * @param headers The Microcks event message headers definition. + * @return A set of rendered Microcks headers. + */ + public Set
renderEventMessageHeaders(TemplateEngine engine, Set
headers) { + if (headers != null && !headers.isEmpty()) { + Set
renderedHeaders = new HashSet<>(headers.size()); + + for (Header header : headers) { + Optional optionalValue = header.getValues().stream().findFirst(); + if (optionalValue.isPresent()) { + String firstValue = optionalValue.get(); + if (firstValue.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + try { + Header renderedHeader = new Header(); + renderedHeader.setName(header.getName()); + renderedHeader.setValues(Set.of(engine.getValue(firstValue))); + renderedHeaders.add(renderedHeader); + } catch (Throwable t) { + logger.error("Failing at evaluating template " + firstValue, t); + Header renderedHeader = new Header(); + renderedHeader.setName(header.getName()); + renderedHeader.setValues(Set.of(firstValue)); + renderedHeaders.add(renderedHeader); + } + } else { + Header renderedHeader = new Header(); + renderedHeader.setName(header.getName()); + renderedHeader.setValues(Set.of(firstValue)); + renderedHeaders.add(renderedHeader); + } + } + } + return renderedHeaders; + } + + return Collections.emptySet(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonCredentialsProviderType.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonCredentialsProviderType.java new file mode 100644 index 000000000..aa24004ee --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonCredentialsProviderType.java @@ -0,0 +1,41 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +/** + * A simple enumeration for the different types of AWS credentials provider. + * @author laurent + */ +public enum AmazonCredentialsProviderType { + PROFILE("profile"), + ENV_VARIABLE("env-variable"); + + + private String value; + + AmazonCredentialsProviderType(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return this.getValue(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonSNSProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonSNSProducerManager.java new file mode 100644 index 000000000..b33ccfa53 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonSNSProducerManager.java @@ -0,0 +1,220 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.util.el.TemplateEngine; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sns.SnsClientBuilder; +import software.amazon.awssdk.services.sns.model.CreateTopicRequest; +import software.amazon.awssdk.services.sns.model.ListTopicsRequest; +import software.amazon.awssdk.services.sns.model.ListTopicsResponse; +import software.amazon.awssdk.services.sns.model.MessageAttributeValue; +import software.amazon.awssdk.services.sns.model.Topic; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import java.net.URI; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Amazon WS SNS implementation of producer for async event messages. + * @author laurent + */ +@ApplicationScoped +public class AmazonSNSProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private SnsClient snsClient; + + private final ConcurrentHashMap topicArns = new ConcurrentHashMap<>(); + + AwsCredentialsProvider credentialsProvider; + + @ConfigProperty(name = "amazonsns.region") + String region; + + @ConfigProperty(name = "amazonsns.credentials-type") + AmazonCredentialsProviderType credentialsType; + + @ConfigProperty(name = "amazonsns.credentials-profile-name") + String credentialsProfileName; + + @ConfigProperty(name = "amazonsns.credentials-profile-location") + String credentialsProfileLocation; + + @ConfigProperty(name = "amazonsns.endpoint-override") + Optional endpointOverride; + + /** + * Initialize the AWS SNS connection post construction. + * @throws Exception If connection to SQS cannot be done. + */ + @PostConstruct + public void create() throws Exception { + try { + switch (credentialsType) { + case ENV_VARIABLE: + credentialsProvider = EnvironmentVariableCredentialsProvider.create(); + break; + case PROFILE: + if (credentialsProfileLocation != null && credentialsProfileLocation.length() > 0) { + ProfileFile profileFile = ProfileFile.builder().type(ProfileFile.Type.CREDENTIALS) + .content(Paths.get(credentialsProfileLocation)).build(); + + credentialsProvider = ProfileCredentialsProvider.builder().profileFile(profileFile) + .profileName(credentialsProfileName).build(); + } + break; + } + + // Default if not set. + if (credentialsProvider == null) { + credentialsProvider = DefaultCredentialsProvider.create(); + } + + // Now create the SNS client with credential provider. + SnsClientBuilder snsClientBuilder = SnsClient.builder().region(Region.of(region)) + .credentialsProvider(credentialsProvider); + + if (endpointOverride.filter(URI::isAbsolute).isPresent()) { + snsClientBuilder.endpointOverride(endpointOverride.get()); + } + + snsClient = snsClientBuilder.build(); + } catch (Exception e) { + logger.errorf("Cannot connect to AWS SNS region %s", region); + logger.errorf("Connection exception: %s", e.getMessage()); + throw e; + } + } + + /** + * Publish a message on specified AWS SNS topic. + * @param topic The short name of topic within the configured region + * @param value The message payload + * @param headers The headers if any (as rendered by renderEventMessageHeaders() method) + */ + public void publishMessage(String topic, String value, Map headers) { + logger.infof("Publishing on topic {%s}, message: %s ", topic, value); + try { + if (topicArns.get(topic) == null) { + boolean exists = false; + // Ensure topic exists on AWS by trying to get it in the list. + ListTopicsRequest listRequest = ListTopicsRequest.builder().build(); + ListTopicsResponse listResponse = snsClient.listTopics(listRequest); + + if (listResponse.hasTopics() && !listResponse.topics().isEmpty()) { + logger.infof("listResponse.hasTopics(): %d", listResponse.topics().size()); + for (Topic topicTopic : listResponse.topics()) { + logger.infof("Found AWS SNS topic: %s", topicTopic.toString()); + if (topicTopic.topicArn().endsWith(":" + topic)) { + topicArns.put(topic, topicTopic.topicArn()); + exists = true; + break; + } + } + } + + if (!exists) { + topicArns.put(topic, createTopicAndGetArn(topic)); + } + } + + // Retrieve topic ARN from local defs and publish message. + String topicArn = topicArns.get(topic); + snsClient.publish(pr -> pr.topicArn(topicArn).message(value).messageAttributes(headers).build()); + } catch (Throwable t) { + logger.warnf("Message sending has thrown an exception", t); + // As it may be relative to queue being deleted and recreated so having different id + // than previous and thus url, we should clean our cache. + topicArns.remove(topic); + } + } + + /** + * Render Microcks headers using the template engine. + * @param engine The template engine to reuse (because we do not want to initialize and manage a context at the + * AmazonSQSProducerManager level.) + * @param headers The Microcks event message headers definition. + * @return A map of rendered headers of Amazon SQS message sender. + */ + public Map renderEventMessageHeaders(TemplateEngine engine, Set
headers) { + if (headers != null && !headers.isEmpty()) { + return headers.stream().collect(Collectors.toMap(Header::getName, header -> { + String firstValue = header.getValues().stream().findFirst().get(); + String finaleValue = firstValue; + if (firstValue.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + try { + finaleValue = engine.getValue(firstValue); + } catch (Throwable t) { + logger.error("Failing at evaluating template " + firstValue, t); + } + } + return MessageAttributeValue.builder().stringValue(finaleValue).dataType("String").build(); + })); + } + return null; + } + + /** + * Compute a topic name from async mock definition. + * @param definition The mock definition + * @param eventMessage The event message to get dynamic part from + * @return The short name of a SNS topic + */ + public String getTopicName(AsyncMockDefinition definition, EventMessage eventMessage) { + logger.debugf("AsyncAPI Operation {%s}", definition.getOperation().getName()); + + // Produce service name part of topic name. + String serviceName = definition.getOwnerService().getName().replace(" ", ""); + serviceName = serviceName.replace("-", ""); + + // Produce version name part of topic name. + String versionName = definition.getOwnerService().getVersion().replace(" ", ""); + versionName = versionName.replace(".", ""); + + // Produce operation name part of topic name. + String operationName = ProducerManager.getDestinationOperationPart(definition.getOperation(), eventMessage); + + // Aggregate the 3 parts using '-' as delimiter. + return serviceName + "-" + versionName + "-" + operationName.replace("/", "-"); + } + + private String createTopicAndGetArn(String topicName) { + logger.infof("Creating new AWS SNS topic: %s", topicName); + CreateTopicRequest createTopicRequest = CreateTopicRequest.builder().name(topicName).build(); + return snsClient.createTopic(createTopicRequest).topicArn(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonSQSProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonSQSProducerManager.java new file mode 100644 index 000000000..e3aff559e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/AmazonSQSProducerManager.java @@ -0,0 +1,207 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.util.el.TemplateEngine; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.SqsClientBuilder; +import software.amazon.awssdk.services.sqs.model.CreateQueueRequest; +import software.amazon.awssdk.services.sqs.model.ListQueuesRequest; +import software.amazon.awssdk.services.sqs.model.ListQueuesResponse; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import java.net.URI; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Amazon WS SQS implementation of producer for async event messages. + * @author laurent + */ +@ApplicationScoped +public class AmazonSQSProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private SqsClient sqsClient; + + private final ConcurrentHashMap queueUrls = new ConcurrentHashMap<>(); + + AwsCredentialsProvider credentialsProvider; + + @ConfigProperty(name = "amazonsqs.region") + String region; + + @ConfigProperty(name = "amazonsqs.credentials-type") + AmazonCredentialsProviderType credentialsType; + + @ConfigProperty(name = "amazonsqs.credentials-profile-name") + String credentialsProfileName; + + @ConfigProperty(name = "amazonsqs.credentials-profile-location") + String credentialsProfileLocation; + + @ConfigProperty(name = "amazonsqs.endpoint-override") + Optional endpointOverride; + + /** + * Initialize the AWS SQS connection post construction. + * @throws Exception If connection to SQS cannot be done. + */ + @PostConstruct + public void create() throws Exception { + try { + switch (credentialsType) { + case ENV_VARIABLE: + credentialsProvider = EnvironmentVariableCredentialsProvider.create(); + break; + case PROFILE: + if (credentialsProfileLocation != null && credentialsProfileLocation.length() > 0) { + ProfileFile profileFile = ProfileFile.builder().type(ProfileFile.Type.CREDENTIALS) + .content(Paths.get(credentialsProfileLocation)).build(); + + credentialsProvider = ProfileCredentialsProvider.builder().profileFile(profileFile) + .profileName(credentialsProfileName).build(); + } + break; + } + + // Default if not set. + if (credentialsProvider == null) { + credentialsProvider = DefaultCredentialsProvider.create(); + } + + // Now create the SQS client with credential provider. + SqsClientBuilder sqsClientBuilder = SqsClient.builder().region(Region.of(region)) + .credentialsProvider(credentialsProvider); + + if (endpointOverride.filter(URI::isAbsolute).isPresent()) { + sqsClientBuilder.endpointOverride(endpointOverride.get()); + } + sqsClient = sqsClientBuilder.build(); + } catch (Exception e) { + logger.errorf("Cannot connect to AWS SQS region %s", region); + logger.errorf("Connection exception: %s", e.getMessage()); + throw e; + } + } + + /** + * Publish a message on specified AWS SQS queue. + * @param queue The short name of queue within the configured region + * @param value The message payload + * @param headers The headers if any (as rendered by renderEventMessageHeaders() method) + */ + public void publishMessage(String queue, String value, Map headers) { + logger.infof("Publishing on queue {%s}, message: %s ", queue, value); + + try { + if (queueUrls.get(queue) == null) { + // Ensure queue exists on AWS by trying to get it in the list. + ListQueuesRequest listRequest = ListQueuesRequest.builder().queueNamePrefix(queue).maxResults(1).build(); + ListQueuesResponse listResponse = sqsClient.listQueues(listRequest); + + if (listResponse.hasQueueUrls()) { + logger.infof("Found AWS SQS queue: %s", listResponse.queueUrls().get(0)); + queueUrls.put(queue, listResponse.queueUrls().get(0)); + } else { + queueUrls.put(queue, createQueueAndGetURL(queue)); + } + } + + // Retrieve queue URL from local defs and publish message. + String queueUrl = queueUrls.get(queue); + sqsClient.sendMessage(mr -> mr.queueUrl(queueUrl).messageBody(value).messageAttributes(headers).build()); + } catch (Throwable t) { + logger.warnf("Message sending has thrown an exception", t); + // As it may be relative to queue being deleted and recreated so having different id + // than previous and thus url, we should clean our cache. + queueUrls.remove(queue); + } + } + + /** + * Render Microcks headers using the template engine. + * @param engine The template engine to reuse (because we do not want to initialize and manage a context at the + * AmazonSQSProducerManager level.) + * @param headers The Microcks event message headers definition. + * @return A map of rendered headers of Amazon SQS message sender. + */ + public Map renderEventMessageHeaders(TemplateEngine engine, Set
headers) { + if (headers != null && !headers.isEmpty()) { + return headers.stream().collect(Collectors.toMap(Header::getName, header -> { + String firstValue = header.getValues().stream().findFirst().get(); + String finaleValue = firstValue; + if (firstValue.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + try { + finaleValue = engine.getValue(firstValue); + } catch (Throwable t) { + logger.error("Failing at evaluating template " + firstValue, t); + } + } + return MessageAttributeValue.builder().stringValue(finaleValue).dataType("String").build(); + })); + } + return null; + } + + /** + * Compute a queue name from async mock definition. + * @param definition The mock definition + * @param eventMessage The event message to get dynamic part from + * @return The short name of a SQS queue + */ + public String getQueueName(AsyncMockDefinition definition, EventMessage eventMessage) { + // Produce service name part of topic name. + String serviceName = definition.getOwnerService().getName().replace(" ", ""); + serviceName = serviceName.replace("-", ""); + + // Produce version name part of topic name. + String versionName = definition.getOwnerService().getVersion().replace(" ", ""); + versionName = versionName.replace(".", ""); + + // Produce operation name part of topic name. + String operationName = ProducerManager.getDestinationOperationPart(definition.getOperation(), eventMessage); + + // Aggregate the 3 parts using '-' as delimiter. + return serviceName + "-" + versionName + "-" + operationName.replace("/", "-"); + } + + private String createQueueAndGetURL(String queueName) { + logger.infof("Creating new AWS SQS queue: %s", queueName); + CreateQueueRequest createQueueRequest = CreateQueueRequest.builder().queueName(queueName).build(); + return sqsClient.createQueue(createQueueRequest).queueUrl(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/GooglePubSubProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/GooglePubSubProducerManager.java new file mode 100644 index 000000000..1c832affd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/GooglePubSubProducerManager.java @@ -0,0 +1,222 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.util.el.TemplateEngine; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.gax.core.CredentialsProvider; +import com.google.api.gax.core.ExecutorProvider; +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.core.InstantiatingExecutorProvider; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.rpc.NotFoundException; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.pubsub.v1.Publisher; +import com.google.cloud.pubsub.v1.TopicAdminClient; +import com.google.cloud.pubsub.v1.TopicAdminSettings; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.ByteString; +import com.google.pubsub.v1.PubsubMessage; +import com.google.pubsub.v1.TopicName; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Google Cloud PubSub implementation of producer for async event messages. + * @author laurent + */ +@ApplicationScoped +public class GooglePubSubProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private static final String CLOUD_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; + + /** + * As {@link Publisher} is bound to topic, default would be to create it at each invocation. We'll use this cache, + * that enforces only one {@link Publisher} per PubSub topic exists. + */ + private final ConcurrentHashMap publishers = new ConcurrentHashMap<>(); + + /** + * As {@link Publisher} is by default associated to its own executor, we have to override this to avoid wasting + * resources. As the push of mock messages is sequential, 1 thread is enough. + */ + private final ExecutorProvider executorProvider = InstantiatingExecutorProvider.newBuilder() + .setExecutorThreadCount(1).build(); + + CredentialsProvider credentialsProvider; + + @ConfigProperty(name = "googlepubsub.project") + String project; + + @ConfigProperty(name = "googlepubsub.service-account-location") + String serviceAccountLocation; + + /** + * Initialize the PubSub connection post construction. + * @throws Exception If connection to PubSub cannot be done. + */ + @PostConstruct + public void create() throws Exception { + try { + if (serviceAccountLocation != null && serviceAccountLocation.length() > 0) { + FileInputStream is = new FileInputStream(serviceAccountLocation); + credentialsProvider = FixedCredentialsProvider + .create(GoogleCredentials.fromStream(is).createScoped(CLOUD_OAUTH_SCOPE)); + } else { + credentialsProvider = NoCredentialsProvider.create(); + } + } catch (Exception e) { + logger.errorf("Cannot read Google Cloud credentials %s", serviceAccountLocation); + throw e; + } + } + + /** + * Publish a message on specified PubSub topic. + * @param topic The short name of topic within the configured project + * @param value The message payload + * @param headers The headers if any (as rendered by renderEventMessageHeaders() method) + */ + public void publishMessage(String topic, String value, Map headers) { + logger.infof("Publishing on topic {%s}, message: %s ", topic, value); + + try { + if (publishers.get(topic) == null) { + // Ensure topic exists on PubSub. + TopicName topicName = TopicName.of(project, topic); + + TopicAdminSettings topicAdminSettings = TopicAdminSettings.newBuilder() + .setCredentialsProvider(credentialsProvider).build(); + TopicAdminClient topicAdminClient = TopicAdminClient.create(topicAdminSettings); + + ensureTopicExists(topicAdminClient, topicName); + } + + // Build a message for corresponding publisher. + Publisher publisher = getPublisher(topic); + ByteString data = ByteString.copyFromUtf8(value); + PubsubMessage pubsubMessage = PubsubMessage.newBuilder().setData(data).putAllAttributes(headers).build(); + + // Publish the message. + ApiFuture messageIdFuture = publisher.publish(pubsubMessage); + ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback<>() { + // Wait for message submission and log the result + public void onSuccess(String messageId) { + logger.debugv("Published with message id {0}", messageId); + } + + public void onFailure(Throwable t) { + logger.debugv("Failed to publish: {0}", t); + } + }, MoreExecutors.directExecutor()); + } catch (IOException ioe) { + logger.warnf("Message sending has thrown an exception", ioe); + } + } + + /** + * Render Microcks headers using the template engine. + * @param engine The template engine to reuse (because we do not want to initialize and manage a context at the + * GooglePubSubProducerManager level.) + * @param headers The Microcks event message headers definition. + * @return A map of rendered headers for GCP Publisher. + */ + public Map renderEventMessageHeaders(TemplateEngine engine, Set
headers) { + if (headers != null && !headers.isEmpty()) { + return headers.stream().collect(Collectors.toMap(Header::getName, header -> { + String firstValue = header.getValues().stream().findFirst().get(); + if (firstValue.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + try { + return engine.getValue(firstValue); + } catch (Exception e) { + logger.error("Failing at evaluating template " + firstValue, e); + return firstValue; + } + } + return firstValue; + })); + } + return Collections.emptyMap(); + } + + /** + * Compute a topic name from async mock definition. + * @param definition The mock definition + * @param eventMessage The event message to get dynamic part from + * @return The short name of a PubSub topic + */ + public String getTopicName(AsyncMockDefinition definition, EventMessage eventMessage) { + // Produce service name part of topic name. + String serviceName = definition.getOwnerService().getName().replace(" ", ""); + serviceName = serviceName.replace("-", ""); + + // Produce version name part of topic name. + String versionName = definition.getOwnerService().getVersion().replace(" ", ""); + + // Produce operation name part of topic name. + String operationName = ProducerManager.getDestinationOperationPart(definition.getOperation(), eventMessage); + + // Aggregate the 3 parts using '_' as delimiter. + return serviceName + "-" + versionName + "-" + operationName; + } + + private void ensureTopicExists(TopicAdminClient topicAdminClient, TopicName topicName) { + try { + topicAdminClient.getTopic(topicName); + } catch (NotFoundException nfe) { + logger.infof("Topic {%s} does not exist yet, creating it", topicName); + topicAdminClient.createTopic(topicName); + } + } + + private Publisher getPublisher(String topic) throws IOException { + try { + return publishers.computeIfAbsent(topic, this::createPublisher); + } catch (RuntimeException re) { + // Just unwrap the underlying IOException... + throw (IOException) re.getCause(); + } + } + + private Publisher createPublisher(String topic) { + try { + return Publisher.newBuilder(TopicName.of(project, topic)).setExecutorProvider(executorProvider) + .setCredentialsProvider(credentialsProvider).build(); + } catch (IOException ioe) { + logger.errorf("IOException while creating a Publisher for topic %s", topic); + throw new RuntimeException("IOException while creating a Publisher for topic ", ioe); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/KafkaProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/KafkaProducerManager.java new file mode 100644 index 000000000..a078be3e6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/KafkaProducerManager.java @@ -0,0 +1,313 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.apicurio.registry.serde.SerdeConfig; +import io.apicurio.registry.serde.avro.AvroKafkaSerializer; +import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; +import io.confluent.kafka.serializers.KafkaAvroSerializer; +import io.github.microcks.domain.EventMessage; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.util.el.TemplateEngine; +import org.apache.avro.generic.GenericRecord; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.config.SaslConfigs; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.header.Header; +import org.apache.kafka.common.header.internals.RecordHeader; +import org.apache.kafka.common.serialization.ByteArraySerializer; +import org.apache.kafka.common.serialization.StringSerializer; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.*; + +/** + * Kafka implementation of producer for async event messages. + * @author laurent + */ +@ApplicationScoped +public class KafkaProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private Producer producer; + + private Producer bytesProducer; + + private Producer registryProducer; + + @Inject + Config config; + + @ConfigProperty(name = "kafka.bootstrap.servers") + String bootstrapServers; + + @ConfigProperty(name = "kafka.schema.registry.url", defaultValue = "") + Optional schemaRegistryUrl; + + @ConfigProperty(name = "kafka.schema.registry.confluent", defaultValue = "false") + boolean schemaRegistryConfluent = false; + + @ConfigProperty(name = "kafka.schema.registry.username", defaultValue = "") + Optional schemaRegistryUsername; + + @ConfigProperty(name = "kafka.schema.registry.credentials.source", defaultValue = "USER_INFO") + String schemaRegistryCredentialsSource = null; + + /** + * Tells if producer is connected to a Schema registry and thus able to send Avro GenericRecord. + * @return True if connected to a Schema registry, false otherwise. + */ + public boolean isRegistryEnabled() { + return registryProducer != null; + } + + @PostConstruct + public void create() { + producer = createProducer(); + bytesProducer = createBytesProducer(); + registryProducer = createRegistryProducer(); + } + + protected Producer createProducer() { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.CLIENT_ID_CONFIG, "microcks-async-minion-str-producer"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + // Add security related properties. + completePropertiesWithSecurityConfig(props, config); + return new KafkaProducer<>(props); + } + + protected Producer createBytesProducer() { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.CLIENT_ID_CONFIG, "microcks-async-minion-bytes-producer"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getName()); + // Add security related properties. + completePropertiesWithSecurityConfig(props, config); + return new KafkaProducer<>(props); + } + + protected Producer createRegistryProducer() { + if (schemaRegistryUrl.isPresent()) { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.CLIENT_ID_CONFIG, "microcks-async-minion-registry-producer"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + + // Configure Schema registry access. + if (schemaRegistryConfluent) { + // Put Confluent Registry specific SerDes class and registry properties. + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KafkaAvroSerializer.class.getName()); + + props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, schemaRegistryUrl.get()); + // If authentication turned on (see https://docs.confluent.io/platform/current/security/basic-auth.html#basic-auth-sr) + if (schemaRegistryUsername.isPresent()) { + props.put(AbstractKafkaSchemaSerDeConfig.USER_INFO_CONFIG, schemaRegistryUsername.get()); + props.put(AbstractKafkaSchemaSerDeConfig.BASIC_AUTH_CREDENTIALS_SOURCE, schemaRegistryCredentialsSource); + } + props.put(AbstractKafkaSchemaSerDeConfig.AUTO_REGISTER_SCHEMAS, true); + // Map the topic name to the artifactId in the registry + props.put(AbstractKafkaSchemaSerDeConfig.VALUE_SUBJECT_NAME_STRATEGY, + io.confluent.kafka.serializers.subject.TopicRecordNameStrategy.class.getName()); + } else { + // Put Apicurio Registry specific SerDes class and registry properties. + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, AvroKafkaSerializer.class.getName()); + + props.put(SerdeConfig.REGISTRY_URL, schemaRegistryUrl.get()); + props.put(SerdeConfig.ENABLE_CONFLUENT_ID_HANDLER, true); + // Get an existing schema or auto-register if not found. + props.put(SerdeConfig.SCHEMA_RESOLVER, io.apicurio.registry.resolver.DefaultSchemaResolver.class.getName()); + props.put(SerdeConfig.AUTO_REGISTER_ARTIFACT, true); + // Set artifact strategy as the same as Confluent default subject strategy. + props.put(SerdeConfig.ARTIFACT_RESOLVER_STRATEGY, + io.apicurio.registry.serde.strategy.TopicIdStrategy.class.getName()); + } + + // Add security related properties. + completePropertiesWithSecurityConfig(props, config); + return new KafkaProducer<>(props); + } + return null; + } + + /** + * Publish a message on specified topic. + * @param topic The destination topic for message + * @param key The message key + * @param value The message payload + * @param headers A set of headers if any (maybe null or empty) + */ + public void publishMessage(String topic, String key, String value, Set
headers) { + logger.infof("Publishing on topic {%s}, message: %s ", topic, value); + ProducerRecord kafkaRecord = new ProducerRecord<>(topic, key, value); + addHeadersToRecord(kafkaRecord, headers); + producer.send(kafkaRecord); + producer.flush(); + } + + /** + * Publish a raw byte array message on specified topic. + * @param topic The destination topic for message + * @param key The message key + * @param value The message payload + * @param headers A set of headers if any (maybe null or empty) + */ + public void publishMessage(String topic, String key, byte[] value, Set
headers) { + logger.infof("Publishing on topic {%s}, bytes: %s ", topic, new String(value)); + ProducerRecord kafkaRecord = new ProducerRecord<>(topic, key, value); + addHeadersToRecord(kafkaRecord, headers); + bytesProducer.send(kafkaRecord); + bytesProducer.flush(); + } + + /** + * Publish an Avro GenericRecord built with Schema onto specified topic and using underlying schema registry. + * @param topic The destination topic for message + * @param key The message key + * @param value The message payload + * @param headers A set of headers if any (maybe null or empty) + */ + public void publishMessage(String topic, String key, GenericRecord value, Set
headers) { + logger.infof("Publishing on topic {%s}, record: %s ", topic, value.toString()); + ProducerRecord kafkaRecord = new ProducerRecord<>(topic, key, value); + addHeadersToRecord(kafkaRecord, headers); + registryProducer.send(kafkaRecord); + registryProducer.flush(); + } + + /** + * Transform and render Microcks headers into Kafka specific headers. + * @param engine The template engine to reuse (because we do not want to initialize and manage a context at the + * KafkaProducerManager level.) + * @param headers The Microcks event message headers definition. + * @return A set of Kafka headers. + */ + public Set
renderEventMessageHeaders(TemplateEngine engine, Set headers) { + if (headers != null && !headers.isEmpty()) { + Set
renderedHeaders = HashSet.newHashSet(headers.size()); + + for (io.github.microcks.domain.Header header : headers) { + // For Kafka, header is mono valued so just consider the first value. + String firstValue = header.getValues().stream().findFirst().get(); + if (firstValue.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + try { + renderedHeaders.add(new RecordHeader(header.getName(), engine.getValue(firstValue).getBytes())); + } catch (Throwable t) { + logger.error("Failing at evaluating template " + firstValue, t); + renderedHeaders.add(new RecordHeader(header.getName(), firstValue.getBytes())); + } + } else { + renderedHeaders.add(new RecordHeader(header.getName(), firstValue.getBytes())); + } + } + return renderedHeaders; + } + return null; + } + + /** + * Get the Kafka topic name corresponding to a AsyncMockDefinition, sanitizing all parameters. + * @param definition The AsyncMockDefinition + * @param eventMessage The message to get topic + * @return The topic name for definition and event + */ + public String getTopicName(AsyncMockDefinition definition, EventMessage eventMessage) { + // Produce service name part of topic name. + String serviceName = definition.getOwnerService().getName().replace(" ", ""); + serviceName = serviceName.replace("-", ""); + + // Produce version name part of topic name. + String versionName = definition.getOwnerService().getVersion().replace(" ", ""); + + // Produce operation name part of topic name. + String operationName = ProducerManager.getDestinationOperationPart(definition.getOperation(), eventMessage); + operationName = operationName.replace('/', '-'); + + // Aggregate the 3 parts using '_' as delimiter. + return serviceName + "-" + versionName + "-" + operationName; + } + + /** + * Completing the ProducerRecord with the set of provided headers. + * @param kafkaRecord The record to complete + * @param headers The set of headers + */ + protected void addHeadersToRecord(ProducerRecord kafkaRecord, Set
headers) { + if (headers != null) { + for (Header header : headers) { + kafkaRecord.headers().add(header); + } + } + } + + private void completePropertiesWithSecurityConfig(Properties props, Config config) { + Optional securityProtocol = config.getOptionalValue("kafka.security.protocol", String.class); + if (securityProtocol.isPresent()) { + String securityProtocolValue = securityProtocol.get(); + props.put("security.protocol", securityProtocolValue); + + if (config.getOptionalValue("kafka.ssl.truststore.location", String.class).isPresent()) { + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, + config.getValue("kafka.ssl.truststore.location", String.class)); + } + if (config.getOptionalValue("kafka.ssl.truststore.password", String.class).isPresent()) { + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, + config.getValue("kafka.ssl.truststore.password", String.class)); + } + if (config.getOptionalValue("kafka.ssl.truststore.type", String.class).isPresent()) { + props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, + config.getValue("kafka.ssl.truststore.type", String.class)); + } + + switch (securityProtocolValue) { + case "SASL_SSL": + logger.debug("Adding SASL_SSL specific connection properties"); + props.put(SaslConfigs.SASL_MECHANISM, config.getValue("kafka.sasl.mechanism", String.class)); + props.put(SaslConfigs.SASL_JAAS_CONFIG, config.getValue("kafka.sasl.jaas.config", String.class)); + if (config.getOptionalValue("kafka.sasl.login.callback.handler.class", String.class).isPresent()) { + props.put(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS, + config.getValue("kafka.sasl.login.callback.handler.class", String.class)); + } + break; + case "SSL": + logger.debug("Adding SSL specific connection properties"); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, + config.getValue("kafka.ssl.keystore.location", String.class)); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, + config.getValue("kafka.ssl.keystore.password", String.class)); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, config.getValue("kafka.ssl.keystore.type", String.class)); + break; + default: + break; + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/MQTTProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/MQTTProducerManager.java new file mode 100644 index 000000000..4a74c412a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/MQTTProducerManager.java @@ -0,0 +1,131 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.paho.client.mqttv3.IMqttClient; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttPersistenceException; +import org.jboss.logging.Logger; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.minion.async.AsyncMockDefinition; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import java.nio.charset.StandardCharsets; + +/** + * MQTT implementation of producer for async event messages. + * @author laurent + */ +@ApplicationScoped +public class MQTTProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private IMqttClient client; + + @ConfigProperty(name = "mqtt.server") + String mqttServer; + + @ConfigProperty(name = "mqtt.clientid", defaultValue = "microcks-async-minion") + String mqttClientId; + + @ConfigProperty(name = "mqtt.username") + String mqttUsername; + + @ConfigProperty(name = "mqtt.password") + String mqttPassword; + + /** + * Initialize the MQTT client post construction. + * @throws Exception If connection to MQTT Broker cannot be done. + */ + @PostConstruct + public void create() throws Exception { + try { + client = createClient(); + } catch (Exception e) { + logger.errorf("Cannot connect to MQTT broker %s", mqttServer); + logger.errorf("Connection exception: %s", e.getMessage()); + throw e; + } + } + + /** + * Create a IMqttClient and connect it to the server. + * @return A new IMqttClient implementation initialized with configuration properties. + * @throws Exception in case of connection failure + */ + protected IMqttClient createClient() throws Exception { + MqttConnectOptions options = new MqttConnectOptions(); + if (mqttUsername != null && mqttUsername.length() > 0 && mqttPassword != null && mqttPassword.length() > 0) { + logger.infof("Connecting to MQTT broker with user '%s'", mqttUsername); + options.setUserName(mqttUsername); + options.setPassword(mqttPassword.toCharArray()); + } + options.setAutomaticReconnect(true); + // Set clean session to false as we're reusing the same clientId. + options.setCleanSession(false); + options.setConnectionTimeout(20); + options.setKeepAliveInterval(10); + IMqttClient publisher = new MqttClient("tcp://" + mqttServer, mqttClientId); + publisher.connect(options); + return publisher; + } + + /** + * Publish a message on specified topic. + * @param topic The destination topic for message + * @param value The message payload + */ + public void publishMessage(String topic, String value) { + logger.infof("Publishing on topic {%s}, message: %s ", topic, value); + try { + client.publish(topic, value.getBytes(StandardCharsets.UTF_8), 0, false); + } catch (MqttPersistenceException mpe) { + logger.warnf("MqttPersistenceException caught while publishing message, ignoring it", mpe); + } catch (MqttException me) { + logger.warnf("MqttException caught while publishing messageMqttException", me); + } + } + + /** + * Get the MQTT topic name corresponding to a AsyncMockDefinition, sanitizing all parameters. + * @param definition The AsyncMockDefinition + * @param eventMessage The message to get topic + * @return The topic name for definition and event + */ + public String getTopicName(AsyncMockDefinition definition, EventMessage eventMessage) { + // Produce service name part of topic name. + String serviceName = definition.getOwnerService().getName().replace(" ", ""); + serviceName = serviceName.replace("-", ""); + + // Produce version name part of topic name. + String versionName = definition.getOwnerService().getVersion().replace(" ", ""); + + // Produce operation name part of topic name. + String operationName = ProducerManager.getDestinationOperationPart(definition.getOperation(), eventMessage); + + // Aggregate the 3 parts using '_' as delimiter. + return serviceName + "-" + versionName + "-" + operationName; + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/NATSProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/NATSProducerManager.java new file mode 100644 index 000000000..b38c3fd09 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/NATSProducerManager.java @@ -0,0 +1,165 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.util.el.TemplateEngine; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +import io.nats.client.Connection; +import io.nats.client.Message; +import io.nats.client.Options; +import io.nats.client.Nats; +import io.nats.client.impl.Headers; +import io.nats.client.impl.NatsMessage; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; +import java.util.Set; + +/** + * NATS implementation of producer for async event messages. + * + * @author laurent + */ +@ApplicationScoped +public class NATSProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + private Connection client; + + @ConfigProperty(name = "nats.server") + String natsServer; + + @ConfigProperty(name = "nats.username") + String natsUsername; + + @ConfigProperty(name = "nats.password") + String natsPassword; + + /** + * Initialize the NATS client post construction. + * + * @throws Exception If connection to NATS Broker cannot be done. + */ + @PostConstruct + public void create() throws Exception { + try { + client = createClient(); + } catch (Exception e) { + logger.errorf("Cannot connect to NATS broker %s", natsServer); + logger.errorf("Connection exception: %s", e.getMessage()); + throw e; + } + } + + /** + * Create a NATS Connection and connect it to the server. + * + * @return A new NATS Connection implementation initialized with configuration properties. + * @throws Exception in case of connection failure + */ + protected Connection createClient() throws Exception { + Options.Builder optionsBuilder = new Options.Builder().server(natsServer).maxReconnects(10); + // Add authentication option. + if (natsUsername != null && natsPassword != null) { + optionsBuilder.userInfo(natsUsername, natsPassword); + } + + client = Nats.connect(optionsBuilder.build()); + return client; + } + + /** + * Publish a message on specified topic. + * + * @param topic The destination topic for message + * @param value The message payload + * @param headers A set of headers if any (maybe null or empty) + */ + public void publishMessage(String topic, String value, Headers headers) { + logger.infof("Publishing on topic {%s}, message: %s ", topic, value); + Message msg = NatsMessage.builder().subject(topic).data(value.getBytes(StandardCharsets.UTF_8)).headers(headers) + .build(); + client.publish(msg); + } + + /** + * Transform and render Microcks headers into NATS specific headers. + * + * @param engine The template engine to reuse (because we do not want to initialize and manage a context at the + * NATSProducerManager level.) + * @param headers The Microcks event message headers definition. + * @return A set of NATS headers. + */ + public Headers renderEventMessageHeaders(TemplateEngine engine, Set headers) { + if (headers != null && !headers.isEmpty()) { + Headers natsHeaders = new Headers(); + for (io.github.microcks.domain.Header header : headers) { + Optional optionalValue = header.getValues().stream().findFirst(); + if (optionalValue.isPresent()) { + String headerValue; + String firstValue = optionalValue.get(); + if (firstValue.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + try { + headerValue = Base64.getEncoder().encodeToString(engine.getValue(firstValue).getBytes()); + } catch (Throwable t) { + logger.error("Failing at evaluating template " + firstValue, t); + headerValue = Base64.getEncoder().encodeToString(firstValue.getBytes()); + } + } else { + headerValue = Base64.getEncoder().encodeToString(firstValue.getBytes()); + } + natsHeaders.add(header.getName(), headerValue); + } + + } + return natsHeaders; + } + + return null; + } + + /** + * Get the NATS topic name corresponding to a AsyncMockDefinition, sanitizing all parameters. + * + * @param definition The AsyncMockDefinition + * @param eventMessage The message to get topic + * @return The topic name for definition and event + */ + public String getTopicName(AsyncMockDefinition definition, EventMessage eventMessage) { + // Produce service name part of topic name. + String serviceName = definition.getOwnerService().getName().replace(" ", ""); + serviceName = serviceName.replace("-", ""); + + // Produce version name part of topic name. + String versionName = definition.getOwnerService().getVersion().replace(" ", ""); + + // Produce operation name part of topic name. + String operationName = ProducerManager.getDestinationOperationPart(definition.getOperation(), eventMessage); + + // Aggregate the 3 parts using '_' as delimiter. + return serviceName + "-" + versionName + "-" + operationName; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/ProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/ProducerManager.java new file mode 100644 index 000000000..7561211e8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/ProducerManager.java @@ -0,0 +1,364 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.Binding; +import io.github.microcks.domain.BindingType; +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.minion.async.AsyncMockRepository; +import io.github.microcks.minion.async.Constants; +import io.github.microcks.minion.async.SchemaRegistry; +import io.github.microcks.util.AvroUtil; +import io.github.microcks.util.SchemaMap; +import io.github.microcks.util.asyncapi.AsyncAPISchemaUtil; +import io.github.microcks.util.asyncapi.AsyncAPISchemaValidator; +import io.github.microcks.util.el.TemplateEngine; +import io.github.microcks.util.el.TemplateEngineFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import io.quarkus.arc.Unremovable; +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericRecord; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * ProducerManager is the responsible for emitting mock event messages when specific frequency triggered is reached. + * Need to specify it as @Unremovable to avoid Quarkus ARC optimization removing beans that are not injected elsewhere + * (this one is resolved using Arc.container().instance() method from ProducerScheduler). + * @author laurent + */ +@Unremovable +@ApplicationScoped +public class ProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + final AsyncMockRepository mockRepository; + + final SchemaRegistry schemaRegistry; + + final KafkaProducerManager kafkaProducerManager; + final MQTTProducerManager mqttProducerManager; + final NATSProducerManager natsProducerManager; + final AMQPProducerManager amqpProducerManager; + final GooglePubSubProducerManager googlePubSubProducerManager; + final AmazonSQSProducerManager amazonSQSProducerManager; + final AmazonSNSProducerManager amazonSNSProducerManager; + + @Inject + @RootWebSocketProducerManager + WebSocketProducerManager wsProducerManager; + + @ConfigProperty(name = "minion.supported-bindings") + String[] supportedBindings; + + @ConfigProperty(name = "minion.default-avro-encoding", defaultValue = "RAW") + String defaultAvroEncoding; + + public ProducerManager(AsyncMockRepository mockRepository, SchemaRegistry schemaRegistry, + KafkaProducerManager kafkaProducerManager, MQTTProducerManager mqttProducerManager, + NATSProducerManager natsProducerManager, AMQPProducerManager amqpProducerManager, + GooglePubSubProducerManager googlePubSubProducerManager, AmazonSQSProducerManager amazonSQSProducerManager, + AmazonSNSProducerManager amazonSNSProducerManager) { + this.mockRepository = mockRepository; + this.schemaRegistry = schemaRegistry; + this.kafkaProducerManager = kafkaProducerManager; + this.mqttProducerManager = mqttProducerManager; + this.natsProducerManager = natsProducerManager; + this.amqpProducerManager = amqpProducerManager; + this.googlePubSubProducerManager = googlePubSubProducerManager; + this.amazonSQSProducerManager = amazonSQSProducerManager; + this.amazonSNSProducerManager = amazonSNSProducerManager; + } + + /** + * Produce all the async mock messages corresponding to specified frequency. + * @param frequency The frequency to emit messages for + */ + public void produceAsyncMockMessagesAt(Long frequency) { + logger.info("Producing async mock messages for frequency: " + frequency); + + Set mockDefinitions = mockRepository.getMockDefinitionsByFrequency(frequency); + for (AsyncMockDefinition definition : mockDefinitions) { + logger.debugf("Processing definition of service {%s}", + definition.getOwnerService().getName() + ':' + definition.getOwnerService().getVersion()); + + for (String binding : definition.getOperation().getBindings().keySet()) { + // Ensure this minion supports this binding. + if (Arrays.asList(supportedBindings).contains(binding)) { + Binding bindingDef = definition.getOperation().getBindings().get(binding); + + switch (BindingType.valueOf(binding)) { + case KAFKA: + produceKafkaMockMessages(definition); + break; + case NATS: + produceNatsMockMessages(definition); + break; + case MQTT: + produceMQTTMocksMessages(definition); + break; + case WS: + produceWSMockMessages(definition); + break; + case AMQP: + produceAMQPMockMessages(definition, bindingDef); + break; + case GOOGLEPUBSUB: + produceGooglePubSubMockMessages(definition); + break; + case SQS: + produceSQSMockMessages(definition); + break; + case SNS: + produceSNSMockMessages(definition); + break; + default: + break; + } + } + } + } + } + + /** Take care publishing Kafka mock messages for definition. */ + protected void produceKafkaMockMessages(AsyncMockDefinition definition) { + for (EventMessage eventMessage : definition.getEventMessages()) { + String topic = kafkaProducerManager.getTopicName(definition, eventMessage); + String key = String.valueOf(System.currentTimeMillis()); + String message = renderEventMessageContent(eventMessage); + + // Check it Avro binary is expected, we should convert to bytes. + if (Constants.AVRO_BINARY_CONTENT_TYPES.contains(eventMessage.getMediaType())) { + // Retrieve an Avro schema for this operation. + Schema schema = null; + + // First browse schema entries for this operation. + List entries = schemaRegistry.getSchemaEntries(definition.getOwnerService()) + .stream().filter(entry -> entry.getOperations() != null + && entry.getOperations().contains(definition.getOperation().getName())) + .toList(); + + if (entries.isEmpty()) { + // If no entry found for the operation, we have to extract Avro schema from the AsyncAPI spec. + entries = schemaRegistry.getSchemaEntries(definition.getOwnerService()).stream() + .filter(entry -> ResourceType.ASYNC_API_SPEC.equals(entry.getType())).toList(); + + SchemaMap schemaMap = new SchemaMap(); + schemaRegistry.getSchemaEntries(definition.getOwnerService()) + .forEach(schemaEntry -> schemaMap.putSchemaEntry(schemaEntry.getPath(), schemaEntry.getContent())); + + try { + // Extract embedded Avro schema from AsyncAPI spec. + JsonNode specificationNode = AsyncAPISchemaValidator + .getJsonNodeForSchema(entries.getFirst().getContent()); + schema = AsyncAPISchemaUtil.retrieveMessageAvroSchema(specificationNode, AsyncAPISchemaUtil + .findMessagePathPointer(specificationNode, definition.getOperation().getName()), schemaMap); + + if (schema.isUnion() && schema.getTypes().size() == 1) { + schema = schema.getTypes().getFirst(); + } + } catch (Exception e) { + logger.errorf("Exception while extracting Avro schema from AsyncAPI spec", e); + } + } else { + // Directly get the Avro schema from one schema entry (.asvc file as external ref). + schema = AvroUtil.getSchema(entries.getFirst().getContent()); + } + + if (schema != null) { + logger.debugf("Found an Avro schema '%s' for operation '%s'", schema, + definition.getOperation().getName()); + + try { + if (Constants.REGISTRY_AVRO_ENCODING.equals(defaultAvroEncoding) + && kafkaProducerManager.isRegistryEnabled()) { + logger.debug("Using a registry and converting message to Avro record"); + GenericRecord avroRecord = AvroUtil.jsonToAvroRecord(message, schema); + kafkaProducerManager.publishMessage(topic, key, avroRecord, + kafkaProducerManager.renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), + eventMessage.getHeaders())); + } else { + logger.debug("Converting message to Avro bytes array"); + byte[] avroBinary = AvroUtil.jsonToAvro(message, schema); + kafkaProducerManager.publishMessage(topic, key, avroBinary, + kafkaProducerManager.renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), + eventMessage.getHeaders())); + } + } catch (Exception e) { + logger.errorf("Exception while converting {%s} to Avro using schema {%s}", message, schema.toString(), + e); + } + } else { + logger.warnf("Failed finding a suitable Avro schema for the '%s' operation. No publication done.", + definition.getOperation().getName()); + } + } else { + kafkaProducerManager.publishMessage(topic, key, message, kafkaProducerManager + .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders())); + } + } + } + + /** Take care publishing Nats mock messages for definition. */ + protected void produceNatsMockMessages(AsyncMockDefinition definition) { + for (EventMessage eventMessage : definition.getEventMessages()) { + String topic = natsProducerManager.getTopicName(definition, eventMessage); + String message = renderEventMessageContent(eventMessage); + natsProducerManager.publishMessage(topic, message, natsProducerManager + .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders())); + } + } + + /** Take care publishing MQTT mock messages for definition. */ + protected void produceMQTTMocksMessages(AsyncMockDefinition definition) { + for (EventMessage eventMessage : definition.getEventMessages()) { + String topic = mqttProducerManager.getTopicName(definition, eventMessage); + String message = renderEventMessageContent(eventMessage); + mqttProducerManager.publishMessage(topic, message); + } + } + + /** Take care publishing WebSocket mock messages for definition. */ + protected void produceWSMockMessages(AsyncMockDefinition definition) { + for (EventMessage eventMessage : definition.getEventMessages()) { + String channel = wsProducerManager.getRequestURI(definition, eventMessage); + String message = renderEventMessageContent(eventMessage); + wsProducerManager.publishMessage(channel, message, eventMessage.getHeaders()); + } + } + + /** Take care publishing AMQP mock messages for definition. */ + protected void produceAMQPMockMessages(AsyncMockDefinition definition, Binding bindingDef) { + for (EventMessage eventMessage : definition.getEventMessages()) { + String destinationName = amqpProducerManager.getDestinationName(definition, eventMessage); + String message = renderEventMessageContent(eventMessage); + amqpProducerManager.publishMessage(bindingDef.getDestinationType(), destinationName, message, + amqpProducerManager.renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), + eventMessage.getHeaders())); + } + } + + /** Take care publishing Google PubSub mock messages for definition. */ + protected void produceGooglePubSubMockMessages(AsyncMockDefinition definition) { + for (EventMessage eventMessage : definition.getEventMessages()) { + String topicName = googlePubSubProducerManager.getTopicName(definition, eventMessage); + String message = renderEventMessageContent(eventMessage); + googlePubSubProducerManager.publishMessage(topicName, message, googlePubSubProducerManager + .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders())); + } + } + + /** Take care publishing SQS mock messages for definition. */ + protected void produceSQSMockMessages(AsyncMockDefinition definition) { + for (EventMessage eventMessage : definition.getEventMessages()) { + String queueName = amazonSQSProducerManager.getQueueName(definition, eventMessage); + String message = renderEventMessageContent(eventMessage); + amazonSQSProducerManager.publishMessage(queueName, message, amazonSQSProducerManager + .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders())); + } + } + + /** Take care publishing SNS mock messages for definition. */ + protected void produceSNSMockMessages(AsyncMockDefinition definition) { + for (EventMessage eventMessage : definition.getEventMessages()) { + String topicName = amazonSNSProducerManager.getTopicName(definition, eventMessage); + String message = renderEventMessageContent(eventMessage); + amazonSNSProducerManager.publishMessage(topicName, message, amazonSNSProducerManager + .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders())); + } + } + + /** + * Format the destination operation part by computing an address with optional dynamic parts. This doesn't include + * protocol specific sanitization (like replacing `/` with `-`, etc.) + * @param operation The operation to format a destination part for + * @param eventMessage The message to format a destination part for + * @return The operation part in a full destination name (usually service + version + operation) + */ + public static String getDestinationOperationPart(Operation operation, EventMessage eventMessage) { + // In AsyncAPI v2, channel address is directly the operation name. + String operationPart = removeActionInOperationName(operation.getName()); + + // Take care of templatized address for URI_PART dispatcher style. + if ("URI_PARTS".equals(operation.getDispatcher())) { + // In AsyncAPI v3, operation is different from channel and channel templatized address may be in resourcePaths. + for (String resourcePath : operation.getResourcePaths()) { + if (resourcePath.contains("{")) { + operationPart = resourcePath; + break; + } + } + operationPart = replacePartPlaceholders(operationPart, eventMessage); + } + return operationPart; + } + + /** Render event message content from definition applying template rendering if required. */ + private String renderEventMessageContent(EventMessage eventMessage) { + String content = eventMessage.getContent(); + if (content.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + logger.debug("EventMessage contains dynamic EL expression, rendering it..."); + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + + try { + content = engine.getValue(content); + } catch (Throwable t) { + logger.errorf("Failing at evaluating template '%s'", content, t); + } + } + return content; + } + + /** Remove the AsyncAPI action (or verb) at the beginning of operation name if present. */ + private static String removeActionInOperationName(String operationName) { + if (operationName.startsWith("SUBSCRIBE ") || operationName.startsWith("PUBLISH ") + || operationName.startsWith("SEND ") || operationName.startsWith("RECEIVE ")) { + return operationName.substring(operationName.indexOf(" ") + 1); + } + return operationName; + } + + /** Replace address placeholders ('{}') with their values coming from message dispatch criteria. */ + private static String replacePartPlaceholders(String address, EventMessage eventMessage) { + String partsCriteria = eventMessage.getDispatchCriteria(); + if (partsCriteria != null && !partsCriteria.isBlank()) { + String[] criterion = partsCriteria.split("/"); + for (String criteria : criterion) { + if (criteria != null && !criteria.isBlank()) { + String[] element = criteria.split("="); + String key = String.format("\\{%s\\}", element[0]); + address = address.replaceAll(key, URLEncoder.encode(element[1], Charset.defaultCharset())); + } + } + } + return address; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/RootWebSocketProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/RootWebSocketProducerManager.java new file mode 100644 index 000000000..83d1e8444 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/RootWebSocketProducerManager.java @@ -0,0 +1,36 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import jakarta.inject.Qualifier; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Retention(RUNTIME) +@Target({ METHOD, FIELD, PARAMETER, TYPE }) +/** + * Qualifier annotation for the root WebSocketProducerManager of the hierarchy of endpoint. + * @author laurent + */ +public @interface RootWebSocketProducerManager { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManager.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManager.java new file mode 100644 index 000000000..a85ea8798 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManager.java @@ -0,0 +1,138 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Header; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.minion.async.AsyncMockRepository; +import org.jboss.logging.Logger; + +import jakarta.inject.Inject; +import jakarta.websocket.CloseReason; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.PathParam; +import java.util.List; +import java.util.Set; + +/** + * WebSocket implementation of producer for async event messages. + * @author laurent + */ +public class WebSocketProducerManager { + + /** Get a JBoss logging logger. */ + private final Logger logger = Logger.getLogger(getClass()); + + @Inject + AsyncMockRepository asyncMockRepository; + + @Inject + WebSocketSessionRegistry sessionRegistry; + + public WebSocketProducerManager() { + } + + /** + * Publish a message on specified channel. + * @param channel The destination channel for message + * @param message The message payload + * @param headers A set of headers if any (maybe null or empty) + */ + public void publishMessage(String channel, String message, Set
headers) { + logger.infof("Publishing on channel {%s}, message: %s ", channel, message); + + List sessions = sessionRegistry.getSessions(channel); + if (sessions != null && !sessions.isEmpty()) { + logger.debugf("Sending message to %d WebSocket sessions", sessions.size()); + sessions.forEach(s -> { + s.getAsyncRemote().sendObject(message, result -> { + if (result.getException() != null) { + logger.error("Unable to send message: " + result.getException()); + } + }); + }); + } + } + + @OnOpen + public void onOpen(Session session, @PathParam("service") String service, @PathParam("version") String version) { + logger.infof("New WebSocket session opening on service {%s} - {%s}. Checking if mocked...", service, version); + + // If service or version were encoded with '+' instead of '%20', remove them. + if (service.contains("+")) { + service = service.replace('+', ' '); + } + if (version.contains("+")) { + version = version.replace('+', ' '); + } + + Set definitions = asyncMockRepository.getMockDefinitionsByServiceAndVersion(service, + version); + if (definitions != null && !definitions.isEmpty()) { + sessionRegistry.putSession(session); + } else { + try { + logger.infof("No mock available on '%s', closing the session", session.getRequestURI().toString()); + session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, + "No mock available on " + session.getRequestURI())); + } catch (Exception e) { + logger.infof("Caught an exception while rejecting a WebSocket opening on unmanaged '%'", + session.getRequestURI().toString()); + } + } + } + + @OnClose + public void onClose(Session session, @PathParam("service") String service, @PathParam("version") String version) { + sessionRegistry.removeSession(session); + } + + @OnError + public void onError(Session session, @PathParam("service") String service, @PathParam("version") String version, + Throwable throwable) { + sessionRegistry.removeSession(session); + } + + @OnMessage + public void onMessage(String message, @PathParam("service") String service, @PathParam("version") String version) { + // Nothing to do here. + logger.debug("Received a message on WebSocketProducerManager, nothing to do..."); + } + + /** + * Get the Websocket endpoint URI corresponding to a AsyncMockDefinition, sanitizing all parameters. + * @param definition The AsyncMockDefinition + * @param eventMessage The message to get topic + * @return The request URI corresponding to def and message + */ + public String getRequestURI(AsyncMockDefinition definition, EventMessage eventMessage) { + // Produce service name part of topic name. + String serviceName = definition.getOwnerService().getName().replace(" ", "+"); + + // Produce version name part of topic name. + String versionName = definition.getOwnerService().getVersion().replace(" ", "+"); + + // Produce operation name part of topic name. + String operationName = ProducerManager.getDestinationOperationPart(definition.getOperation(), eventMessage); + + return "/api/ws/" + serviceName + "/" + versionName + "/" + operationName; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerRoot.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerRoot.java new file mode 100644 index 000000000..97ab3a3a9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerRoot.java @@ -0,0 +1,29 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.server.ServerEndpoint; + +@ApplicationScoped +@RootWebSocketProducerManager +@ServerEndpoint("/api/ws/{service}/{version}/") +/** + * WebSocket endpoint with no sub part in channel name. + * @author laurent + */ +public class WebSocketProducerManagerRoot extends WebSocketProducerManager { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub.java new file mode 100644 index 000000000..acd94bf14 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.quarkus.arc.Unremovable; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.server.ServerEndpoint; + +@Unremovable +@ApplicationScoped +@ServerEndpoint("/api/ws/{service}/{version}/{path}") +/** + * WebSocket endpoint with 1 sub part in channel name. + * @author laurent + */ +public class WebSocketProducerManagerSub extends WebSocketProducerManager { + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub2.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub2.java new file mode 100644 index 000000000..b895b868e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub2.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.quarkus.arc.Unremovable; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.server.ServerEndpoint; + +/** + * WebSocket endpoint with 2 sub parts in channel name. + * @author laurent + */ +@Unremovable +@ApplicationScoped +@ServerEndpoint("/api/ws/{service}/{version}/{path}/{sub}") +public class WebSocketProducerManagerSub2 extends WebSocketProducerManager { + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub3.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub3.java new file mode 100644 index 000000000..0d6465358 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub3.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.quarkus.arc.Unremovable; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.server.ServerEndpoint; + +/** + * WebSocket endpoint with 3 sub parts in channel name. + * @author laurent + */ +@Unremovable +@ApplicationScoped +@ServerEndpoint("/api/ws/{service}/{version}/{path}/{sub}/{sub2}") +public class WebSocketProducerManagerSub3 extends WebSocketProducerManager { + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub4.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub4.java new file mode 100644 index 000000000..79a0cb7df --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub4.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.quarkus.arc.Unremovable; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.server.ServerEndpoint; + +/** + * WebSocket endpoint with 4 sub parts in channel name. + * @author laurent + */ +@Unremovable +@ApplicationScoped +@ServerEndpoint("/api/ws/{service}/{version}/{path}/{sub}/{sub2}/{sub3}") +public class WebSocketProducerManagerSub4 extends WebSocketProducerManager { + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub5.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub5.java new file mode 100644 index 000000000..a8b283a5d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub5.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.quarkus.arc.Unremovable; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.server.ServerEndpoint; + +/** + * WebSocket endpoint with 5 sub parts in channel name. + * @author laurent + */ +@Unremovable +@ApplicationScoped +@ServerEndpoint("/api/ws/{service}/{version}/{path}/{sub}/{sub2}/{sub3}/{sub4}") +public class WebSocketProducerManagerSub5 extends WebSocketProducerManager { + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub6.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub6.java new file mode 100644 index 000000000..a4bf183b7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketProducerManagerSub6.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.quarkus.arc.Unremovable; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.server.ServerEndpoint; + +/** + * WebSocket endpoint with 6 sub parts in channel name. + * @author laurent + */ +@Unremovable +@ApplicationScoped +@ServerEndpoint("/api/ws/{service}/{version}/{path}/{sub}/{sub2}/{sub3}/{sub4}/{sub6}") +public class WebSocketProducerManagerSub6 extends WebSocketProducerManager { + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketSessionRegistry.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketSessionRegistry.java new file mode 100644 index 000000000..aaafc0a5e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/java/io/github/microcks/minion/async/producer/WebSocketSessionRegistry.java @@ -0,0 +1,71 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.Session; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Simple registry for holding WebSocket sessions based on the requested channel path. This registry is intended to be + * shared by the different WebSocket endpoints that allows client registration. Root producer will then use the registry + * to select target sessions when a mock get published on a channel path. + * @author laurent + */ +@ApplicationScoped +public class WebSocketSessionRegistry { + + private final Map> sessions = new ConcurrentHashMap<>(); + + /** + * Store a session within registry according the requested URI. + * @param session A WebSocket session + */ + public void putSession(Session session) { + // As session.getRequestURI() doubles query parameters but session.getQueryString() not, + // we need to build the channelURI manually. + String channelURI = session.getRequestURI().getPath(); + if (session.getQueryString() != null) { + channelURI += "?" + session.getQueryString(); + } + + List channelSessions = sessions.computeIfAbsent(channelURI, k -> new ArrayList<>()); + channelSessions.add(session); + } + + /** + * Get the sessions connected to a request URI. + * @param requestURI The URI to get sessions for. + * @return A List of WebSocket sessions. + */ + public List getSessions(String requestURI) { + return sessions.get(requestURI); + } + + /** + * Remove a session from registry (typically when closed or errored). + * @param session The WebSocket session to remote + */ + public void removeSession(Session session) { + List channelSessions = sessions.get(session.getRequestURI().toString()); + if (channelSessions != null) { + channelSessions.remove(session); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/resources/META-INF/native-image/io.github.microcks/microcks-async-minion/native-image.properties b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/resources/META-INF/native-image/io.github.microcks/microcks-async-minion/native-image.properties new file mode 100644 index 000000000..57b65be06 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/resources/META-INF/native-image/io.github.microcks/microcks-async-minion/native-image.properties @@ -0,0 +1 @@ +JavaArgs=--add-opens java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.x509=ALL-UNNAMED \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/resources/application.properties b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/resources/application.properties new file mode 100644 index 000000000..40af3169d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/main/resources/application.properties @@ -0,0 +1,81 @@ +# Configuration file. +quarkus.http.port=8081 + +# Configure the log level. +quarkus.log.level=INFO +quarkus.log.console.level=INFO + +# Configure kafka integration into health probe. +quarkus.kafka.health.enabled=true + +# Disable Apicurio Registry devservices. +quarkus.apicurio-registry.devservices.enabled=false + +# Disable AWS SQS & SNS devservices. +quarkus.sqs.devservices.enabled=false +quarkus.sns.devservices.enabled=false + +# Access to Microcks API server. +io.github.microcks.minion.async.client.MicrocksAPIConnector/mp-rest/url=http://localhost:8080 +microcks.serviceaccount=microcks-serviceaccount +microcks.serviceaccount.credentials=ab54d329-e435-41ae-a900-ec6b3fe15c54 + +# Access to Keycloak URL if you override the one coming from Microcks config +#keycloak.auth.url=http://localhost:8180/auth + +# Access to Kafka broker. +kafka.bootstrap.servers=localhost:9092 +# For Apicurio registry +#kafka.schema.registry.url=http://localhost:8888 +#kafka.schema.registry.confluent=false +# For Confluent registry +#kafka.schema.registry.url=http://localhost:8889 +kafka.schema.registry.confluent=true +kafka.schema.registry.username= +kafka.schema.registry.credentials.source=USER_INFO + +mp.messaging.incoming.microcks-services-updates.connector=smallrye-kafka +mp.messaging.incoming.microcks-services-updates.topic=microcks-services-updates +mp.messaging.incoming.microcks-services-updates.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer +mp.messaging.incoming.microcks-services-updates.value.deserializer=io.github.microcks.minion.async.client.ServiceViewChangeEventDeserializer +# Do not save any consumer-offset on the broker as there's a re-sync on each minion startup. +mp.messaging.incoming.microcks-services-updates.enable.auto.commit=false +mp.messaging.incoming.microcks-services-updates.bootstrap.servers=localhost:9092 + +# Access to NATS broker. +nats.server=localhost:4222 +nats.username=microcks +nats.password=microcks + +# Access to MQTT broker. +mqtt.server=localhost:1883 +mqtt.username=microcks +mqtt.password=microcks + +# Access to RabbitMQ broker. +amqp.server=localhost:5672 +amqp.username=microcks +amqp.password=microcks + +# Access to Google PubSub. +googlepubsub.project=my-project +googlepubsub.service-account-location=/deployments/config/googlecloud-service-account.json + +# Access to Amazon SQS +amazonsqs.region=eu-west-3 +amazonsqs.credentials-type=env-variable +amazonsqs.credentials-profile-name=microcks-sqs-admin +amazonsqs.credentials-profile-location=/deployments/config/amazon-sqs/aws.profile +#amazonsqs.endpoint-override=http://localhost:4566 + +# Access to Amazon SNS +amazonsns.region=eu-west-3 +amazonsns.credentials-type=env-variable +amazonsns.credentials-profile-name=microcks-sns-admin +amazonsns.credentials-profile-location=/deployments/config/amazon-sns/aws.profile +#amazonsns.endpoint-override=http://localhost:4566 + +# Configure the minion own behavioral properties. +minion.supported-bindings=KAFKA,WS +minion.restricted-frequencies=3,10,30 +minion.default-avro-encoding=RAW diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/AsyncAPITestManagerIT.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/AsyncAPITestManagerIT.java new file mode 100644 index 000000000..08e61506f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/AsyncAPITestManagerIT.java @@ -0,0 +1,180 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceView; +import io.github.microcks.domain.TestCaseResult; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.domain.TestRunnerType; +import io.github.microcks.minion.async.client.KeycloakConfig; +import io.github.microcks.minion.async.client.MicrocksAPIConnector; +import io.github.microcks.minion.async.client.dto.TestCaseReturnDTO; + +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringSerializer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +/** + * This is an integration test case using Testcontainers to test + * {@link AsyncAPITestManager} class. + * @author laurent + */ +@Testcontainers +class AsyncAPITestManagerIT { + + private static final String TOPIC_NAME = "test-topic"; + + private static Stream testConfigs() { + return Stream.of(new TestConfig("user-signedup-asyncapi-3.0.yaml", "SEND publishUserSignedUps", """ + { + "fullName": "Laurent Broudoux", + "email": "laurent@microcks.io", + "age": 4%s + } + """), new TestConfig("send-message-asyncapi-3.0.yaml", "SEND sendEchoMessage", "test")); + } + + private static final Network NETWORK = Network.newNetwork(); + + @Container + private static final KafkaContainer kafkaContainer = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:7.5.0")).withNetwork(NETWORK).withNetworkAliases("kafka") + .withListener(() -> "kafka:19092"); + + @ParameterizedTest + @MethodSource("testConfigs") + void testKafkaAsyncAPITestSuccess(TestConfig config) throws Exception { + // Arrange. + String asyncAPIContent = Files + .readString(Paths.get("target/test-classes/io/github/microcks/minion/async", config.specificationFile)); + Map reportedTestCases = new HashMap<>(); + MicrocksAPIConnector microcksAPIConnector = new MicrocksAPIConnector() { + @Override + public KeycloakConfig getKeycloakConfig() { + return null; + } + + @Override + public List listServices(String authorization, int page, int size) { + return null; + } + + @Override + public ServiceView getService(String authorization, String serviceId, boolean messages) { + return null; + } + + @Override + public List getResources(String serviceId) { + if ("d3d5a3ed-13bf-493f-a06d-bf93392f420b".equals(serviceId)) { + Resource resource = new Resource(); + resource.setId("578497d2-2695-4aff-b7c6-ff7b9a373aa9"); + resource.setType(ResourceType.ASYNC_API_SPEC); + resource.setContent(asyncAPIContent); + return List.of(resource); + } + return Collections.emptyList(); + } + + @Override + public TestCaseResult reportTestCaseResult(String testResultId, TestCaseReturnDTO testCaseReturn) { + reportedTestCases.put(testResultId, testCaseReturn); + return null; + } + }; + SchemaRegistry schemaRegistry = new SchemaRegistry(microcksAPIConnector); + schemaRegistry.updateRegistryForService("d3d5a3ed-13bf-493f-a06d-bf93392f420b"); + + AsyncTestSpecification testSpecification = new AsyncTestSpecification(); + testSpecification.setServiceId("d3d5a3ed-13bf-493f-a06d-bf93392f420b"); + testSpecification.setEndpointUrl( + "kafka://%s/%s".formatted(kafkaContainer.getBootstrapServers().replace("PLAINTEXT://", ""), TOPIC_NAME)); + testSpecification.setRunnerType(TestRunnerType.ASYNC_API_SCHEMA); + testSpecification.setAsyncAPISpec(asyncAPIContent); + testSpecification.setOperationName(config.operationName); + testSpecification.setTimeoutMS(2200L); + testSpecification.setTestResultId("test-result-id"); + + AsyncAPITestManager manager = new AsyncAPITestManager(microcksAPIConnector, schemaRegistry); + manager.microcksUrl = "http://microcks:8080"; + + // Act. + manager.launchTest(testSpecification); + + // Wait a bit so that consumption task has actually started. + await().during(1250, TimeUnit.MILLISECONDS).until(() -> true); + sendTextMessagesOnTopic(5, config.message); + + // Wait a bit so that consumption task has actually finished. + await().during(3, TimeUnit.SECONDS).until(() -> true); + + // Assert. + Assertions.assertEquals(1, reportedTestCases.size()); + Assertions.assertTrue(reportedTestCases.containsKey("test-result-id")); + TestCaseReturnDTO testCaseReturn = reportedTestCases.get("test-result-id"); + Assertions.assertEquals(5, testCaseReturn.getTestReturns().size()); + for (TestReturn testReturn : testCaseReturn.getTestReturns()) { + Assertions.assertEquals(TestReturn.SUCCESS_CODE, testReturn.getCode()); + } + } + + private static void sendTextMessagesOnTopic(int number, String message) { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + props.put(ProducerConfig.CLIENT_ID_CONFIG, "microcks-async-minion-str-producer"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + + KafkaProducer producer = new KafkaProducer<>(props); + + for (int i = 0; i < number; i++) { + ProducerRecord kafkaRecord = new ProducerRecord<>(TOPIC_NAME, + String.valueOf(System.currentTimeMillis()), message.formatted(i)); + producer.send(kafkaRecord); + } + producer.flush(); + producer.close(); + } + + // record to group the test config + record TestConfig(String specificationFile, String operationName, String message) { + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/AsyncAPITestManagerTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/AsyncAPITestManagerTest.java new file mode 100644 index 000000000..78e153e8d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/AsyncAPITestManagerTest.java @@ -0,0 +1,89 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async; + + +import com.fasterxml.jackson.databind.JsonNode; +import io.github.microcks.util.asyncapi.AsyncAPISchemaUtil; +import io.github.microcks.util.asyncapi.AsyncAPISchemaValidator; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +@Slf4j +public class AsyncAPITestManagerTest { + + private static final String ASYNC_API_2_SCHEMA_TEXT1 = """ + asyncapi: '2.0.0' + info: + title: Example + version: '1.0.0' + description: Example YAML + channels: + sendMessage: + description: Example YAML + publish: + summary: Example YAML + operationId: sendMessage + message: + $ref: '#/components/messages/send' + components: + messages: + send: + summary: Example message + contentType: text/plain + """; + + @Test + void testGetExpectedContentType() { + JsonNode specificationNode = null; + try { + specificationNode = AsyncAPISchemaValidator.getJsonNodeForSchema(ASYNC_API_2_SCHEMA_TEXT1); + } catch (IOException e) { + Assertions.fail("Exception should not be thrown"); + } + String messagePathPointer = AsyncAPISchemaUtil.findMessagePathPointer(specificationNode, "SEND sendMessage"); + Assertions.assertNotNull(messagePathPointer); + Assertions.assertTrue(messagePathPointer.contains("/channels/sendMessage/publish/message")); + String expectedContentType = getExpectedContentType(specificationNode, messagePathPointer); + Assertions.assertTrue(expectedContentType.contains("text/plain")); + } + + /** Retrieve the expected content type for an AsyncAPI message. */ + private String getExpectedContentType(JsonNode specificationNode, String messagePathPointer) { + // Retrieve default content type, defaulting to application/json. + String defaultContentType = specificationNode.path("defaultContentType").asText("application/json"); + // Get message real content type if defined. + String contentType = defaultContentType; + JsonNode messageNode = specificationNode.at(messagePathPointer); + // messageNode will be an array of messages + if (messageNode.isArray() && messageNode.size() > 0) { + messageNode = messageNode.get(0); + } + // If it's a $ref, then navigate to it. + while (messageNode.has("$ref")) { + // $ref: '#/components/messages/lightMeasured' + String ref = messageNode.path("$ref").asText(); + messageNode = specificationNode.at(ref.substring(1)); + } + if (messageNode.has("contentType")) { + contentType = messageNode.path("contentType").asText(); + } + return contentType; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTaskIT.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTaskIT.java new file mode 100644 index 000000000..50ce2d754 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTaskIT.java @@ -0,0 +1,281 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import com.rabbitmq.client.*; +import io.github.microcks.domain.Secret; +import io.github.microcks.minion.async.AsyncTestSpecification; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +/** + * This is an integration test case using Testcontainers to test + * {@link AMQPMessageConsumptionTask} class. + * @author mcruzdev + */ +@Testcontainers +class AMQPMessageConsumptionTaskIT { + + private static final String RABBIT_MQ_PORT = "5672"; + private static final String QUEUE_NAME = "logs"; + private static final String EXCHANGE_DIRECT_NAME = "direct_logs"; + private static final String RABBIT_MQ_3_7_25 = "rabbitmq:3.7.25-management-alpine"; + + @Container + private static final RabbitMQContainer rabbitMQContainer = new RabbitMQContainer(); + + static class RabbitMQContainer extends org.testcontainers.containers.RabbitMQContainer { + public RabbitMQContainer() { + super(DockerImageName.parse(RABBIT_MQ_3_7_25)); + this.setPortBindings(List.of("%s:%s".formatted(RABBIT_MQ_PORT, RABBIT_MQ_PORT))); + } + } + + @Test + void shouldReceiveMessageOnQueueCorrectly() throws Exception { + // arrange + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(2000L); + asyncTestSpecification.setEndpointUrl("amqp://localhost:%s/q/%s".formatted(RABBIT_MQ_PORT, QUEUE_NAME)); + + AMQPMessageConsumptionTask amqpMessageConsumptionTask = new AMQPMessageConsumptionTask(asyncTestSpecification); + + sendMessageIntoQueue(); + + // act + List messages = amqpMessageConsumptionTask.call(); + + // assert + Assertions.assertFalse(messages.isEmpty()); + } + + @Test + void shouldReceiveMessageOnExchangeWithDirectTypeCorrectly() throws Exception { + // arrange + long secondsToWait = Duration.ofSeconds(3).toMillis(); + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(secondsToWait); + asyncTestSpecification.setEndpointUrl( + "amqp://localhost:%s/d/%s?routingKey=info&durable=true".formatted(RABBIT_MQ_PORT, EXCHANGE_DIRECT_NAME)); + AMQPMessageConsumptionTask amqpMessageConsumptionTask = new AMQPMessageConsumptionTask(asyncTestSpecification); + + // We need to wait the existence of binding from AMQPMessageConsumptionTask's side + new Thread(() -> { + try { + Thread.sleep(secondsToWait); + sendMessageIntoExchange(EXCHANGE_DIRECT_NAME, "direct", "info", true, null); + } catch (IOException | TimeoutException | InterruptedException e) { + throw new RuntimeException("Error while sending message to queue", e); + } + }).start(); + + // act + List messages = amqpMessageConsumptionTask.call(); + + // assert + Assertions.assertFalse(messages.isEmpty()); + } + + @Test + void shouldReceiveMessageOnExchangeWithTopicTypeCorrectly() throws Exception { + // arrange + String exchangeName = "topic_logs"; + long secondsToWait = Duration.ofSeconds(3).toMillis(); + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(secondsToWait + 1000); + asyncTestSpecification + .setEndpointUrl("amqp://localhost:%s/t/%s?durable=false".formatted(RABBIT_MQ_PORT, exchangeName)); + AMQPMessageConsumptionTask amqpMessageConsumptionTask = new AMQPMessageConsumptionTask(asyncTestSpecification); + + // We need to wait the existence of binding from AMQPMessageConsumptionTask's side + new Thread(() -> { + try { + Thread.sleep(secondsToWait); + sendMessageIntoExchange(exchangeName, "topic", "", false, null); + } catch (IOException | TimeoutException | InterruptedException e) { + throw new RuntimeException("Error while sending message to queue", e); + } + }).start(); + + // act + List messages = amqpMessageConsumptionTask.call(); + + // assert + Assertions.assertFalse(messages.isEmpty()); + } + + @Test + void shouldReceiveMessageOnExchangeWithHeadersTypeCorrectly() throws Exception { + // arrange + String exchangeName = "headers_logs"; + long secondsToWait = Duration.ofSeconds(3).toMillis(); + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(secondsToWait + 1000); + asyncTestSpecification + .setEndpointUrl("amqp://localhost:%s/h/%s?h.severity=info".formatted(RABBIT_MQ_PORT, exchangeName)); + AMQPMessageConsumptionTask amqpMessageConsumptionTask = new AMQPMessageConsumptionTask(asyncTestSpecification); + + // We need to wait the existence of binding from AMQPMessageConsumptionTask's side + new Thread(() -> { + try { + Thread.sleep(secondsToWait); + Map props = new HashMap<>(); + props.put("severity", "info"); + // headers.put("x-match", "any"); + System.out.println("Sending ..."); + sendMessageIntoExchange(exchangeName, "headers", "", false, props); + } catch (IOException | TimeoutException | InterruptedException e) { + throw new RuntimeException("Error while sending message to queue", e); + } + }).start(); + + // act + List messages = amqpMessageConsumptionTask.call(); + + // assert + Assertions.assertFalse(messages.isEmpty()); + } + + @Test + void shouldReceiveMessageOnExchangeWithHeadersWithoutItemsTypeCorrectly() throws Exception { + // arrange + String exchangeName = "headers_empty_logs"; + long secondsToWait = Duration.ofSeconds(3).toMillis(); + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(secondsToWait + 1000); + asyncTestSpecification.setEndpointUrl("amqp://localhost:%s/h/%s".formatted(RABBIT_MQ_PORT, exchangeName)); + AMQPMessageConsumptionTask amqpMessageConsumptionTask = new AMQPMessageConsumptionTask(asyncTestSpecification); + + // We need to wait the existence of binding from AMQPMessageConsumptionTask's side + new Thread(() -> { + try { + Thread.sleep(secondsToWait); + Map headers = new HashMap<>(); + System.out.println("Sending ..."); + sendMessageIntoExchange(exchangeName, "headers", "", false, headers); + } catch (IOException | TimeoutException | InterruptedException e) { + throw new RuntimeException("Error while sending message to queue", e); + } + }).start(); + + // act + List messages = amqpMessageConsumptionTask.call(); + + // assert + Assertions.assertFalse(messages.isEmpty()); + } + + @Test + void shouldReceiveMessageOnExchangeWithFanoutTypeCorrectly() throws Exception { + // arrange + String exchangeName = "fanout_logs"; + long secondsToWait = Duration.ofSeconds(3).toMillis(); + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(secondsToWait + 1000); + asyncTestSpecification.setEndpointUrl("amqp://localhost:%s/f/%s".formatted(RABBIT_MQ_PORT, exchangeName)); + AMQPMessageConsumptionTask amqpMessageConsumptionTask = new AMQPMessageConsumptionTask(asyncTestSpecification); + + // We need to wait the existence of binding from AMQPMessageConsumptionTask's side + new Thread(() -> { + try { + Thread.sleep(secondsToWait); + System.out.println("Sending ..."); + sendMessageIntoExchange(exchangeName, "fanout", "", false, null); + } catch (IOException | TimeoutException | InterruptedException e) { + throw new RuntimeException("Error while sending message to queue", e); + } + }).start(); + + // act + List messages = amqpMessageConsumptionTask.call(); + + // assert + Assertions.assertFalse(messages.isEmpty()); + } + + @Test + void shouldReceiveMessageOnExchangeWithSecretsCorrectly() throws Exception { + // arrange + String exchangeName = "fanout_secrets_logs"; + long secondsToWait = Duration.ofSeconds(3).toMillis(); + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(secondsToWait + 1000); + asyncTestSpecification.setEndpointUrl("amqp://localhost:%s/f/%s".formatted(RABBIT_MQ_PORT, exchangeName)); + Secret secret = new Secret(); + secret.setUsername("guest"); + secret.setPassword("guest"); + asyncTestSpecification.setSecret(secret); + AMQPMessageConsumptionTask amqpMessageConsumptionTask = new AMQPMessageConsumptionTask(asyncTestSpecification); + + // We need to wait the existence of binding from AMQPMessageConsumptionTask's side + new Thread(() -> { + try { + Thread.sleep(secondsToWait); + System.out.println("Sending ..."); + sendMessageIntoExchange(exchangeName, "fanout", "", false, null); + } catch (IOException | TimeoutException | InterruptedException e) { + throw new RuntimeException("Error while sending message to queue", e); + } + }).start(); + + // act + List messages = amqpMessageConsumptionTask.call(); + + // assert + Assertions.assertFalse(messages.isEmpty()); + } + + private static void sendMessageIntoQueue() throws IOException, TimeoutException { + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + try (Connection connection = connectionFactory.newConnection()) { + Channel channel = connection.createChannel(); + channel.queueDeclare(QUEUE_NAME, true, false, false, null); + String message = "[INFO] Hello from Microcks"; + channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, + message.getBytes(StandardCharsets.UTF_8)); + } + } + + private static void sendMessageIntoExchange(String exchangeName, String type, String routingKey, boolean durable, + Map headers) throws IOException, TimeoutException { + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + try (Connection connection = connectionFactory.newConnection()) { + Channel channel = connection.createChannel(); + channel.exchangeDeclare(exchangeName, type, durable); + String message = "[INFO] Hello from Microcks using exchange"; + + AMQP.BasicProperties props = null; + if (headers != null) { + props = new AMQP.BasicProperties.Builder().headers(headers).build(); + } + + channel.basicPublish(exchangeName, routingKey, props, message.getBytes(StandardCharsets.UTF_8)); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTaskTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTaskTest.java new file mode 100644 index 000000000..a6dbaf762 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/AMQPMessageConsumptionTaskTest.java @@ -0,0 +1,137 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for AMQPMessageConsumptionTask. + * @author laurent + */ +public class AMQPMessageConsumptionTaskTest { + + @Test + public void testAcceptEndpoint() { + + assertTrue(AMQPMessageConsumptionTask.acceptEndpoint("amqp://localhost/q/testQueue")); + + assertTrue(AMQPMessageConsumptionTask.acceptEndpoint("amqp://localhost/vHost/q/testQueue")); + + assertTrue(AMQPMessageConsumptionTask.acceptEndpoint("amqp://localhost:5671/q/testQueue")); + + assertTrue(AMQPMessageConsumptionTask.acceptEndpoint("amqp://localhost:5671/vHost/q/testQueue")); + + assertTrue(AMQPMessageConsumptionTask.acceptEndpoint("amqp://localhost:5671/q/testQueue/with/path/elements")); + + assertTrue( + AMQPMessageConsumptionTask.acceptEndpoint("amqp://localhost:5671/vHost/q/testQueue/with/path/elements")); + + assertTrue(AMQPMessageConsumptionTask.acceptEndpoint("amqp://localhost:5671/f/testExchange?durable=true")); + + assertTrue(AMQPMessageConsumptionTask + .acceptEndpoint("amqp://localhost:5671/t/testExchange?durable=true&routingKey=samples.*")); + + assertTrue(AMQPMessageConsumptionTask + .acceptEndpoint("amqp://localhost:5671/vHost/t/testExchange?durable=true&routingKey=samples.*")); + } + + @Test + public void testAcceptEndpointFailures() { + + assertFalse(AMQPMessageConsumptionTask.acceptEndpoint("amqp://localhost/x/testQueue")); + + assertFalse(AMQPMessageConsumptionTask.acceptEndpoint("rabbit://localhost/x/testQueue")); + } + + //@Test + public void testConsumptionFromQueue() { + try { + ConnectionFactory factory = new ConnectionFactory(); + factory.setUri("amqp://localhost"); + factory.setUsername("microcks"); + factory.setPassword("microcks"); + + Connection connection = factory.newConnection(); + Channel channel = connection.createChannel(); + + String queueName = "my-queue"; + + String consumerTag = channel.basicConsume(queueName, false, new DefaultConsumer(channel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, + byte[] body) throws IOException { + System.err.println("Received a new AMQP Message: " + new String(body)); + channel.basicAck(envelope.getDeliveryTag(), false); + } + }); + + Thread.sleep(10000L); + + channel.basicCancel(consumerTag); + channel.close(); + connection.close(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + //@Test + public void testConsumptionFromExchange() { + try { + ConnectionFactory factory = new ConnectionFactory(); + factory.setUri("amqp://localhost"); + factory.setUsername("microcks"); + factory.setPassword("microcks"); + + Connection connection = factory.newConnection(); + Channel channel = connection.createChannel(); + + String exchangeName = "my-exchange"; + + channel.exchangeDeclare(exchangeName, "fanout", true); + String queueName = channel.queueDeclare().getQueue(); + channel.queueBind(queueName, exchangeName, ""); + + String consumerTag = channel.basicConsume(queueName, false, new DefaultConsumer(channel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, + byte[] body) throws IOException { + System.err.println("Received a new AMQP Message: " + new String(body)); + channel.basicAck(envelope.getDeliveryTag(), false); + } + }); + + Thread.sleep(10000L); + + channel.basicCancel(consumerTag); + channel.close(); + connection.close(); + } catch (Throwable t) { + t.printStackTrace(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/GooglePubSubMessageConsumptionTaskTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/GooglePubSubMessageConsumptionTaskTest.java new file mode 100644 index 000000000..c8878fb09 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/GooglePubSubMessageConsumptionTaskTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for GooglePubSubMessageConsumptionTask. + * @author laurent + */ +class GooglePubSubMessageConsumptionTaskTest { + + @Test + void testAcceptEndpoint() { + + assertTrue(GooglePubSubMessageConsumptionTask.acceptEndpoint("googlepubsub://my-own-project-id/my-topic")); + } + + @Test + void testAcceptEndpointFailures() { + + assertFalse(GooglePubSubMessageConsumptionTask.acceptEndpoint("googlepubsub:///my-own-project-id")); + + assertFalse(GooglePubSubMessageConsumptionTask.acceptEndpoint("googlepubsub:///my-own-project-id/my/topic/name")); + + assertFalse(GooglePubSubMessageConsumptionTask.acceptEndpoint("rabbit://localhost/x/testChannel")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTaskIT.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTaskIT.java new file mode 100644 index 000000000..6d911e11c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTaskIT.java @@ -0,0 +1,234 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.minion.async.AsyncTestSpecification; +import io.github.microcks.util.AvroUtil; + +import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; +import io.confluent.kafka.serializers.KafkaAvroSerializer; +import org.apache.avro.generic.GenericRecord; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.TopicCollection; +import org.apache.kafka.common.serialization.StringSerializer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +/** + * This is an integration test case using Testcontainers to test + * {@link KafkaMessageConsumptionTask} class. + * @author laurent + */ +@Testcontainers +class KafkaMessageConsumptionTaskIT { + + private static final String TOPIC_NAME = "test-topic"; + private static final String TEXT_MESSAGE_TEMPLATE = "{\"greeting\": \"Hello World!\", \"number\": %s}"; + private static final String AVRO_SCHEMA = """ + { + "namespace": "microcks.avro", + "type": "record", + "name": "Message", + "fields": [ + {"name": "greeting", "type": "string"}, + {"name": "number", "type": "int"} + ] + } + """; + + private static final Network NETWORK = Network.newNetwork(); + + @Container + private static final KafkaContainer kafkaContainer = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:7.5.0")).withNetwork(NETWORK).withNetworkAliases("kafka") + .withListener(() -> "kafka:19092"); + + @BeforeEach + void beforeEach() { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + + try (AdminClient adminClient = AdminClient.create(props)) { + // Delete test-topic topic + adminClient.deleteTopics(TopicCollection.ofTopicNames(List.of(TOPIC_NAME))); + } + } + + @Test + void testReceiveTextMessageOnTopicCorrectly() throws Exception { + // Arrange. + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(1000L); + asyncTestSpecification.setEndpointUrl( + "kafka://%s/%s".formatted(kafkaContainer.getBootstrapServers().replace("PLAINTEXT://", ""), TOPIC_NAME)); + + KafkaMessageConsumptionTask kafkaMessageConsumptionTask = new KafkaMessageConsumptionTask(asyncTestSpecification); + + // Act. + ExecutorService executorService = Executors.newFixedThreadPool(2); + List>> outputs = executorService + .invokeAll(List.of(new Callable>() { + @Override + public List call() throws Exception { + // Wait a bit so that consumption task has actually start. + await().during(600, TimeUnit.MILLISECONDS).until(() -> true); + sendTextMessagesOnTopic(1); + return Collections.emptyList(); + } + }, kafkaMessageConsumptionTask), asyncTestSpecification.getTimeoutMS() + 1000L, TimeUnit.MILLISECONDS); + + List messages = outputs.get(1).get(); + + // Assert. + Assertions.assertFalse(messages.isEmpty()); + Assertions.assertEquals(1, messages.size()); + ConsumedMessage message = messages.get(0); + Assertions.assertEquals(TEXT_MESSAGE_TEMPLATE.formatted(0), + new String(message.getPayload(), StandardCharsets.UTF_8)); + } + + @Test + void testReceiveTextMessageOnTopicWithOffsetsCorrectly() throws Exception { + // Arrange. + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(1000L); + asyncTestSpecification.setEndpointUrl("kafka://%s/%s?startOffset=0&endOffset=4" + .formatted(kafkaContainer.getBootstrapServers().replace("PLAINTEXT://", ""), TOPIC_NAME)); + + KafkaMessageConsumptionTask kafkaMessageConsumptionTask = new KafkaMessageConsumptionTask(asyncTestSpecification); + + // Send 10 messages. + sendTextMessagesOnTopic(10); + + // Act. + List messages = kafkaMessageConsumptionTask.call(); + + // Assert. + Assertions.assertFalse(messages.isEmpty()); + Assertions.assertEquals(5, messages.size()); + for (int i = 0; i < 5; i++) { + ConsumedMessage message = messages.get(i); + Assertions.assertEquals(TEXT_MESSAGE_TEMPLATE.formatted(i), + new String(message.getPayload(), StandardCharsets.UTF_8)); + } + } + + @Test + void testReceiveAvroMessageOnTopicWithRegistry() throws Exception { + // Arrange + GenericContainer schemaRegistryContainer = new GenericContainer<>( + DockerImageName.parse("confluentinc/cp-schema-registry:7.5.0")).withNetwork(NETWORK).withExposedPorts(8889) + .withEnv("SCHEMA_REGISTRY_HOST_NAME", "schema-registry") + .withEnv("SCHEMA_REGISTRY_LISTENERS", "http://0.0.0.0:8889") + .withEnv("SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS", + //"PLAINTEXT://" + kafkaContainer.getNetworkAliases().get(0) + ":9092") + "PLAINTEXT://kafka:19092") + .waitingFor(Wait.forHttp("/subjects").forStatusCode(200)); + schemaRegistryContainer.start(); + + AsyncTestSpecification asyncTestSpecification = new AsyncTestSpecification(); + asyncTestSpecification.setTimeoutMS(1000L); + asyncTestSpecification.setEndpointUrl(("kafka://%s/%s?startOffset=0" + + "®istryUrl=http://localhost:%s®istryUsername=fred:letmein®istryAuthCredSource=USER_INFO") + .formatted(kafkaContainer.getBootstrapServers().replace("PLAINTEXT://", ""), TOPIC_NAME, + schemaRegistryContainer.getMappedPort(8889))); + + KafkaMessageConsumptionTask kafkaMessageConsumptionTask = new KafkaMessageConsumptionTask(asyncTestSpecification); + + // Send 1 Avro message. + sendAvroMessagesOnTopicWithRegistry(schemaRegistryContainer.getMappedPort(8889), 1); + + // Act. + List messages = kafkaMessageConsumptionTask.call(); + schemaRegistryContainer.stop(); + + // Assert. + Assertions.assertFalse(messages.isEmpty()); + Assertions.assertEquals(1, messages.size()); + ConsumedMessage message = messages.get(0); + + Assertions.assertNull(message.getPayload()); + Assertions.assertNotNull(message.getPayloadRecord()); + Assertions.assertEquals(TEXT_MESSAGE_TEMPLATE.formatted(0), message.getPayloadRecord().toString()); + } + + private static void sendTextMessagesOnTopic(int number) { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + props.put(ProducerConfig.CLIENT_ID_CONFIG, "microcks-async-minion-str-producer"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + + KafkaProducer producer = new KafkaProducer<>(props); + + for (int i = 0; i < number; i++) { + ProducerRecord kafkaRecord = new ProducerRecord<>(TOPIC_NAME, + String.valueOf(System.currentTimeMillis()), TEXT_MESSAGE_TEMPLATE.formatted(i)); + producer.send(kafkaRecord); + } + producer.flush(); + producer.close(); + } + + private static void sendAvroMessagesOnTopicWithRegistry(int schemaRegistryPort, int number) throws Exception { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + props.put(ProducerConfig.CLIENT_ID_CONFIG, "microcks-async-minion-registry-producer"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + + // Put Confluent Registry specific SerDes class and registry properties. + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KafkaAvroSerializer.class.getName()); + props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, + "http://localhost:%d".formatted(schemaRegistryPort)); + props.put(AbstractKafkaSchemaSerDeConfig.AUTO_REGISTER_SCHEMAS, true); + props.put(AbstractKafkaSchemaSerDeConfig.VALUE_SUBJECT_NAME_STRATEGY, + io.confluent.kafka.serializers.subject.TopicRecordNameStrategy.class.getName()); + + KafkaProducer producer = new KafkaProducer<>(props); + + for (int i = 0; i < number; i++) { + GenericRecord avroRecord = AvroUtil.jsonToAvroRecord(TEXT_MESSAGE_TEMPLATE.formatted(i), AVRO_SCHEMA); + ProducerRecord kafkaRecord = new ProducerRecord<>(TOPIC_NAME, + String.valueOf(System.currentTimeMillis()), avroRecord); + producer.send(kafkaRecord); + } + producer.flush(); + producer.close(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTaskTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTaskTest.java new file mode 100644 index 000000000..fad1fa3e1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/KafkaMessageConsumptionTaskTest.java @@ -0,0 +1,188 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.apicurio.registry.serde.SerdeConfig; +import io.apicurio.registry.serde.avro.AvroKafkaSerializer; +import io.confluent.kafka.serializers.AbstractKafkaAvroSerDeConfig; +import io.confluent.kafka.serializers.KafkaAvroDeserializer; +import io.github.microcks.minion.async.AsyncTestSpecification; + +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericRecord; +import org.apache.avro.generic.IndexedRecord; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.time.Duration; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for KafkaMessageConsumptionTask. + * @author laurent + */ +class KafkaMessageConsumptionTaskTest { + + @Test + void testAcceptEndpoint() { + AsyncTestSpecification specification = new AsyncTestSpecification(); + KafkaMessageConsumptionTask task = new KafkaMessageConsumptionTask(specification); + + assertTrue(KafkaMessageConsumptionTask.acceptEndpoint("kafka://localhost/testTopic")); + + assertTrue(KafkaMessageConsumptionTask.acceptEndpoint("kafka://localhost:9092/testTopic")); + + assertTrue( + KafkaMessageConsumptionTask.acceptEndpoint("kafka://localhost/testTopic?securityProtocol=SASL_PLAINTEXT")); + ; + + assertTrue(KafkaMessageConsumptionTask + .acceptEndpoint("kafka://localhost:9094/testTopic?securityProtocol=SASL_PLAINTEXT")); + + assertTrue(KafkaMessageConsumptionTask.acceptEndpoint( + "kafka://my-cluster-kafka-bootstrap-kafka.apps.cluster-943b.943b.example.com:443/UsersignedupAPI_0.1.2_user-signedup")); + } + + @Test + void testAcceptEndpointFailures() { + AsyncTestSpecification specification = new AsyncTestSpecification(); + KafkaMessageConsumptionTask task = new KafkaMessageConsumptionTask(specification); + + assertFalse(KafkaMessageConsumptionTask.acceptEndpoint("localhost:9092/testTopic")); + + assertFalse(KafkaMessageConsumptionTask.acceptEndpoint("ssl://localhost:9092/testTopic")); + + assertFalse(KafkaMessageConsumptionTask.acceptEndpoint("kafka://localhost")); + + assertFalse(KafkaMessageConsumptionTask.acceptEndpoint("kafka://localhost:port")); + + assertFalse(KafkaMessageConsumptionTask.acceptEndpoint("kafka://localhost:port/testTopic")); + } + + @Test + void testEndpointPattern() { + Matcher matcher = KafkaMessageConsumptionTask.ENDPOINT_PATTERN + .matcher("kafka://localhost:9092/UsersignedupAPI_0.1.2_user-signedup?registryUrl=http://localhost:8888"); + // Call matcher.find() to be able to use named expressions. + matcher.find(); + String endpointBrokerUrl = matcher.group("brokerUrl"); + String endpointTopic = matcher.group("topic"); + String options = matcher.group("options"); + + assertEquals("localhost:9092", endpointBrokerUrl); + assertEquals("UsersignedupAPI_0.1.2_user-signedup", endpointTopic); + assertEquals("registryUrl=http://localhost:8888", options); + } + + @Test + void testInitializeOptionsMap() { + AsyncTestSpecification specification = new AsyncTestSpecification(); + specification.setEndpointUrl( + "kafka://localhost/testTopic?registryUrl=http://localhost:8888®istryUsername=reg-user®istryAuthCredSource=USER_INFO&startOffset=100"); + String options = "registryUrl=http://localhost:8888®istryUsername=reg-user®istryAuthCredSource=USER_INFO&startOffset=100"; + + KafkaMessageConsumptionTask task = new KafkaMessageConsumptionTask(specification); + Map optionsMap = ConsumptionTaskCommons.initializeOptionsMap(options); + + assertNotNull(optionsMap); + assertEquals("http://localhost:8888", optionsMap.get(KafkaMessageConsumptionTask.REGISTRY_URL_OPTION)); + assertEquals("reg-user", optionsMap.get(KafkaMessageConsumptionTask.REGISTRY_USERNAME_OPTION)); + assertEquals("USER_INFO", optionsMap.get(KafkaMessageConsumptionTask.REGISTRY_AUTH_CREDENTIALS_SOURCE)); + assertEquals("100", optionsMap.get(KafkaMessageConsumptionTask.START_OFFSET)); + } + + /* + * @Test public void testApicurioSchemaRegistry() { try { Properties props = new Properties(); + * props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); props.put(ConsumerConfig.GROUP_ID_CONFIG, + * "microcks-async-minion-apicurio-consumer"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + * StringDeserializer.class.getName()); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + * AvroKafkaDeserializer.class.getName()); + * + * // Configure Schema registry access props.put(AbstractKafkaSerDe.REGISTRY_URL_CONFIG_PARAM, + * "http://localhost:8888/api"); props.put(AbstractKafkaSerDe.REGISTRY_CONFLUENT_ID_HANDLER_CONFIG_PARAM, true); + * + * KafkaConsumer consumer = new KafkaConsumer<>(props); + * consumer.subscribe(Arrays.asList("users")); + * + * // Start polling consumer for records. ConsumerRecords records = + * consumer.poll(Duration.ofMillis(10000)); + * + * Schema schema = new Schema.Parser() .parse(new + * File("target/test-classes/io/github/microcks/minion/async/format/user-signedup-bad.avsc")); Schema badSchema = new + * Schema.Parser() .parse(new File("target/test-classes/io/github/microcks/minion/async/format/user-signedup.avsc")); + * + * for (ConsumerRecord record : records) { System.err.println("Received: " + record.value()); + * + * System.err.println("Validation with correct schema: " + GenericData.get().validate(schema, record.value())); + * Iterator iterator = schema.getFields().iterator(); while (iterator.hasNext()) { Schema.Field f = + * (Schema.Field)iterator.next(); System.err.println(" Field: " + f.name() + " => " + + * GenericData.get().validate(f.schema(), record.value().get(f.name()))); } + * + * System.err.println("Validation with bad schema: " + GenericData.get().validate(badSchema, record.value())); + * iterator = badSchema.getFields().iterator(); while (iterator.hasNext()) { Schema.Field f = + * (Schema.Field)iterator.next(); System.err.println(" Field: " + f.name() + " => " + + * GenericData.get().validate(f.schema(), record.value().get(f.name()))); } } } catch (Throwable t) { + * t.printStackTrace(); } } + * + * @Test public void testConfluentSchemaRegistry() { try { Properties props = new Properties(); + * props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); props.put(ConsumerConfig.GROUP_ID_CONFIG, + * "microcks-async-minion-confluent-consumer"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + * StringDeserializer.class.getName()); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + * KafkaAvroDeserializer.class.getName()); + * + * // Configure Schema registry access //props.put(AbstractKafkaSerDe.REGISTRY_URL_CONFIG_PARAM, + * "http://localhost:8888"); //props.put(AbstractKafkaSerDe.REGISTRY_CONFLUENT_ID_HANDLER_CONFIG_PARAM, true); + * props.put(AbstractKafkaAvroSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, "http://localhost:8888"); + * + * KafkaConsumer consumer = new KafkaConsumer<>(props); + * consumer.subscribe(Arrays.asList("users")); + * + * // Start polling consumer for records. ConsumerRecords records = + * consumer.poll(Duration.ofMillis(10000)); + * + * Schema schema = new Schema.Parser() .parse(new + * File("target/test-classes/io/github/microcks/minion/async/format/user.avsc")); Schema badSchema = new + * Schema.Parser() .parse(new File("target/test-classes/io/github/microcks/minion/async/format/user-signedup.avsc")); + * + * for (ConsumerRecord record : records) { System.err.println("Received: " + record.value()); + * + * System.err.println("Validation with correct schema: " + GenericData.get().validate(schema, record.value())); + * Iterator iterator = schema.getFields().iterator(); while (iterator.hasNext()) { Schema.Field f = + * (Schema.Field)iterator.next(); System.err.println(" Field: " + f.name() + " => " + + * GenericData.get().validate(f.schema(), record.value().get(f.name()))); } + * + * System.err.println("Validation with bad schema: " + GenericData.get().validate(badSchema, record.value())); + * iterator = badSchema.getFields().iterator(); while (iterator.hasNext()) { Schema.Field f = + * (Schema.Field)iterator.next(); System.err.println(" Field: " + f.name() + " => " + + * GenericData.get().validate(f.schema(), record.value().get(f.name()))); } } } catch (Throwable t) { + * t.printStackTrace(); } } + */ +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/MQTTMessageConsumptionTaskTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/MQTTMessageConsumptionTaskTest.java new file mode 100644 index 000000000..65293e201 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/MQTTMessageConsumptionTaskTest.java @@ -0,0 +1,122 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.domain.Secret; +import io.github.microcks.minion.async.AsyncTestSpecification; + +import org.eclipse.paho.client.mqttv3.IMqttClient; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for MQTTMessageConsumptionTask. + * @author laurent + */ +public class MQTTMessageConsumptionTaskTest { + + @Test + public void testAcceptEndpoint() { + AsyncTestSpecification specification = new AsyncTestSpecification(); + MQTTMessageConsumptionTask task = new MQTTMessageConsumptionTask(specification); + + assertTrue(MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost/testTopic")); + + assertTrue(MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost:1883/testTopic")); + + assertTrue(MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost:1883/topic/with/path/elements")); + + assertTrue( + MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost:1883/topic/with/path/elements?option1=value1")); + + assertTrue(MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost/testTopic/option1=value1")); + + assertTrue(MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost:1883/testTopic/option1=value1")); + + assertTrue(MQTTMessageConsumptionTask.acceptEndpoint( + "mqtt://my-cluster-activemq.apps.cluster-943b.943b.example.com:1883/testTopic/option1=value1")); + } + + @Test + public void testAcceptEndpointFailures() { + AsyncTestSpecification specification = new AsyncTestSpecification(); + MQTTMessageConsumptionTask task = new MQTTMessageConsumptionTask(specification); + + assertFalse(MQTTMessageConsumptionTask.acceptEndpoint("localhost:1883/testTopic")); + + assertFalse(MQTTMessageConsumptionTask.acceptEndpoint("ssl://localhost:1883/testTopic")); + + assertFalse(MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost")); + + assertFalse(MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost:1883")); + + assertFalse(MQTTMessageConsumptionTask.acceptEndpoint("mqtt://localhost:port/testTopic")); + } + + /* + * @Test public void testTLSConnection() { try { MqttConnectOptions connectOptions = new MqttConnectOptions(); + * connectOptions.setAutomaticReconnect(false); connectOptions.setCleanSession(true); + * connectOptions.setConnectionTimeout(10); + * + * connectOptions.setUserName("admin"); connectOptions.setPassword("admin".toCharArray()); + * + * AsyncTestSpecification specification = new AsyncTestSpecification(); Secret secret = new Secret(); + * secret.setCaCertPem("-----BEGIN CERTIFICATE-----\n" + + * "MIIDTTCCAjWgAwIBAgIEZ3f4vzANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJG\n" + + * "UjEPMA0GA1UEBxMGTGVNYW5zMRMwEQYDVQQKEwpyZWRoYXQuY29tMQ4wDAYDVQQL\n" + + * "EwVTYWxlczESMBAGA1UEAxMJbGJyb3Vkb3V4MB4XDTIwMTIxNjA5MDQ1OFoXDTIx\n" + + * "MDMxNjA5MDQ1OFowVzELMAkGA1UEBhMCRlIxDzANBgNVBAcTBkxlTWFuczETMBEG\n" + + * "A1UEChMKcmVkaGF0LmNvbTEOMAwGA1UECxMFU2FsZXMxEjAQBgNVBAMTCWxicm91\n" + + * "ZG91eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMv+ZNhj1P6MF7i/\n" + + * "6Vdid9GNNi3q2CKgTKLPMUQjJON8Fn908WLuwsATqZILwpKUGl3Fl7lYDETaAu0h\n" + + * "viaRol4TFok8yU9i1YSt/lP7mNhnoi+/GprxlyTIifWmm/PVApzyyT+j2jUhQnuh\n" + + * "mizikQwfrGC+Ma3irJb7/lDqjU1LGNYHdOe+zGrTbVY0KavnMk8cD/pJeOtFNOU2\n" + + * "HvWax2Iah51qoYBVJfz3+yQdMbTG2GYjATaoEqF7HRrfxlahVTWus5giSszwNfGG\n" + + * "SoeTNVJbLZzMVOoNhQ7P98Ft0jH3ger9rT+5yDrFeqdsW6lo0uN+DZvBjYaHIN6/\n" + + * "rqVF9wECAwEAAaMhMB8wHQYDVR0OBBYEFK+gMMjv7ATBlDiq497auPf6sb0JMA0G\n" + + * "CSqGSIb3DQEBCwUAA4IBAQCap2OWWuhVyo2v55voV83aa3TLPMxWIQGEcKIFKSQy\n" + + * "orA+l6gvVF920XOS7/m/RU3NBxHKetk+/UnvjyxbPJGGyJFLqGfp1loT9m0RbzU6\n" + + * "M7iY0DuXja08gxlVO0tpaquNfMgasH3JoAYGS8f3DpHEOExb1gHiumIXEzlcNutV\n" + + * "Zn/ZSR+EdCJaDvC/3cLmVR07pFmGH8JM5xLWxh7wuYk9bHEEoAgVTsFk1vp+MUUU\n" + + * "KNEUjdRBSJbPKLIJKPlWWX5srefTIypUJrHys3Sxccpzlk064QIY5/IiaFk0+vXs\n" + + * "cPDGdmeplII2oAxj1qrAIFtaUZyyhDOmFFpQYm27+bYh\n" + "-----END CERTIFICATE-----"); specification.setSecret(secret); + * File trustStore = ConsumptionTaskCommons.installBrokerCertificate(specification); + * System.err.println("Using trustStore: " + trustStore.getAbsolutePath()); + * + * Properties sslProperties = new Properties(); sslProperties.put("com.ibm.ssl.trustStore", + * trustStore.getAbsolutePath()); sslProperties.put("com.ibm.ssl.trustStorePassword", + * ConsumptionTaskCommons.TRUSTSTORE_PASSWORD); sslProperties.put("com.ibm.ssl.trustStoreType", "JKS"); + * connectOptions.setSSLProperties(sslProperties); + * + * IMqttClient subscriber = new + * MqttClient("ssl://artemis-my-acceptor-0-svc-rte-microcks.apps.cluster-87b8.87b8.example.opentlc.com:443", + * "microcks-async-minion-test-" + System.currentTimeMillis()); subscriber.connect(connectOptions); + * + * // Start subscribing to the server endpoint topic. subscriber.subscribe("streetlights-event-lighting-measured", + * (topic, mqttMessage) -> { System.err.println("Received a new MQTT Message: " + new + * String(mqttMessage.getPayload())); }); + * + * Thread.sleep(10000L); + * + * } catch (Throwable t) { t.printStackTrace(); fail(); } } + */ +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/NATSMessageConsumptionTaskTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/NATSMessageConsumptionTaskTest.java new file mode 100644 index 000000000..84b19a649 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/NATSMessageConsumptionTaskTest.java @@ -0,0 +1,52 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import io.github.microcks.minion.async.AsyncTestSpecification; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for NATSMessageConsumptionTask. + * @author laurent + */ +public class NATSMessageConsumptionTaskTest { + + @Test + public void testAcceptEndpoint() { + AsyncTestSpecification specification = new AsyncTestSpecification(); + NATSMessageConsumptionTask task = new NATSMessageConsumptionTask(specification); + + assertTrue(NATSMessageConsumptionTask.acceptEndpoint("nats://localhost:4222/testTopic")); + + assertTrue(NATSMessageConsumptionTask.acceptEndpoint("nats://mynats.acme.com/testTopic")); + } + + @Test + public void testAcceptEndpointFailures() { + AsyncTestSpecification specification = new AsyncTestSpecification(); + NATSMessageConsumptionTask task = new NATSMessageConsumptionTask(specification); + + assertFalse(NATSMessageConsumptionTask.acceptEndpoint("ssl://localhost:1883/testTopic")); + + assertFalse(NATSMessageConsumptionTask.acceptEndpoint("mqtt://localhost:1883")); + + assertFalse(NATSMessageConsumptionTask.acceptEndpoint("nats://localhost:port/testTopic")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/WebSocketMessageConsumptionTaskTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/WebSocketMessageConsumptionTaskTest.java new file mode 100644 index 000000000..ebfb30470 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/consumer/WebSocketMessageConsumptionTaskTest.java @@ -0,0 +1,66 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.consumer; + +import org.junit.jupiter.api.Test; + +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; +import java.net.URI; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for WebSocketMessageConsumptionTask. + * @author laurent + */ +public class WebSocketMessageConsumptionTaskTest { + + @Test + public void testAcceptEndpoint() { + assertTrue(WebSocketMessageConsumptionTask.acceptEndpoint("ws://localhost:4000/")); + + assertTrue(WebSocketMessageConsumptionTask.acceptEndpoint("ws://localhost:4000/websocket")); + + assertTrue(WebSocketMessageConsumptionTask + .acceptEndpoint("ws://microcks-ws.example.com/api/ws/Service/1.0.0/channel/sub/path")); + } + + @Test + public void testAcceptEndpointFailures() { + assertFalse(WebSocketMessageConsumptionTask.acceptEndpoint("localhost:4000/websocket")); + + assertFalse(WebSocketMessageConsumptionTask + .acceptEndpoint("microcks-ws.example.com/api/ws/Service/1.0.0/channel/sub/path")); + } + + /* + * @Test public void testBasicReceive() { + * + * try { WebSocketClient client = new WebSocketClient(); //Session session = + * ContainerProvider.getWebSocketContainer().connectToServer(client, + * URI.create("ws://localhost:8081/api/ws/User+signed-up+WebSocket+API/0.1.9/user/signedup")); Session session = + * ContainerProvider.getWebSocketContainer().connectToServer(client, URI.create("ws://localhost:4000/websocket")); + * + * Thread.sleep(10000L); + * + * List messages = client.getMessages(); for (int i=0; i eventsMessages = List.of(eventMessage); + + AsyncMockDefinition definition = new AsyncMockDefinition(service, operation, eventsMessages); + + String queueName = producerManager.getTopicName(definition, eventMessage); + assertEquals("StreetlightsAPI-010-receiveLightMeasurement", queueName); + } + + @Test + void testTopicNameWithPart() { + AmazonSNSProducerManager producerManager = new AmazonSNSProducerManager(); + + Service service = new Service(); + service.setName("Pastry orders API"); + service.setVersion("0.1.0"); + + Operation operation = new Operation(); + operation.setName("SUBSCRIBE pastry/orders"); + operation.setMethod("SUBSCRIBE"); + operation.setDispatcher("URI_PARTS"); + operation.setResourcePaths(Set.of("pastry/orders/{orderId}")); + service.addOperation(operation); + + EventMessage eventMessage = new EventMessage(); + eventMessage.setName("Sample"); + eventMessage.setDispatchCriteria("/orderId=123-456-789"); + List eventsMessages = List.of(eventMessage); + + AsyncMockDefinition definition = new AsyncMockDefinition(service, operation, eventsMessages); + + String queueName = producerManager.getTopicName(definition, eventMessage); + assertEquals("PastryordersAPI-010-pastry-orders-123-456-789", queueName); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/AmazonSQSProducerManagerTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/AmazonSQSProducerManagerTest.java new file mode 100644 index 000000000..a272b4b50 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/AmazonSQSProducerManagerTest.java @@ -0,0 +1,86 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; +import io.github.microcks.minion.async.AsyncMockDefinition; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for AmazonSQSProducerManager. + * @author laurent + */ +class AmazonSQSProducerManagerTest { + + @Test + void testGetQueueName() { + AmazonSQSProducerManager producerManager = new AmazonSQSProducerManager(); + + Service service = new Service(); + service.setName("Streetlights API"); + service.setVersion("0.1.0"); + + Operation operation = new Operation(); + operation.setName("RECEIVE receiveLightMeasurement"); + operation.setMethod("RECEIVE"); + operation.setResourcePaths(Set.of("smartylighting.streetlights.1.0.event.lighting.measured")); + service.addOperation(operation); + + EventMessage eventMessage = new EventMessage(); + eventMessage.setName("Sample"); + List eventsMessages = List.of(eventMessage); + + AsyncMockDefinition definition = new AsyncMockDefinition(service, operation, eventsMessages); + + String queueName = producerManager.getQueueName(definition, eventMessage); + assertEquals("StreetlightsAPI-010-receiveLightMeasurement", queueName); + } + + @Test + void testGetQueueNameWithPart() { + AmazonSQSProducerManager producerManager = new AmazonSQSProducerManager(); + + Service service = new Service(); + service.setName("Pastry orders API"); + service.setVersion("0.1.0"); + + Operation operation = new Operation(); + operation.setName("SUBSCRIBE pastry/orders"); + operation.setMethod("SUBSCRIBE"); + operation.setDispatcher("URI_PARTS"); + operation.setResourcePaths(Set.of("pastry/orders/{orderId}")); + service.addOperation(operation); + + EventMessage eventMessage = new EventMessage(); + eventMessage.setName("Sample"); + eventMessage.setDispatchCriteria("/orderId=123-456-789"); + List eventsMessages = List.of(eventMessage); + + AsyncMockDefinition definition = new AsyncMockDefinition(service, operation, eventsMessages); + + String queueName = producerManager.getQueueName(definition, eventMessage); + assertEquals("PastryordersAPI-010-pastry-orders-123-456-789", queueName); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/KafkaProducerManagerIT.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/KafkaProducerManagerIT.java new file mode 100644 index 000000000..53780807a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/KafkaProducerManagerIT.java @@ -0,0 +1,460 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.ServiceView; +import io.github.microcks.domain.TestCaseResult; +import io.github.microcks.minion.async.AsyncMockDefinition; +import io.github.microcks.minion.async.AsyncMockRepository; +import io.github.microcks.minion.async.SchemaRegistry; +import io.github.microcks.minion.async.client.KeycloakConfig; +import io.github.microcks.minion.async.client.MicrocksAPIConnector; +import io.github.microcks.minion.async.client.dto.TestCaseReturnDTO; +import io.github.microcks.util.AvroUtil; + +import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; +import io.confluent.kafka.serializers.KafkaAvroDeserializer; +import org.apache.avro.Schema; +import org.apache.avro.SchemaBuilder; +import org.apache.avro.generic.GenericRecord; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.TopicCollection; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.serialization.ByteArrayDeserializer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigValue; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.Converter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.shaded.com.github.dockerjava.core.MediaType; +import org.testcontainers.shaded.com.google.common.net.HttpHeaders; +import org.testcontainers.utility.DockerImageName; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is an integration test case using Testcontainers to test + * {@link KafkaProducerManager} and {@link ProducerManager} classes. + * @author laurent + */ +@Testcontainers +class KafkaProducerManagerIT { + + private static final String TOPIC_NAME = "UsersignedupAvroAPI-0.1.1-user-signedup"; + private static final Network NETWORK = Network.newNetwork(); + + @Container + private static final KafkaContainer kafkaContainer = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:7.5.0")).withNetwork(NETWORK).withNetworkAliases("kafka") + .withListener(() -> "kafka:19092"); + + @BeforeEach + void beforeEach() { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + + try (AdminClient adminClient = AdminClient.create(props)) { + // Delete test-topic topic + adminClient.deleteTopics(TopicCollection.ofTopicNames(List.of(TOPIC_NAME))); + } + } + + @Test + void testProduceAvroMockMessages() throws Exception { + // Arrange. + String asyncAPIContent = Files.readString(Paths.get("target/test-classes/io/github/microcks/minion/async", + "user-signedup-avro-asyncapi-oneof-2.3.yaml")); + + // Prepare some event messages. + EventMessage aliceEvent = new EventMessage(); + aliceEvent.setName("Alice"); + aliceEvent.setMediaType("avro/binary"); + aliceEvent.setContent("{\"displayName\": \"Alice\"}"); + EventMessage bobEvent = new EventMessage(); + bobEvent.setName("Bob"); + bobEvent.setMediaType("avro/binary"); + bobEvent.setContent("{\"email\": \"bob@example.com\"}"); + + // Prepare associated service and operation. + Service service = new Service(); + service.setId("d3d5a3ed-13bf-493f-a06d-bf93392f420b"); + service.setName("User signed-up Avro API"); + service.setVersion("0.1.1"); + service.setType(ServiceType.EVENT); + + Operation signedupOperation = new Operation(); + signedupOperation.setName("SUBSCRIBE user/signedup"); + service.setOperations(List.of(signedupOperation)); + + // Assemble them into a repository. + AsyncMockRepository mockRepository = new AsyncMockRepository(); + AsyncMockDefinition mockDefinition = new AsyncMockDefinition(service, signedupOperation, + List.of(aliceEvent, bobEvent)); + mockRepository.storeMockDefinition(mockDefinition); + + MicrocksAPIConnector microcksAPIConnector = new FakeMicrocksAPIConnector("d3d5a3ed-13bf-493f-a06d-bf93392f420b", + asyncAPIContent); + SchemaRegistry schemaRegistry = new SchemaRegistry(microcksAPIConnector); + schemaRegistry.updateRegistryForService("d3d5a3ed-13bf-493f-a06d-bf93392f420b"); + + // Finally, arrange the objects under test. + KafkaProducerManager kafkaProducerManager = new KafkaProducerManager(); + kafkaProducerManager.config = new FakeConfig(); + kafkaProducerManager.schemaRegistryUrl = Optional.empty(); + kafkaProducerManager.bootstrapServers = kafkaContainer.getBootstrapServers(); + kafkaProducerManager.create(); + + ProducerManager producerManager = new ProducerManager(mockRepository, schemaRegistry, kafkaProducerManager, null, + null, null, null, null, null); + + // Act. + producerManager.produceKafkaMockMessages(mockDefinition); + + // Consume messages on topic during 1 second. + List messages = consumeMessagesFromTopic(TOPIC_NAME, 1000); + + // Assert. + assertFalse(messages.isEmpty()); + + Schema signupUser = SchemaBuilder.record("SignupUser").fields().requiredString("displayName").endRecord(); + Schema loginUser = SchemaBuilder.record("LoginUser").fields().requiredString("email").endRecord(); + Schema union = SchemaBuilder.unionOf().type(signupUser).and().type(loginUser).endUnion(); + + for (byte[] message : messages) { + String json = AvroUtil.avroToJson(message, union); + assertTrue(json.contains("Alice") || json.contains("bob@example.com")); + assertTrue(json.contains("displayName") || json.contains("name")); + // We'll probably have a {"displayName": "bob@example.com"} result here because of the union type + // and the limitation of Avro that doesn't serialize property names. Both schema can actually be used + // for deserialization and so the first wins! + } + } + + @Test + void testProduceAvroMockMessagesWithRegistry() throws Exception { + // Arrange. + GenericContainer schemaRegistryContainer = new GenericContainer<>( + DockerImageName.parse("confluentinc/cp-schema-registry:7.5.0")).withNetwork(NETWORK).withExposedPorts(8889) + .withEnv("SCHEMA_REGISTRY_HOST_NAME", "schema-registry") + .withEnv("SCHEMA_REGISTRY_LISTENERS", "http://0.0.0.0:8889") + .withEnv("SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS", + //"PLAINTEXT://" + kafkaContainer.getNetworkAliases().get(0) + ":9092") + "PLAINTEXT://kafka:19092") + .waitingFor(Wait.forHttp("/subjects").forStatusCode(200)); + schemaRegistryContainer.start(); + + String asyncAPIContent = Files.readString(Paths.get("target/test-classes/io/github/microcks/minion/async", + "user-signedup-avro-asyncapi-oneof-2.3.yaml")); + + // Prepare some event messages. + EventMessage aliceEvent = new EventMessage(); + aliceEvent.setName("Alice"); + aliceEvent.setMediaType("avro/binary"); + aliceEvent.setContent("{\"displayName\": \"Alice\"}"); + EventMessage bobEvent = new EventMessage(); + bobEvent.setName("Bob"); + bobEvent.setMediaType("avro/binary"); + bobEvent.setContent("{\"email\": \"bob@example.com\"}"); + + // Prepare associated service and operation. + Service service = new Service(); + service.setId("d3d5a3ed-13bf-493f-a06d-bf93392f420b"); + service.setName("User signed-up Avro API"); + service.setVersion("0.1.1"); + service.setType(ServiceType.EVENT); + + Operation signedupOperation = new Operation(); + signedupOperation.setName("SUBSCRIBE user/signedup"); + service.setOperations(List.of(signedupOperation)); + + // Assemble them into a repository. + AsyncMockRepository mockRepository = new AsyncMockRepository(); + AsyncMockDefinition mockDefinition = new AsyncMockDefinition(service, signedupOperation, + List.of(aliceEvent, bobEvent)); + mockRepository.storeMockDefinition(mockDefinition); + + MicrocksAPIConnector microcksAPIConnector = new FakeMicrocksAPIConnector("d3d5a3ed-13bf-493f-a06d-bf93392f420b", + asyncAPIContent); + SchemaRegistry schemaRegistry = new SchemaRegistry(microcksAPIConnector); + schemaRegistry.updateRegistryForService("d3d5a3ed-13bf-493f-a06d-bf93392f420b"); + + // Finally, arrange the objects under test. + KafkaProducerManager kafkaProducerManager = new KafkaProducerManager(); + kafkaProducerManager.config = new FakeConfig(); + kafkaProducerManager.schemaRegistryUrl = Optional + .of("http://localhost:%s".formatted(schemaRegistryContainer.getMappedPort(8889))); + kafkaProducerManager.schemaRegistryConfluent = true; + kafkaProducerManager.schemaRegistryUsername = Optional.empty(); + kafkaProducerManager.schemaRegistryCredentialsSource = "USER_INFO"; + kafkaProducerManager.bootstrapServers = kafkaContainer.getBootstrapServers(); + kafkaProducerManager.create(); + + ProducerManager producerManager = new ProducerManager(mockRepository, schemaRegistry, kafkaProducerManager, null, + null, null, null, null, null); + producerManager.defaultAvroEncoding = "REGISTRY"; + + // Act. + producerManager.produceKafkaMockMessages(mockDefinition); + + // Consume messages on topic during 1 second. + List messages = consumeAvroMessagesFromTopic(TOPIC_NAME, + kafkaProducerManager.schemaRegistryUrl.get(), 1000); + + // Assert. + assertFalse(messages.isEmpty()); + + for (GenericRecord message : messages) { + assertTrue( + message.getSchema().getName().equals("SignupUser") || message.getSchema().getName().equals("LoginUser")); + + if ("SignupUser".equals(message.getSchema().getName())) { + assertEquals("Alice", message.get("displayName").toString()); + } else { + assertEquals("bob@example.com", message.get("email").toString()); + } + } + + // Check that schemas have been put into registry. + URL url = new URL(kafkaProducerManager.schemaRegistryUrl.get() + "/subjects"); + HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); + httpConn.setRequestMethod("GET"); + httpConn.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.getMediaType()); + httpConn.setDoOutput(false); + + if (httpConn.getResponseCode() == 200) { + // Read response content and disconnect Http connection. + StringBuilder responseContent = new StringBuilder(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(httpConn.getInputStream(), StandardCharsets.UTF_8))) { + String responseLine; + while ((responseLine = br.readLine()) != null) { + responseContent.append(responseLine.trim()); + } + } + + assertEquals( + "[\"UsersignedupAvroAPI-0.1.1-user-signedup-LoginUser\",\"UsersignedupAvroAPI-0.1.1-user-signedup-SignupUser\"]", + responseContent.toString()); + } else { + httpConn.disconnect(); + schemaRegistryContainer.stop(); + fail("Schema registry /subjects request failed with code " + httpConn.getResponseCode()); + } + + schemaRegistryContainer.stop(); + } + + private static List consumeMessagesFromTopic(String topic, long timeout) { + List messages = new ArrayList<>(); + + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "random-" + System.currentTimeMillis()); + props.put(ConsumerConfig.CLIENT_ID_CONFIG, "random-" + System.currentTimeMillis()); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName()); + + // Only retrieve incoming messages and do not persist offset. + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); + + try (KafkaConsumer consumer = new KafkaConsumer<>(props)) { + // Subscribe Kafka consumer and receive messages. + consumer.subscribe(Collections.singletonList(topic), new ConsumerRebalanceListener() { + @Override + public void onPartitionsRevoked(Collection partitions) { + } + + @Override + public void onPartitionsAssigned(Collection partitions) { + partitions.forEach(p -> consumer.seek(p, 0)); + } + }); + + long startTime = System.currentTimeMillis(); + long timeoutTime = startTime + timeout; + + while (System.currentTimeMillis() - startTime < timeout) { + ConsumerRecords records = consumer + .poll(Duration.ofMillis(timeoutTime - System.currentTimeMillis())); + if (!records.isEmpty()) { + records.forEach(rec -> messages.add(rec.value())); + } + } + } catch (Exception e) { + fail("Exception while connecting to Kafka broker", e); + } + return messages; + } + + private static List consumeAvroMessagesFromTopic(String topic, String registryUrl, long timeout) { + List messages = new ArrayList<>(); + + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "random-" + System.currentTimeMillis()); + props.put(ConsumerConfig.CLIENT_ID_CONFIG, "random-" + System.currentTimeMillis()); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, KafkaAvroDeserializer.class.getName()); + props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, registryUrl); + + // Only retrieve incoming messages and do not persist offset. + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); + + try (KafkaConsumer consumer = new KafkaConsumer<>(props)) { + // Subscribe Kafka consumer and receive messages. + consumer.subscribe(Collections.singletonList(topic), new ConsumerRebalanceListener() { + @Override + public void onPartitionsRevoked(Collection partitions) { + } + + @Override + public void onPartitionsAssigned(Collection partitions) { + partitions.forEach(p -> consumer.seek(p, 0)); + } + }); + + long startTime = System.currentTimeMillis(); + long timeoutTime = startTime + timeout; + + while (System.currentTimeMillis() - startTime < timeout) { + ConsumerRecords records = consumer + .poll(Duration.ofMillis(timeoutTime - System.currentTimeMillis())); + if (!records.isEmpty()) { + records.forEach(rec -> messages.add(rec.value())); + } + } + } catch (Exception e) { + fail("Exception while connecting to Kafka broker", e); + } + return messages; + } + + private static class FakeMicrocksAPIConnector implements MicrocksAPIConnector { + + private String serviceId; + private String asyncAPIContent; + + FakeMicrocksAPIConnector(String serviceId, String asyncAPIContent) { + this.serviceId = serviceId; + this.asyncAPIContent = asyncAPIContent; + } + + @Override + public KeycloakConfig getKeycloakConfig() { + return null; + } + + @Override + public List listServices(String authorization, int page, int size) { + return null; + } + + @Override + public ServiceView getService(String authorization, String serviceId, boolean messages) { + return null; + } + + @Override + public List getResources(String serviceId) { + if (this.serviceId.equals(serviceId)) { + Resource resource = new Resource(); + resource.setId("578497d2-2695-4aff-b7c6-ff7b9a373aa9"); + resource.setType(ResourceType.ASYNC_API_SPEC); + resource.setContent(this.asyncAPIContent); + return List.of(resource); + } + return Collections.emptyList(); + } + + @Override + public TestCaseResult reportTestCaseResult(String testResultId, TestCaseReturnDTO testCaseReturn) { + return null; + } + } + + private static class FakeConfig implements Config { + @Override + public T getValue(String propertyName, Class propertyType) { + return null; + } + + @Override + public ConfigValue getConfigValue(String propertyName) { + return null; + } + + @Override + public Optional getOptionalValue(String propertyName, Class propertyType) { + return Optional.empty(); + } + + @Override + public Iterable getPropertyNames() { + return null; + } + + @Override + public Iterable getConfigSources() { + return null; + } + + @Override + public Optional> getConverter(Class forType) { + return Optional.empty(); + } + + @Override + public T unwrap(Class type) { + return null; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/KafkaProducerManagerTest.java b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/KafkaProducerManagerTest.java new file mode 100644 index 000000000..cdb701da9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/java/io/github/microcks/minion/async/producer/KafkaProducerManagerTest.java @@ -0,0 +1,89 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.minion.async.producer; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; +import io.github.microcks.minion.async.AsyncMockDefinition; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for KafkaProducerManager. + * @author laurent + */ +class KafkaProducerManagerTest { + + @Test + void testGetTopicName() { + KafkaProducerManager producerManager = new KafkaProducerManager(); + + Service service = new Service(); + service.setName("Streetlights API"); + service.setVersion("0.1.0"); + + Operation operation = new Operation(); + operation.setName("RECEIVE receiveLightMeasurement"); + operation.setMethod("RECEIVE"); + operation.setResourcePaths(Set.of("smartylighting.streetlights.1.0.event.lighting.measured")); + service.addOperation(operation); + + EventMessage eventMessage = new EventMessage(); + eventMessage.setName("Sample"); + List eventsMessages = List.of(eventMessage); + + AsyncMockDefinition definition = new AsyncMockDefinition(service, operation, eventsMessages); + + String topicName = producerManager.getTopicName(definition, eventMessage); + assertEquals("StreetlightsAPI-0.1.0-receiveLightMeasurement", topicName); + } + + @Test + void testGetDynamicTopicName() { + KafkaProducerManager producerManager = new KafkaProducerManager(); + + Service service = new Service(); + service.setName("Streetlights API"); + service.setVersion("0.1.0"); + + Operation operation = new Operation(); + operation.setName("RECEIVE receiveLightMeasurement"); + operation.setMethod("RECEIVE"); + operation.setDispatcher("URI_PARTS"); + operation.setDispatcherRules("/streetlightId"); + operation.setResourcePaths(Set.of("smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", + "smartylighting.streetlights.1.0.event.da059782-3ad0-4e45-88ce-ef3392bc7797.lighting.measured")); + service.addOperation(operation); + + EventMessage eventMessage = new EventMessage(); + eventMessage.setName("Sample"); + eventMessage.setDispatchCriteria("streetlightId=da059782-3ad0-4e45-88ce-ef3392bc7797"); + List eventsMessages = List.of(eventMessage); + + AsyncMockDefinition definition = new AsyncMockDefinition(service, operation, eventsMessages); + + String topicName = producerManager.getTopicName(definition, eventMessage); + assertEquals( + "StreetlightsAPI-0.1.0-smartylighting.streetlights.1.0.event.da059782-3ad0-4e45-88ce-ef3392bc7797.lighting.measured", + topicName); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/send-message-asyncapi-3.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/send-message-asyncapi-3.0.yaml new file mode 100644 index 000000000..2ec81a8b2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/send-message-asyncapi-3.0.yaml @@ -0,0 +1,48 @@ +asyncapi: '3.0.0' +info: + title: Message Sender API + version: '1.0.0' + description: | + This AsyncAPI specification defines a channel `sendMessage` + that allows sending messages with any type of payload. + +servers: + echoServer: + host: echo-websocket.hoppscotch.io + protocol: wss + +channels: + echo: + description: > + The single channel where messages are sent to and echoed back. + address: / + messages: + echo: + $ref: '#/components/messages/echo' + +operations: + sendEchoMessage: + action: send + channel: + $ref: '#/channels/echo' + summary: Send a message to the echo server. + messages: + - $ref: '#/channels/echo/messages/echo' + +components: + messages: + echo: + summary: A message exchanged with the echo server. + contentType: text/plain + examples: + - name: string + payload: test + - name: boolean + payload: true + - name: number + payload: 123 + - name: object + payload: + test: test text + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/user-signedup-asyncapi-3.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/user-signedup-asyncapi-3.0.yaml new file mode 100644 index 000000000..bc9005754 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/user-signedup-asyncapi-3.0.yaml @@ -0,0 +1,86 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.3.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent@microcks.io + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user-signedup: + description: The topic on which user signed up events may be consumed + messages: + userSignedUp: + $ref: '#/components/messages/userSignedUp' +operations: + publishUserSignedUps: + action: 'send' + channel: + $ref: '#/channels/user-signedup' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/user-signedup/messages/userSignedUp' +components: + messages: + userSignedUp: + description: An event describing that a user just signed up + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: + fullName: Laurent Broudoux + email: "laurent@microcks.io" + age: 41 + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + - laurent: + my-app-header: 21 + - yacine: + my-app-header: 22 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/user-signedup-avro-asyncapi-oneof-2.3.yaml b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/user-signedup-avro-asyncapi-oneof-2.3.yaml new file mode 100644 index 000000000..9702c787f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/minions/async/src/test/resources/io/github/microcks/minion/async/user-signedup-avro-asyncapi-oneof-2.3.yaml @@ -0,0 +1,45 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up Avro API + version: 0.1.1 + description: Sample AsyncAPI for user signedup/login events defined using Avro +defaultContentType: application/json +channels: + user/signedup: + subscribe: + message: + description: An event describing that a user just signed up. + oneOf: + - $ref: '#/components/messages/signup' + - $ref: '#/components/messages/login' +components: + messages: + signup: + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + doc: User information + name: SignupUser + fields: + - name: displayName + type: string + examples: + - name: Alice + payload: + displayName: Alice + login: + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + doc: User information + name: LoginUser + fields: + - name: email + type: string + examples: + - name: Bob + payload: + email: bob@example.com diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/README.md b/jdk_21_maven/cs/rest-gui/microcks/observability/README.md new file mode 100644 index 000000000..5b478ec2a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/README.md @@ -0,0 +1,89 @@ +# OpenTelemetry observability on Microcks instance + +This folder provides resources for configuring OpenTelemetry support in Microcks and running an observability stack on your +local development machine. + +## OpenTelemetry configuration + +In order to enable OpenTelemetry on your Microcks instance, you'll have to define those 2 additional environment variables: + +* `OTEL_JAVAAGENT_ENABLED` is set to false by default, so you'll have to explicitly set it to `true` +* `OTEL_EXPORTER_OTLP_ENDPOINT` is set to a local dummy endpoint, so you'll have to set it to an OpenTelemetry collector endpoint + of your environment. Something like `http://otel-collector.acme.com:4317` for example. + +Optionally, you may override this following environment variables to fit your environment requirements: + +* `OTEL_METRIC_EXPORT_INTERVAL`, those default value is set to `10000` +* `OTEL_EXPORTER_OTLP_TIMEOUT`, those default value is set to `7000` +* `OTEL_RESOURCE_ATTRIBUTES`, those default value is set to `service.name=microcks,service.namespace=microcks-ns,service.instance.id=my-microcks-dev,service.version=1.9.0` + +The above environment variables may be easily setup via the Microcks Helm Chart or Operator using the `microcks.env` properties. + +## Grafana dashboard + +You can find a Grafana dashboard for Microcks in the `./dashboards/microcks-overview.json` file. You'll get here detailed performance +view on Microcks mock endpoints, as well as logs, as well as distributed tracing. + +## Want to generate some load? + +Check our [benchmark](../benchmark/README.md) folder in this GitHub repository. + +## Running the observability stack locally + +We provide everything so that you can easily test and try the observability features locally! + +Open a terminal and use our `start-o11y-dev-stack.sh` shell script like below: + +```shell +$ ./start-o11y-dev-stack.sh +--- OUTPUT --- +Setting up dedicated network bridge.. +cabc290d6b9f9e23569103f17ec4201407f099695016486cd332663b27b082dc +✅ o11y docker network created ! +e88eb45e85894f13cae717c090940dcc17fc65a65271a9fb22a4f7c6790ea0c8 +393fa7f135cecba4a1b2d64cb75d0cc319c595a2bffcd8944223db9fdaf9b3fd +ad09df1e2fd225cbbddf0619ef99ab7de9c6501b916f3cc220d297d000d731d9 +1bba5d36bc885fc6c89587ac260fb839a294d1f7ee71884392e247bf1a185a65 +b464ab2682b2398f56ba84cc9dc559afca794153b6aee45481600b89d76d076f +Check state of the stack ! +✅ Prometheus OK +✅ Grafana OK +✅ OpenTelemetryCollector OK +✅ Tempo OK +✅ Loki OK +Finished ! +``` + +This will start a bunch of containers providing the OpenTelemetry services: + +```shell +$ docker ps +--- OUTPUT --- +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +b464ab2682b2 grafana/grafana:10.2.3 "/run.sh" 22 minutes ago Up 22 minutes 0.0.0.0:3000->3000/tcp o11y_grafana +1bba5d36bc88 otel/opentelemetry-collector-contrib:0.92.0 "/otelcol-contrib --…" 22 minutes ago Up 22 minutes 0.0.0.0:4317->4317/tcp, 0.0.0.0:13133->13133/tcp, 55678-55679/tcp o11y_otel +ad09df1e2fd2 grafana/tempo:2.3.0 "/tempo -config.file…" 22 minutes ago Up 22 minutes 0.0.0.0:3200->3200/tcp o11y_tempo +393fa7f135ce grafana/loki:2.9.3 "/usr/bin/loki -conf…" 22 minutes ago Up 22 minutes 0.0.0.0:3100->3100/tcp o11y_loki +e88eb45e8589 prom/prometheus:v2.48.1 "/bin/prometheus --c…" 22 minutes ago Up 22 minutes 0.0.0.0:9080->9090/tcp +``` + +You can now open Grafana, loaded with a Microcks dashboard into your browser at `http://localhost:3000`. + +> For people running Microcks locally using docker-compose or other, you'll need to add the extra environment variables introduced above +and set `OTEL_EXPORTER_OTLP_ENDPOINT` to `http://host.docker.internal:4317` to access the OpenTelemetry collector from within the docker network. + +> For Microcks developers: enabling the OpenTelemetry features is accessible via a Maven profile. Just run `mvn -Pdev-otel spring-boot:run` +and Microcks is started and configured to use this local OpenTelemetry stack. + +To stop all the observability-related containers and free resources on your machine, you can simply execute our `teardown-o11y-dev-stack.sh`: + +```shell +$ ./teardown-o11y-dev-stack.sh +--- OUTPUT --- +o11y_prom +o11y_loki +o11y_tempo +o11y_grafana +o11y_otel +o11y +``` \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/configs/grafana-datasources.yaml b/jdk_21_maven/cs/rest-gui/microcks/observability/configs/grafana-datasources.yaml new file mode 100644 index 000000000..bb4fbbf6a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/configs/grafana-datasources.yaml @@ -0,0 +1,50 @@ +# config file version +apiVersion: 1 +datasources: + - name: Prometheus + uid: grafanacloud-prom + type: prometheus + access: proxy + url: http://o11y_prom:9090 + jsonData: + exemplarTraceIdDestinations: + - datasourceUid: grafanacloud-traces + name: trace_id + httpMethod: POST + prometheusType: "Prometheus" + prometheusVersion: "2.48.0" + isDefault: true + editable: true + + + - name: Loki + uid: grafanacloud-logs + type: loki + access: proxy + url: http://o11y_loki:3100 + editable: true + jsonData: + derivedFields: + - datasourceUid: grafanacloud-traces + matcherRegex: '[tT]race_?[iI][dD]"?[:=]"?(\w+)' + name: traceId + url: '$${__value.raw}' + + - name: Tempo + type: tempo + access: proxy + uid: grafanacloud-traces + url: http://o11y_tempo:3200 + editable: true + jsonData: + nodeGraph: + enabled: true + tracesToLogsV2: + datasourceUid: grafanacloud-logs + filterBySpanID: false + filterByTraceID: false + spanEndTimeShift: "5m" + spanStartTimeShift: "-5m" + customQuery: true + query: | + { job = "$${__span.tags['service.namespace']}/$${__span.tags['service.name']}"} |= "$${__trace.traceId}" diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/configs/otel-collector.yaml b/jdk_21_maven/cs/rest-gui/microcks/observability/configs/otel-collector.yaml new file mode 100644 index 000000000..a558d0994 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/configs/otel-collector.yaml @@ -0,0 +1,27 @@ +receivers: + otlp: + protocols: + grpc: +exporters: + otlp: + endpoint: o11y_tempo:4317 + tls: + insecure: true + loki: + endpoint: http://o11y_loki:3100/loki/api/v1/push + prometheusremotewrite: + endpoint: http://o11y_prom:9090/api/v1/write +service: + pipelines: + traces: + receivers: [otlp] + exporters: [otlp] + metrics: + receivers: [otlp] + exporters: [prometheusremotewrite] + logs: + receivers: [otlp] + exporters: [loki] + extensions: [health_check] +extensions: + health_check: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/configs/tempo.yaml b/jdk_21_maven/cs/rest-gui/microcks/observability/configs/tempo.yaml new file mode 100644 index 000000000..80e0188b8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/configs/tempo.yaml @@ -0,0 +1,55 @@ +server: + http_listen_port: 3200 + +query_frontend: + search: + duration_slo: 5s + throughput_bytes_slo: 1.073741824e+09 + trace_by_id: + duration_slo: 5s + +distributor: + receivers: # this configuration will listen on all ports and protocols that tempo is capable of. + jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can + protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver + thrift_http: # + grpc: # for a production deployment you should only enable the receivers you need! + thrift_binary: + thrift_compact: + zipkin: + otlp: + protocols: + http: + grpc: + opencensus: + +ingester: + max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally + +compactor: + compaction: + block_retention: 1h # overall Tempo trace retention. set for demo purposes + +metrics_generator: + registry: + external_labels: + source: tempo + cluster: docker-compose + storage: + path: /tmp/tempo/generator/wal + remote_write: + - url: http://o11y_prom:9090/api/v1/write + send_exemplars: true + +storage: + trace: + backend: local # backend configuration to use + wal: + path: /tmp/tempo/wal # where to store the the wal locally + local: + path: /tmp/tempo/blocks + +overrides: + defaults: + metrics_generator: + processors: [service-graphs, span-metrics] # enables metrics generator \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/dashboards.yml b/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/dashboards.yml new file mode 100644 index 000000000..b7004cf8f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/dashboards.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: +- name: 'provisioned-dashboards' + orgId: 1 + folder: '' + type: file + disableDeletion: true + allowUiUpdates: true + options: + path: /etc/grafana/provisioning/dashboards + foldersFromFilesStructure: true diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/k6.json b/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/k6.json new file mode 100644 index 000000000..c594f5d20 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/k6.json @@ -0,0 +1,2039 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Visualize k6 OSS results stored in Prometheus (https://grafana.com/grafana/dashboards/19665-k6-prometheus/)", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 19665, + "graphTooltip": 0, + "id": 2, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Grafana k6 OSS Docs: Prometheus Remote Write", + "tooltip": "Open docs in a new tab", + "type": "link", + "url": "https://k6.io/docs/results-output/real-time/prometheus-remote-write/" + } + ], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "http_req_s_errors" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "unit", + "value": "reqps" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "http_req_s" + }, + "properties": [ + { + "id": "unit", + "value": "reqps" + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "vus" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed" + } + }, + { + "id": "unit", + "value": "VUs" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_vus{testid=~\"$testid\"})", + "instant": false, + "legendFormat": "vus", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_duration_$quantile_stat", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http_req_s", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(round(k6_http_req_failed_rate{testid=~\"$testid\"}, 0.1)*100)", + "hide": true, + "instant": false, + "legendFormat": "http_req_failed", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\", expected_response=\"false\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http_req_s_errors", + "range": true, + "refId": "D" + } + ], + "title": "Performance Overview", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 1, + "panels": [], + "title": "Performance Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 12 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "sum(k6_http_reqs_total{testid=~\"$testid\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "HTTP requests", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 12 + }, + "id": 22, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "sum(k6_http_reqs_total{testid=~\"$testid\", expected_response=\"false\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "HTTP request failures", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 12 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Peak RPS", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "description": "Select a different Stat to change the query", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 12 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Request Duration", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(irate(k6_data_sent_total{testid=~\"$testid\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "data_sent", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg(irate(k6_data_received_total{testid=~\"$testid\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "data_received", + "range": true, + "refId": "B" + } + ], + "title": "Transfer Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "dropped_iterations" + }, + "properties": [ + { + "id": "unit", + "value": "none" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_iteration_duration_$quantile_stat{testid=~\"$testid\"})", + "instant": false, + "legendFormat": "iteration_duration_$quantile_stat", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg(k6_dropped_iterations_total{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "dropped_iterations", + "range": true, + "refId": "B" + } + ], + "title": "Iterations", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 16, + "panels": [], + "title": "HTTP", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "description": "Select a different Stat to change the query\n\nHTTP-specific built-in metrics", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 24 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_blocked_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_blocked_$quantile_stat", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_tls_handshaking_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_tls_handshaking_$quantile_stat", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_sending_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_sending_$quantile_stat", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_waiting_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_waiting_$quantile_stat", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_receiving_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_receiving_$quantile_stat", + "range": true, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_duration_$quantile_stat", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Latency Timings", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "description": "Select a different Stat to change the query", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "errors_http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "success_http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "http_req_duration_[a-zA-Z0-9_]+" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 24 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\"})", + "hide": false, + "instant": false, + "legendFormat": "http_req_duration_$quantile_stat", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\", expected_response=\"true\"})", + "instant": false, + "legendFormat": "success_http_req_duration_$quantile_stat", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(k6_http_req_duration_$quantile_stat{testid=~\"$testid\", expected_response=\"false\"})", + "hide": false, + "instant": false, + "legendFormat": "errors_http_req_duration_$quantile_stat", + "range": true, + "refId": "B" + } + ], + "title": "HTTP Latency Stats", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "http_req_s_errors" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "http_req_s" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "http_req_s_success" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + }, + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 24 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "http_req_s", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\", expected_response=\"false\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http_req_s_errors", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "sum(irate(k6_http_reqs_total{testid=~\"$testid\", expected_response=\"true\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "http_req_s_success", + "range": true, + "refId": "C" + } + ], + "title": "HTTP Request Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "description": "min/max/p95/p99 depends on the available Quantile Stats", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "name" + }, + "properties": [ + { + "id": "filterable", + "value": false + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "method" + }, + "properties": [ + { + "id": "filterable", + "value": false + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "status" + }, + "properties": [ + { + "id": "filterable", + "value": false + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "min" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "p95" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "p99" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 17, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 2, + "showHeader": true + }, + "pluginVersion": "10.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg by(name, method, status) (k6_http_req_duration_min{testid=~\"$testid\"})", + "format": "table", + "hide": false, + "instant": false, + "legendFormat": "min", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg by(name, method, status) (k6_http_req_duration_max{testid=~\"$testid\"})", + "format": "table", + "hide": false, + "instant": false, + "legendFormat": "max", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg by(name, method, status) (k6_http_req_duration_p95{testid=~\"$testid\"})", + "format": "table", + "hide": false, + "instant": false, + "legendFormat": "p95", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg by(name, method, status) (k6_http_req_duration_p99{testid=~\"$testid\"})", + "format": "table", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "E" + } + ], + "title": "Requests by URL", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "groupBy", + "options": { + "fields": { + "Value #B": { + "aggregations": [ + "min" + ], + "operation": "aggregate" + }, + "Value #C": { + "aggregations": [ + "max" + ], + "operation": "aggregate" + }, + "Value #D": { + "aggregations": [ + "mean" + ], + "operation": "aggregate" + }, + "Value #E": { + "aggregations": [ + "mean" + ], + "operation": "aggregate" + }, + "method": { + "aggregations": [], + "operation": "groupby" + }, + "name": { + "aggregations": [], + "operation": "groupby" + }, + "status": { + "aggregations": [], + "operation": "groupby" + } + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Time": 0, + "Value #B": 4, + "Value #C": 5, + "Value #D": 6, + "Value #E": 7, + "method": 2, + "name": 1, + "status": 3 + }, + "renameByName": { + "Value #B": "min", + "Value #B (min)": "min", + "Value #C": "max", + "Value #C (max)": "max", + "Value #D": "p95", + "Value #D (mean)": "p95", + "Value #E": "p99", + "Value #E (mean)": "p99" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 11, + "panels": [], + "title": "Checks", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Success Rate" + }, + "properties": [ + { + "id": "custom.hidden", + "value": false + }, + { + "id": "unit", + "value": "%" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Value (mean)" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "check" + }, + "properties": [ + { + "id": "filterable", + "value": false + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 2, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Value (count)" + } + ] + }, + "pluginVersion": "10.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "round(k6_checks_rate{testid=~\"$testid\"}, 0.1)", + "format": "table", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Checks list", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "__name__", + "check" + ], + "mode": "columns" + } + }, + { + "id": "groupBy", + "options": { + "fields": { + "Value": { + "aggregations": [ + "mean" + ], + "operation": "aggregate" + }, + "check": { + "aggregations": [], + "operation": "groupby" + }, + "k6_checks_rate": { + "aggregations": [ + "sum", + "count" + ], + "operation": "aggregate" + } + } + } + }, + { + "id": "calculateField", + "options": { + "alias": "Success Rate", + "binary": { + "left": "Value (mean)", + "operator": "*", + "reducer": "sum", + "right": "100" + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + } + } + }, + { + "id": "convertFieldType", + "options": { + "conversions": [], + "fields": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "description": "Filter by check name to query a particular check", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 100, + "axisSoftMin": 0, + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "%" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "expr": "avg(round(k6_checks_rate{testid=~\"$testid\"}, 0.1)*100)", + "instant": false, + "legendFormat": "k6_checks_rate", + "range": true, + "refId": "A" + } + ], + "title": "Checks Success Rate (aggregate individual checks)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 48 + }, + "id": 23, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "### Visualize other k6 results \n\nAt the top of the dashboard, click `Add` and select `Visualization` from the dropdown menu. Choose the visualization type and input the PromQL queries for the `k6_` metric(s).\n\nAlternatively, click on the `Explore` icon on the menu bar and input the queries for the `k6_` metric(s). From `Explore`, you can add new Panels to this dashboard. \n\nNote that all k6 metrics are prefixed with the `k6_` namespace when sent to Prometheus.", + "mode": "markdown" + }, + "pluginVersion": "10.2.0", + "type": "text" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [ + "prometheus", + "k6" + ], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "default", + "value": "default" + }, + "description": "Choose a Prometheus Data Source", + "hide": 0, + "includeAll": false, + "label": "Prometheus DS", + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "definition": "label_values(testid)", + "description": "Filter by \"testid\" tag. Define it by tagging: k6 run --tag testid=xyz", + "hide": 0, + "includeAll": true, + "label": "Test ID", + "multi": true, + "name": "testid", + "options": [], + "query": { + "query": "label_values(testid)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "p99", + "value": "p99" + }, + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "definition": "metrics(k6_http_req_duration_)", + "description": "Statistic for Trend Metrics Queries. The available options depend on the values of the K6_PROMETHEUS_RW_TREND_STATS setting.", + "hide": 0, + "includeAll": false, + "label": "Trend Metrics Query", + "multi": false, + "name": "quantile_stat", + "options": [], + "query": { + "query": "metrics(k6_http_req_duration_)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "/http_req_duration_(min|max|count|sum|avg|med|p[0-9]+)/g", + "skipUrlSync": false, + "sort": 2, + "type": "query" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "description": "Adhoc filters are applied to all panels. To enable it, go to Dashboard Settings / Variables / adhoc_filter and select the target Prometheus data source.", + "filters": [], + "hide": 0, + "label": "AdhocFilter", + "name": "adhoc_filter", + "skipUrlSync": false, + "type": "adhoc" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "k6-prometheus", + "uid": "k6-prometheus", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/microcks-overview.json b/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/microcks-overview.json new file mode 100644 index 000000000..5df8ff2c4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/dashboards/microcks-overview.json @@ -0,0 +1,910 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 8, + "panels": [], + "title": "RED Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "right", + "cellOptions": { + "mode": "lcd", + "type": "gauge", + "valueDisplayMode": "text" + }, + "filterable": true, + "inspect": false + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "max": 10, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 2 + }, + { + "color": "orange", + "value": 3 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "tps" + }, + "properties": [ + { + "id": "unit", + "value": "reqps" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + }, + { + "id": "custom.width", + "value": 96 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "requests" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + }, + { + "id": "custom.width", + "value": 97 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "success-rate" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 10 + }, + { + "color": "#EAB839", + "value": 60 + }, + { + "color": "green", + "value": 80 + } + ] + } + }, + { + "id": "max", + "value": 100 + }, + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + }, + { + "id": "custom.width", + "value": 133 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "service" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 14, + "x": 0, + "y": 1 + }, + "id": 3, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(service,version)(rate(http_server_requests_seconds_count{job=\"$job\"}[$__range]))", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "tps" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (service,version) (increase(http_server_requests_seconds_sum{job=\"$job\"}[$__range])) / sum by (service,version) (increase(http_server_requests_seconds_count{job=\"$job\"}[$__range]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "avg-response-time-seconds" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(.95,sum(rate(http_server_requests_seconds_bucket{job=\"$job\"}[$__range])) by(service,version,le))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "p95" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(.99,sum(rate(http_server_requests_seconds_bucket{job=\"$job\"}[$__range])) by(service,version,le))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "p99" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(service,version)(increase(http_server_requests_seconds_count{job=\"$job\"}[$__range]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "requests" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(service,version) (http_server_requests_seconds_count{job=\"$job\", status=~\"2.*\",}) / sum by(service,version) (http_server_requests_seconds_count{job=\"$job\"})*100", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "success-rate" + } + ], + "title": "Performance metrics overview per service during selected time range", + "transformations": [ + { + "id": "groupBy", + "options": { + "fields": { + "Value #avg-response-time-seconds": { + "aggregations": [ + "last" + ], + "operation": "aggregate" + }, + "Value #p95": { + "aggregations": [ + "last" + ], + "operation": "aggregate" + }, + "Value #p99": { + "aggregations": [ + "last" + ], + "operation": "aggregate" + }, + "Value #requests": { + "aggregations": [ + "last" + ], + "operation": "aggregate" + }, + "Value #success-rate": { + "aggregations": [ + "last" + ], + "operation": "aggregate" + }, + "Value #tps": { + "aggregations": [ + "last" + ], + "operation": "aggregate" + }, + "service": { + "aggregations": [], + "operation": "groupby" + }, + "version": { + "aggregations": [] + } + } + } + }, + { + "id": "joinByField", + "options": { + "byField": "service", + "mode": "inner" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Time 2": true, + "Time 3": true, + "Time 4": true, + "Time 5": true, + "service": false, + "service 4": true, + "service 5": true, + "service/version": false, + "version": true, + "version 2": true, + "version 3": true, + "version 4": true, + "version 5": true, + "version 6": true + }, + "includeByName": {}, + "indexByName": { + "Value #avg-response-time-seconds (last)": 8, + "Value #p95 (last)": 11, + "Value #p99 (last)": 14, + "Value #requests (last)": 3, + "Value #success-rate (last)": 5, + "Value #tps (last)": 4, + "service": 0, + "service 2": 6, + "service 3": 9, + "service 4": 12, + "service 5": 15, + "service 6": 17, + "service/version": 1, + "version": 2, + "version 2": 7, + "version 3": 10, + "version 4": 13, + "version 5": 16, + "version 6": 18 + }, + "renameByName": { + "Value #avg-response-time-seconds": "avg-response-time", + "Value #avg-response-time-seconds (last)": "average-response-time", + "Value #p95": "p95", + "Value #p95 (last)": "p95", + "Value #p99 (last)": "p99", + "Value #requests": "requests", + "Value #requests (last)": "requests", + "Value #success-rate": "success-rate", + "Value #success-rate (last)": "success-rate", + "Value #tps": "tps", + "Value #tps (last)": "tps", + "service/version": "" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "+Inf" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 10, + "x": 14, + "y": 1 + }, + "id": 5, + "options": { + "displayMode": "gradient", + "maxVizHeight": 300, + "minVizHeight": 75, + "minVizWidth": 16, + "namePlacement": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": false, + "sizing": "manual", + "text": {}, + "valueMode": "text" + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (le) (increase(http_server_requests_seconds_bucket{job=\"$job\"}[$__range]))", + "format": "heatmap", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Response time distribution in seconds over selected time range", + "transformations": [], + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 0, + "y": 8 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum by(service,version)(rate(http_server_requests_seconds_count{job=\"$job\"}[1m]))", + "interval": "", + "legendFormat": "{{service}} | {{version}}", + "range": true, + "refId": "A" + } + ], + "title": "TPS per service & version over last minute", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 9, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(.99,sum(rate(http_server_requests_seconds_bucket{job=\"$job\"}[5m])) by(service,version, le))", + "interval": "", + "legendFormat": "{{service}} | {{version}}", + "range": true, + "refId": "A" + } + ], + "title": "P99 per service & version over last 5 min", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(.95,sum(rate(http_server_requests_seconds_bucket{job=\"$job\"}[5m])) by(service,version, le))", + "interval": "", + "legendFormat": "{{service}} | {{version}}", + "range": true, + "refId": "A" + } + ], + "title": "P95 per service & version over last 5 min", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 6, + "panels": [], + "title": "Logs", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "grafanacloud-logs" + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 7, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "pluginVersion": "8.4.3", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "grafanacloud-logs" + }, + "editorMode": "code", + "expr": "{job=\"$job\"} | json | line_format `\u001b[1m{{if .level }}{{alignRight 5 .level}}{{else if .severity}}{{alignRight 5 .severity}}{{end}}\u001b[0m \u001b[90m[{{alignRight 10 .resources_service_instance_id}}{{if .attributes_thread_name}}/{{alignRight 20 .attributes_thread_name}}{{else if eq \"java\" .resources_telemetry_sdk_language }} {{end}}]\u001b[0m \u001b[36m{{if .instrumentation_scope_name }}{{alignRight 40 .instrumentation_scope_name}}{{end}}\u001b[0m {{.body}} {{if .traceid}} \u001b[37m\u001b[3m[traceid={{.traceid}}]{{end}}`", + "hide": false, + "maxLines": 80, + "queryType": "range", + "refId": "A" + } + ], + "title": "Log of All Spring Boot Apps", + "type": "logs" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "microcks-app-ns/microcks-app", + "value": "microcks-app-ns/microcks-app" + }, + "datasource": { + "type": "prometheus", + "uid": "grafanacloud-prom" + }, + "definition": "label_values(process_uptime_seconds,job)", + "hide": 0, + "includeAll": false, + "label": "Job Name", + "multi": false, + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(process_uptime_seconds,job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "microcks-overview", + "uid": "microcks-overview", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/start-o11y-dev-stack.sh b/jdk_21_maven/cs/rest-gui/microcks/observability/start-o11y-dev-stack.sh new file mode 100644 index 000000000..3b97a73e0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/start-o11y-dev-stack.sh @@ -0,0 +1,143 @@ + +# this script launches all observability backends for microcks +# create docker network for mlt apps + +# https://hub.docker.com/r/prom/prometheus/tags?page=1&name=2. +export PROMETHEUS_VERSION=v2.48.1 + +# https://hub.docker.com/r/grafana/loki/tags?page=1&name=2. +export LOKI_VERSION=2.9.3 + +# https://hub.docker.com/r/grafana/tempo/tags?page=1&name=2. +export TEMPO_VERSION=2.3.1 + +# https://hub.docker.com/r/otel/opentelemetry-collector-contrib/tags?page=1&name=0. +export OTEL_VERSION=0.92.0 + +# https://hub.docker.com/r/grafana/grafana/tags?page=1&name=10. +export GRAFANA_VERSION=10.2.3 + + +#Network name +export NETWORK_NAME=o11y + + +function readiness_check { + name=$1 + url=$2 + res=$(curl --retry 10 -f --retry-all-errors --retry-delay 5 -s -w "%{http_code}" -o /dev/null "$url") && if [ $res -eq "200" ]; then echo "✅ $name OK"; else echo "❌ $name FAILED"; fi +} + +############################## +### Docker Network############ +############################## + +echo "Setting up dedicated network bridge.." + +if [ -z $(docker network ls --filter name=^${NETWORK_NAME}$ --format="{{ .Name }}") ] ; then + docker network create --driver=bridge --subnet=172.19.0.0/16 --gateway=172.19.0.1 ${NETWORK_NAME} ; + echo "✅ $NETWORK_NAME docker network created !" +else + echo "✅ $NETWORK_NAME already exisits ! " +fi + +############################## +### Prometheus ############### +############################## + +CONTAINER_NAME=o11y_prom +if ! docker ps -a --format '{{.Names}}' | grep -w $CONTAINER_NAME &> /dev/null; then + docker run -d \ + --name $CONTAINER_NAME \ + -h $CONTAINER_NAME \ + --network=$NETWORK_NAME \ + -p 9080:9090 \ + prom/prometheus:${PROMETHEUS_VERSION} --config.file=/etc/prometheus/prometheus.yml --web.enable-remote-write-receiver --enable-feature=exemplar-storage +else + docker start $CONTAINER_NAME +fi + +############################## +### Loki ##################### +############################## + +CONTAINER_NAME=o11y_loki +if ! docker ps -a --format '{{.Names}}' | grep -w $CONTAINER_NAME &> /dev/null; then + docker run -d \ + --name $CONTAINER_NAME \ + -h $CONTAINER_NAME \ + --network=$NETWORK_NAME \ + -p 3100:3100 \ + grafana/loki:${LOKI_VERSION} -config.file=/etc/loki/local-config.yaml +else + docker start $CONTAINER_NAME +fi + +############################## +### Tempo #################### +############################## + +CONTAINER_NAME=o11y_tempo +if ! docker ps -a --format '{{.Names}}' | grep -w $CONTAINER_NAME &> /dev/null; then + docker run -d \ + --name $CONTAINER_NAME \ + -h $CONTAINER_NAME \ + --network=$NETWORK_NAME \ + -p 3200:3200 \ + -v $(pwd)/configs/tempo.yaml:/etc/tempo.yaml:ro \ + grafana/tempo:2.3.0 -config.file=/etc/tempo.yaml +else + docker start $CONTAINER_NAME +fi + +############################## +### OTEL ##################### +############################## + +CONTAINER_NAME=o11y_otel +if ! docker ps -a --format '{{.Names}}' | grep -w $CONTAINER_NAME &> /dev/null; then + docker run -d \ + --name $CONTAINER_NAME \ + -h $CONTAINER_NAME \ + --network=$NETWORK_NAME \ + -p 4317:4317 \ + -p 13133:13133 \ + -v $(pwd)/configs/otel-collector.yaml:/etc/otel-collector.yaml:ro \ + otel/opentelemetry-collector-contrib:${OTEL_VERSION} --config=/etc/otel-collector.yaml +else + docker start $CONTAINER_NAME +fi + + +############################## +### Grafana ################## +############################## + +CONTAINER_NAME=o11y_grafana +if ! docker ps -a --format '{{.Names}}' | grep -w $CONTAINER_NAME &> /dev/null; then + docker run -d \ + --name $CONTAINER_NAME \ + -h $CONTAINER_NAME \ + --network=$NETWORK_NAME \ + -p 3000:3000 \ + -e GF_AUTH_ANONYMOUS_ENABLED=true \ + -e GF_AUTH_ANONYMOUS_ORG_ROLE=Admin \ + -v $(pwd)/configs/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml:ro \ + -v $(pwd)/dashboards/:/etc/grafana/provisioning/dashboards/:ro \ + grafana/grafana:${GRAFANA_VERSION} + +else + docker start $CONTAINER_NAME +fi + + + +echo "Check state of the stack !" +readiness_check Prometheus localhost:9080/metrics & +readiness_check Loki localhost:3100/ready & +readiness_check Tempo localhost:3200/ready & +readiness_check OpenTelemetryCollector localhost:13133 & +readiness_check Grafana localhost:3000/api/health & + +wait +echo "Finished !" \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/observability/teardown-o11y-dev-stack.sh b/jdk_21_maven/cs/rest-gui/microcks/observability/teardown-o11y-dev-stack.sh new file mode 100644 index 000000000..ef00b7856 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/observability/teardown-o11y-dev-stack.sh @@ -0,0 +1,3 @@ + +docker rm -f o11y_prom o11y_loki o11y_tempo o11y_grafana o11y_otel +docker network rm o11y \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/pom.xml new file mode 100644 index 000000000..61eddc6ca --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/pom.xml @@ -0,0 +1,262 @@ + + + 4.0.0 + + io.github.microcks + microcks + pom + 1.12.2-SNAPSHOT + + commons/util + commons/model + commons/util-el + webapp + minions/async + distro + + + Microcks + Microcks: easy mocks for your microservices + http://microcks.github.io + + Microcks + http://microcks.github.io + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + microcks + Microcks Development Team + https://microcks.github.io/ + + + + + scm:git:https://github.com/microcks/microcks.git + scm:git:https://github.com/microcks/microcks.git + http://github.com/microcks/microcks/ + HEAD + + + + + oss-sonatype-staging + https://oss.sonatype.org/content/repositories/snapshots + + + oss-sonatype-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + UTF-8 + ${project.basedir} + microcks + 3.3.10 + 3.2.5 + microcks + https://sonarcloud.io + + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + true + true + release + deploy + + + + com.diffplug.spotless + spotless-maven-plugin + 2.39.0 + + + + + + ${multi-project.rootdir}/eclipse-formatter.xml + + + true + 3 + + + + + + + check + + compile + + + + + + + + + coverage + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/*Test.java + **/*IT.java + + + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + prepare-agent + + prepare-agent + + + + report + + report + + + + XML + + + + + + + + + + release-old + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + ${gpg.passphrase} + + + + sign-artifacts + verify + + sign + + + + + + + + + release + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + + attach-javadoc + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-source + + jar + + + + + + org.spdx + spdx-maven-plugin + 0.7.4 + + + build-spdx + package + + createSPDX + + + + + ${project.reporting.outputDirectory}/${project.artifactId}-${project.version}.spdx-sbom.json + http://spdx.org/spdxpackages/${project.artifactId}-${project.version} + + + + org.jreleaser + jreleaser-maven-plugin + 1.19.0 + false + + jreleaser.yml + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/post-release.sh b/jdk_21_maven/cs/rest-gui/microcks/post-release.sh new file mode 100644 index 000000000..f10acd5da --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/post-release.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# +# Copyright The Microcks Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +root_dir=$(pwd) + +# Need 2 arguments: first is version we just release, second is issue id for release. +if [[ $# -eq 2 ]]; then + # Package Helm chart. + cd install/kubernetes + helm package microcks + echo $root_dir + + # Get a local copy of microcks.io and move help package. + mkdir $root_dir/tmp && cd $root_dir/tmp + git clone https://github.com/microcks/microcks.io && cd microcks.io + mkdir ./static/helm/tmp + mv $root_dir/install/kubernetes/microcks-$1.tgz ./static/helm/tmp/microcks-$1.tgz + + # Update the index.yaml file of Help repo. + mv ./static/helm/index.yaml ./static/helm/index.yaml.backup + helm repo index static/helm/tmp --url https://microcks.io/helm --merge ./static/helm/index.yaml.backup + mv ./static/helm/tmp/index.yaml ./static/helm/index.yaml + mv ./static/helm/tmp/microcks-$1.tgz ./static/helm/microcks-$1.tgz + + # Add and commit before cleaning up things. + git add ./static/helm/microcks-$1.tgz + git commit -m 'microcks/microcks#'"$2"' chore: Release Helm chart for '"$1"'' ./static/helm/index.yaml ./static/helm/microcks-$1.tgz + git push origin master + + rm -rf ./static/helm/tmp + rm ./static/helm/index.yaml.backup + + # Get back to root. + cd $root_dir + rm -rf $root_dir/tmp +else + echo "post-release.sh must be called with as 1st argument. Example:" + echo "$ ./post-release.sh 1.7.1 837" +fi \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/pre-release.sh b/jdk_21_maven/cs/rest-gui/microcks/pre-release.sh new file mode 100644 index 000000000..fc4486529 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/pre-release.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# +# Copyright The Microcks Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Update container image version in docker-compose, podman-compose and helm chart files. +if [[ $# -eq 1 ]]; then + if [ "$(uname)" == "Darwin" ]; then + sed -i '' 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/docker-compose/docker-compose*.yml + sed -i '' 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/docker-compose/docker-compose*.yml + sed -i '' 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/docker-compose/*-addon.yml + sed -i '' 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/docker-compose/*-addon.yml + #sed -i '' 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/kubernetes/microcks/values.yaml + #sed -i '' 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/kubernetes/microcks/values.yaml + sed -i '' 's=tag: nightly=tag: '"$1"'=g' install/kubernetes/microcks/values.yaml + sed -i '' 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/podman-compose/podman-compose*.yml + sed -i '' 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/podman-compose/podman-compose*.yml + sed -i '' 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/podman-compose/*-addon.yml + sed -i '' 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/podman-compose/*-addon.yml + elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + sed -i 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/docker-compose/docker-compose*.yml + sed -i 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/docker-compose/docker-compose*.yml + sed -i 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/docker-compose/*-addon.yml + sed -i 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/docker-compose/*-addon.yml + #sed -i 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/kubernetes/microcks/values.yaml + #sed -i 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/kubernetes/microcks/values.yaml + sed -i 's=tag: nightly=tag: '"$1"'=g' install/kubernetes/microcks/values.yaml + sed -i 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/podman-compose/podman-compose*.yml + sed -i 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/podman-compose/podman-compose*.yml + sed -i 's=quay.io/microcks/microcks:nightly=quay.io/microcks/microcks:'"$1"'=g' install/podman-compose/*-addon.yml + sed -i 's=quay.io/microcks/microcks-async-minion:nightly=quay.io/microcks/microcks-async-minion:'"$1"'=g' install/podman-compose/*-addon.yml + fi +else + echo "pre-release.sh must be called with as 1st argument. Example:" + echo "$ ./pre-release.sh 1.7.1" +fi \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/prep-next-iteration.sh b/jdk_21_maven/cs/rest-gui/microcks/prep-next-iteration.sh new file mode 100644 index 000000000..3ae6c762b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/prep-next-iteration.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# +# Copyright The Microcks Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +root_dir=$(pwd) + +# Need 2 arguments: first is version we just release, second is container tag for next iteration. +if [[ $# -eq 2 ]]; then + # Update container image version in docker-compose, podman-compose and helm chart files. + if [ "$(uname)" == "Darwin" ]; then + sed -i '' 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/docker-compose/docker-compose*.yml + sed -i '' 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/docker-compose/docker-compose*.yml + sed -i '' 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/docker-compose/*-addon.yml + sed -i '' 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/docker-compose/*-addon.yml + #sed -i '' 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/kubernetes/microcks/values.yaml + #sed -i '' 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/kubernetes/microcks/values.yaml + sed -i '' 's=tag: '"$1"'=tag: '"$2"'=g' install/kubernetes/microcks/values.yaml + sed -i '' 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/podman-compose/podman-compose*.yml + sed -i '' 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/podman-compose/podman-compose*.yml + sed -i '' 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/podman-compose/*-addon.yml + sed -i '' 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/podman-compose/*-addon.yml + elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + sed -i 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/docker-compose/docker-compose.yml + sed -i 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/docker-compose/docker-compose*.yml + sed -i 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/docker-compose/*-addon.yml + sed -i 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/docker-compose/*-addon.yml + #sed -i 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/kubernetes/microcks/values.yaml + #sed -i 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/kubernetes/microcks/values.yaml + sed -i 's=tag: '"$1"'=tag: '"$2"'=g' install/kubernetes/microcks/values.yaml + sed -i 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/podman-compose/podman-compose*.yml + sed -i 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/podman-compose/podman-compose*.yml + sed -i 's=quay.io/microcks/microcks:'"$1"'=quay.io/microcks/microcks:'"$2"'=g' install/podman-compose/*-addon.yml + sed -i 's=quay.io/microcks/microcks-async-minion:'"$1"'=quay.io/microcks/microcks-async-minion:'"$2"'=g' install/podman-compose/*-addon.yml + fi +else + echo "prep-next-iteration.sh must be called with as 1st argument. Example:" + echo "$ ./prep-next-iteration.sh 1.7.1 nightly" +fi \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/.DS_Store b/jdk_21_maven/cs/rest-gui/microcks/samples/.DS_Store new file mode 100644 index 000000000..5008ddfcf Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/samples/.DS_Store differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/APIPastry-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/samples/APIPastry-openapi.yaml new file mode 100644 index 000000000..7334a81f9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/APIPastry-openapi.yaml @@ -0,0 +1,201 @@ +--- +openapi: 3.0.2 +info: + title: API Pastry - 2.0 + version: 2.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /pastry: + summary: Global operations on pastries + get: + tags: + - pastry + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pastry' + examples: + pastries_json: + value: + - name: Baba Rhum + description: Delicieux Baba au Rhum pas calorique du tout + size: L + price: 3.2 + status: available + - name: Divorces + description: Delicieux Divorces pas calorique du tout + size: M + price: 2.8 + status: available + - name: Tartelette Fraise + description: Delicieuse Tartelette aux Fraises fraiches + size: S + price: 2 + status: available + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries + /pastry/{name}: + summary: Specific operation on pastry + get: + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + Millefeuille: + value: Millefeuille + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.5 + status: available + Millefeuille: + value: + name: Millefeuille + description: Delicieux Millefeuille pas calorique du tout + size: L + price: 4.4 + status: available + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: |- + + Eclair Cafe + Delicieux Eclair au Cafe pas calorique du tout + M + 2.5 + available + + description: Pastry with specified name + operationId: GetPastryByName + summary: Get Pastry by name + description: Get Pastry by name + patch: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + price: 2.6 + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: "\n\t2.6\n" + required: true + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.6 + status: available + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: |- + + Eclair Cafe + Delicieux Eclair au Cafe pas calorique du tout + M + 2.6 + available + + description: Changed pastry + operationId: PatchPastry + summary: Patch existing pastry + parameters: + - name: name + description: pastry name + schema: + type: string + in: path + required: true +components: + schemas: + Pastry: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + description: + description: A short description of this pastry + type: string + size: + description: Size of pastry (S, M, L) + type: string + price: + format: double + description: Price (in USD) of this pastry + type: number + status: + description: Status in stock (available, out_of_stock) + type: string + example: + name: My Pastry + description: A short description os my pastry + size: M + price: 4.5 + status: available +tags: +- name: pastry + description: Pastry resource diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/BeerCatalogAPI-collection.json b/jdk_21_maven/cs/rest-gui/microcks/samples/BeerCatalogAPI-collection.json new file mode 100644 index 000000000..d3be1fccc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/BeerCatalogAPI-collection.json @@ -0,0 +1,369 @@ +{ + "variables": [], + "info": { + "name": "Beer Catalog API", + "_postman_id": "7194f912-d5f5-3ca0-cf75-8a0b912abc4e", + "description": "version=0.99 - An API for querying beer catalog of Acme Inc.", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "beer", + "description": "Folder for beer", + "item": [ + { + "name": "Get beer having name", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var expectedName = globals[\"name\"];", + "var jsonData = JSON.parse(responseBody);", + "", + "var schema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": { \"type\": \"string\", \"enum\": [expectedName] },", + " \"country\": { \"type\": \"string\" },", + " \"type\": { \"type\": \"string\" },", + " \"rating\": { \"type\": \"number\" },", + " \"status\": { \"type\": \"string\" }", + " }", + "};", + "", + "tests[\"Valid name in response\"] = tv4.validate(jsonData, schema);" + ] + } + } + ], + "request": { + "url": { + "raw": "http:///beer/:name", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + ":name" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "name", + "value": "" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Get beer having name" + }, + "response": [ + { + "id": "809e4ade-2462-454b-b8de-880f520e8c79", + "name": "Rodenbach", + "originalRequest": { + "url": { + "raw": "http:///beer/:name", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + ":name" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "name", + "value": "Rodenbach" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\"name\": \"Rodenbach\", \"country\": \"Belgium\", \"type\": \"Brown ale\", \"rating\": 4.2, \"status\": \"available\"}" + }, + { + "id": "b205add4-5386-4c79-a38c-61cd75a94435", + "name": "Weissbier", + "originalRequest": { + "url": { + "raw": "http:///beer/:name", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + ":name" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "name", + "value": "Weissbier" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"name\": \"Weissbier\",\n \"country\": \"Germany\",\n \"type\": \"Wheat\",\n \"rating\": 4.1,\n \"status\": \"out_of_stock\"\n}" + } + ] + }, + { + "name": "Get beers having status", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var expectedStatus = globals[\"status\"];", + "var jsonData = JSON.parse(responseBody);", + "", + "var schema = {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": { \"type\": \"string\" },", + " \"country\": { \"type\": \"string\" },", + " \"type\": { \"type\": \"string\" },", + " \"rating\": { \"type\": \"number\" },", + " \"status\": { \"type\": \"string\", \"enum\": [expectedStatus] }", + " }", + " }", + "};", + "", + "tests[\"Valid response\"] = tv4.validate(jsonData, schema);" + ] + } + } + ], + "request": { + "url": { + "raw": "http:///beer/findByStatus/:status", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + "findByStatus", + ":status" + ], + "query": [], + "variable": [ + { + "key": "status", + "value": "" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Get beers having status" + }, + "response": [ + { + "id": "2cef0eba-abe8-468e-8dc9-0731f5758b52", + "name": "Get available beers", + "originalRequest": { + "url": { + "raw": "http:///beer/findByStatus/:status", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + "findByStatus", + ":status" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "status", + "value": "available" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[{\"name\": \"Rodenbach\", \"country\": \"Belgium\", \"type\": \"Brown ale\", \"rating\": 4.2, \"status\": \"available\"},\n{\"name\": \"Westmalle Triple\", \"country\": \"Belgium\", \"type\": \"Trappist\", \"rating\": 3.8, \"status\": \"available\"}]" + }, + { + "id": "5b0ccc56-539d-428c-8334-d641a900b60e", + "name": "Get out_of_stock beers", + "originalRequest": { + "url": { + "raw": "http:///beer/findByStatus/:status", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + "findByStatus", + ":status" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "status", + "value": "out_of_stock" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[{\"name\": \"Weissbier\", \"country\": \"Germany\", \"type\": \"Wheat\", \"rating\": 4.1, \"status\": \"out_of_stock\"}]" + } + ] + }, + { + "name": "List beers within catalog", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "tests[\"Status code is OK\"] = (responseCode.code === 200 || responseCode.code === 404);" + ] + } + } + ], + "request": { + "url": { + "raw": "http:///beer?page={{page}}", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer" + ], + "query": [ + { + "key": "page", + "value": "{{page}}", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "List beers within catalog" + }, + "response": [ + { + "id": "6941bfe0-0e27-4ee6-a244-f03dfca6fe25", + "name": "List page 0", + "originalRequest": { + "url": { + "raw": "http:///beer?page=0", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer" + ], + "query": [ + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[\n {\n \"name\": \"Rodenbach\",\n \"country\": \"Belgium\",\n \"type\": \"Brown ale\",\n \"rating\": 4.2,\n \"status\": \"available\"\n },\n {\n \"name\": \"Westmalle Triple\",\n \"country\": \"Belgium\",\n \"type\": \"Trappist\",\n \"rating\": 3.8,\n \"status\": \"available\"\n },\n {\n \"name\": \"Weissbier\",\n \"country\": \"Germany\",\n \"type\": \"Wheat\",\n \"rating\": 4.1,\n \"status\": \"out_of_stock\"\n }\n]" + } + ] + } + ] + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/BeerCatalogAPI-swagger.json b/jdk_21_maven/cs/rest-gui/microcks/samples/BeerCatalogAPI-swagger.json new file mode 100644 index 000000000..0b42161f9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/BeerCatalogAPI-swagger.json @@ -0,0 +1,171 @@ +{ + "swagger": "2.0", + "info": { + "title": "Beer Catalog API", + "version": "0.99", + "description": "An API for querying beer catalog of Acme Inc.", + "contact": { + "name": "Laurent Broudoux", + "url": "http://github.com/lbroudoux", + "email": "laurent.broudoux@gmail.com" + }, + "license": { + "name": "MIT License", + "url": "https://opensource.org/licenses/MIT" + }, + "x-microcks": { + "labels": { + "domain": "beers", + "status": "beta", + "team": "Team A" + } + } + }, + "paths": { + "/beer/{name}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "beer" + ], + "responses": { + "200": { + "description": "Beer having requested name", + "schema": { + "$ref": "#/definitions/Beer" + }, + "examples": { + "application/json": "{\n \"name\": \"Rodenbach\",\n \"country\": \"Belgium\",\n \"type\": \"Fruit\",\n \"rating\": 4.3,\n \"status\": \"available\"\n}" + } + } + }, + "operationId": "GetBeer", + "summary": "Get beer having name", + "description": "Get beer having name" + }, + "parameters": [ + { + "name": "name", + "description": "Name of beer to retrieve", + "in": "path", + "required": true, + "type": "string" + } + ] + }, + "/beer/findByStatus/{status}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "beer" + ], + "responses": { + "200": { + "description": "List of beers having requested status", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Beer" + } + }, + "examples": { + "application/json": "[\n {\n \"name\": \"Rodenbach,\n \"country\": \"Belgium\",\n \"type\": \"Fruit\",\n \"rating\": 4.2,\n \"status\": \"available\"\n },\n {\n \"name\": \"Weissbier\",\n \"country\": \"Germany\",\n \"type\": \"Wheat\",\n \"rating\": 4.1,\n \"status\": \"available\"\n }\n]" + } + } + }, + "operationId": "FindBeersByStatus", + "summary": "Get beers having status", + "description": "Get beers having status" + }, + "parameters": [ + { + "name": "status", + "description": "Status of beers to retrieve", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "page", + "description": "Number of page to retrieve", + "in": "query", + "type": "number" + } + ] + }, + "/beer": { + "get": { + "tags": [ + "beer" + ], + "parameters": [ + { + "name": "myparam", + "description": "Description", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Array of beers", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Beer" + } + } + } + }, + "operationId": "ListBeers", + "summary": "List beers within catalog", + "description": "List beers within catalog" + }, + "parameters": [ + { + "name": "page", + "description": "Number of page to retrieve", + "in": "query", + "type": "number" + } + ] + } + }, + "definitions": { + "Beer": { + "properties": { + "name": { + "description": "Name of Beer", + "type": "string" + }, + "country": { + "description": "Origin country of Beer", + "type": "string" + }, + "type": { + "description": "Type of Beer", + "type": "string" + }, + "rating": { + "description": "Rating from customers", + "type": "number" + }, + "status": { + "description": "Stock status", + "type": "string" + } + } + } + }, + "tags": [ + { + "name": "beer", + "description": "Beer resource" + } + ] + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/HelloAPI-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloAPI-soapui-project.xml new file mode 100644 index 000000000..30b47aef8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloAPI-soapui-project.xml @@ -0,0 +1,28 @@ + +This is a sample Hello APIhttp://api.example.comhttp://lbroudoux-OSX.local:8089/Get a new greeting message +Get a new greeting messagenameQUERYName of people to greetapplication/xml200application/json200application/xml405application/json405http://api.example.comNo AuthorizationTestSuite generated for REST Service [Hello API]SEQUENTIALTestCase generated for REST Resource [/hello] located at [/hello]<xml-fragment/>http://lbroudoux-OSX.local:8089/http://lbroudoux-OSX.local/v1/hello200greetingHello David !falsefalsefalse200No Authorization<xml-fragment/>UTF-8http://lbroudoux-OSX.local:8089/http://lbroudoux-OSX.local/v1/hello200greetingHello Gavin !falsefalsefalse200No Authorizationversion0.8Unknwon ResponseSCRIPT// Script dispatcher is used to select a response based on the incoming request. +// Here are few examples showing how to match based on path, query param, header and body + +// Match based on query parameter +def queryString = mockRequest.getRequest().getQueryString() +log.info "QueryString: " + queryString + +if( queryString.contains("David") ) +{ + // return the name of the response you want to dispatch + return "David Response" +} +else if( queryString.contains("Gavin") ) +{ + // return the name of the response you want to dispatch + return "Gavin Response" +} +else return "Unknown Response"{ + 'name':'David', + 'greeting':'Hello David !' +} +{ + 'name':'Gavin', + 'greeting':'Hello Gavin !' +} + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/HelloAPI-swagger.json b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloAPI-swagger.json new file mode 100644 index 000000000..ad3f84e08 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloAPI-swagger.json @@ -0,0 +1,68 @@ +{ + "swagger":"2.0", + "info":{ + "description":"This is a sample Hello API", + "version":"0.9", + "title":"Hello API", + "termsOfService":"http://swagger.io/terms/", + "contact":{ + "email":"lbroudoux@github.io" + }, + "license":{ + "name":"Apache 2.0", + "url":"http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host":"api.example.com", + "basePath":"/v1", + "tags":[ + { + "name":"hello", + "description":"Simple Hello World sample" + }, + { + "name":"sample", + "description":"Simple Hello World sample" + } + ], + "schemes":["http"], + "paths":{ + "/hello":{ + "get":{ + "tags":["hello"], + "summary":"Get a new greeting message", + "description":"Get a new greeting message", + "operationId":"getGreeting", + "produces":["application/xml","application/json"], + "parameters":[ + { + "name":"name", + "in":"query", + "description":"Name of people to greet", + "required":true, + "type":"string" + } + ], + "responses":{ + "200":{ + "description":"successful operation", + "schema":{"$ref":"#/definitions/ApiResponse"} + }, + "405":{ + "description":"Invalid input" + } + } + } + } + }, + "definitions":{ + "ApiResponse":{ + "type":"object", + "properties":{ + "name":{"type":"string"}, + "greeting":{"type":"string"} + } + } + }, + "externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"} +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-multi-operations-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-multi-operations-soapui-project.xml new file mode 100644 index 000000000..3eeafc8e5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-multi-operations-soapui-project.xml @@ -0,0 +1,217 @@ + +file:/Users/lbroudou/Development/github/microcks/samples/HelloService-multi-operations.wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://localhost:8080/services/HelloService<entry key="Authorization" value="Bearer 123" xmlns="http://eviware.com/soapui/config"/>UTF-8http://localhost:8088/multiOpMockService + + + + Andrew + + +]]>No Authorization<xml-fragment/>UTF-8http://localhost:8088/multiOpMockService + + + + Andrew + + +]]>No AuthorizationSEQUENTIALHelloServicesayHello<xml-fragment/>UTF-8http://localhost:8080/services/HelloService + + + + Andrew + + +]]>No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://localhost:8080/services/HelloService + + + + Karla + + +]]>No AuthorizationHelloServicesayGoodbye<xml-fragment/>UTF-8http://localhost:8080/services/HelloService + + + + Andrew + + +]]>No AuthorizationHelloServicesayGoodbye<xml-fragment/>UTF-8http://localhost:8080/services/HelloService + + + + Karla + + +]]>No Authorizationfalseversion0.99Response 1SCRIPTimport com.eviware.soapui.support.XmlHolder +def holder = new XmlHolder( mockRequest.requestContent ) +def name = holder["//name"] +log.info "name: " + name + +def authHeaders = mockRequest.getRequestHeaders().get("authorization") +log.info "authHeaders: " + authHeaders + +if (authHeaders!=null && authHeaders[0].contains("Bearer") ){ + // return the name of the response you want to dispatch + return "Karla Response" +} + + +if (name == "Andrew"){ + return "Andrew Response" +} else if (name == "Karla"){ + return "Karla Response" +} else { + return "Karla Response" +} + + + + + Goodbye Andrew + + +]]> + + + + Goodbye Karla + + +]]>Response 1SCRIPTimport com.eviware.soapui.support.XmlHolder +def holder = new XmlHolder( mockRequest.requestContent ) +def name = holder["//name"] +log.info "name: " + name + +def authHeaders = mockRequest.getRequestHeaders().get("authorization") +log.info "authHeaders: " + authHeaders + +if (authHeaders!=null && authHeaders[0].contains("Bearer") ){ + // return the name of the response you want to dispatch + return "Karla Response" +} + + +if (name == "Andrew"){ + return "Andrew Response" +} else if (name == "Karla"){ + return "Karla Response" +} else { + return "Karla Response" +} + + + + + Hello Andrew + + +]]> + + + + Hello Karla + + +]]> \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-multi-operations.wsdl b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-multi-operations.wsdl new file mode 100644 index 000000000..3d9c3de51 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-multi-operations.wsdl @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-soapui-project.xml new file mode 100644 index 000000000..d6c763cef --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService-soapui-project.xml @@ -0,0 +1,137 @@ + +file:/Users/lbroudou/Development/github/microcks/samples/HelloService.wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://lbroudoux-OSX.local:8088/mockHelloServicehttp://localhost:8080/services/HelloService<xml-fragment/>UTF-8http://localhost:8080/soap/HelloService Mock/0.9/ + + + + Karla + + +]]>No AuthorizationSEQUENTIALHelloServicesayHello<xml-fragment/>UTF-8http://:8088/mockHelloService + + + + Andrew + + +]]>200declare namespace ser='http://www.example.com/hello'; +//ser:sayHelloResponse/sayHello<sayHello>Hello Andrew !</sayHello>false500No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://localhost:8080/services/HelloService + + + + Karla + + +]]>No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://lbroudoux-OSX.local:8088/mockHelloService + + + + World + + +]]>500No Authorizationfalseversion0.9Response 1SCRIPTimport com.eviware.soapui.support.XmlHolder +def holder = new XmlHolder( mockRequest.requestContent ) +def name = holder["//name"] + +if (name == "Andrew"){ + return "Andrew Response" +} else if (name == "Karla"){ + return "Karla Response" +} else { + return "World Response" +} + + + + + Hello Andrew ! + + +]]> + + + + Hello Karla ! + + +]]> + + + + + soapenv:Sender + + rpc:BadArguments + + + + Processing error + + + + 999 + + + + +]]>Andrewdeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameAndrewAndrew ResponseKarladeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameKarlaKarla Response \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.metadata.yml b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.metadata.yml new file mode 100644 index 000000000..642fb0920 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.metadata.yml @@ -0,0 +1,20 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIMetadata +metadata: + name: io.github.microcks.grpc.hello.v1.HelloService + version: v1 + labels: + domain: samples + status: GA +operations: + 'greeting': + dispatcher: JSON_BODY + dispatcherRules: |- + { + "exp": "/firstname", + "operator": "equals", + "cases": { + "Laurent": "Laurent", + "default": "John" + } + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.postman.json b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.postman.json new file mode 100644 index 000000000..abd56767b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.postman.json @@ -0,0 +1,99 @@ +{ + "info": { + "_postman_id": "448f2755-4b3c-45a7-b6d4-af16d8110e9d", + "name": "io.github.microcks.grpc.hello.v1.HelloService", + "description": "version=v1 - This collection holds mock messages for HelloService v1 GRPC", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "greeting", + "item": [ + { + "name": "greeting", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http:///greeting", + "protocol": "http", + "path": [ + "greeting" + ] + } + }, + "response": [ + { + "name": "John", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstname\": \"John\",\n \"lastname\": \"Doe\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http:///greeting", + "protocol": "http", + "path": [ + "greeting" + ] + } + }, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"greeting\": \"Hello John Doe !\"\n}" + }, + { + "name": "Laurent", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstname\": \"Laurent\",\n \"lastname\": \"Broudoux\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http:///greeting", + "protocol": "http", + "path": [ + "greeting" + ] + } + }, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"greeting\": \"Hello Laurent Broudoux !\"\n}" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.wsdl b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.wsdl new file mode 100644 index 000000000..d3b2e759e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/HelloService.wsdl @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/PetstoreAPI-collection.json b/jdk_21_maven/cs/rest-gui/microcks/samples/PetstoreAPI-collection.json new file mode 100644 index 000000000..fee392a3d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/PetstoreAPI-collection.json @@ -0,0 +1,412 @@ +{ + "variables": [], + "info": { + "name": "Petstore API", + "_postman_id": "c0bc3513-c83a-8b5d-bbff-1a4a0dc001d7", + "description": "version=1.0 - This is a PetStore API description", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "pet", + "description": "", + "item": [ + { + "name": "Finds Pets by status", + "request": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=998bac0775b1d5f588e0a6ca7c11b852&status=available", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true, + "description": "" + }, + { + "key": "status", + "value": "available", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "" + }, + "response": [ + { + "id": "c7e562f0-3d87-4925-96d1-c38e0dc4c0d6", + "name": "available response", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=70f735676ec46351c6699c4bb767878a&status=available", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "user_key", + "value": "70f735676ec46351c6699c4bb767878a", + "equals": true, + "description": "" + }, + { + "key": "status", + "value": "available", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "" + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "" + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "" + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:31:45 GMT", + "description": "" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.15.1", + "description": "" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "" + }, + { + "name": "transfer-encoding", + "key": "transfer-encoding", + "value": "chunked", + "description": "" + } + ], + "cookie": [], + "responseTime": 1466, + "body": "[\n {\n \"id\": 190192062,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192063,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192285,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192654,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192671,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192727,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192736,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192768,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192878,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192907,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190193000,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": -98125093,\n \"category\": {\n \"id\": -517488397,\n \"name\": \"EJvNbK\"\n },\n \"name\": \"LuEfMZATrHz\",\n \"photoUrls\": [\n \"XCXOVVkaxa\",\n \"gNwYqHEmC\",\n \"nvCvphDeuqztysUBNed\",\n \"W\",\n \"vmrxRIViyXqumolLIeoB\",\n \"JRqHVxk\",\n \"tCUGbegVHoXajm\",\n \"UiHppQn\"\n ],\n \"tags\": [\n {\n \"id\": 727599428,\n \"name\": \"RemggEDzxPljbrlktdWf\"\n },\n {\n \"id\": 1987753751,\n \"name\": \"zWqdKAGHMmhPPlomljaNtuvm\"\n },\n {\n \"id\": 1251632392,\n \"name\": \"BAgtgtKOxZGdsS\"\n },\n {\n \"id\": -1813025208,\n \"name\": \"OkKxtfAkCMEICbbQDVPi\"\n },\n {\n \"id\": -730110346,\n \"name\": \"WshDF\"\n },\n {\n \"id\": 2100951153,\n \"name\": \"yxUFSknQEleIAQCoocl\"\n },\n {\n \"id\": -2135188117,\n \"name\": \"M\"\n },\n {\n \"id\": 1352243140,\n \"name\": \"koKHsjysHXW\"\n },\n {\n \"id\": 1696778814,\n \"name\": \"KaihiyarcZkIzkkquWPZ\"\n },\n {\n \"id\": 659492963,\n \"name\": \"xqIzulcBPzWMyUpQwQK\"\n },\n {\n \"id\": -2118372841,\n \"name\": \"naYFGuHmqDqOpfHH\"\n }\n ],\n \"status\": \"available\"\n }\n]" + } + ] + }, + { + "name": "Find pet by ID", + "request": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/:petId?user_key={{user_key}}", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "{{user_key}}", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "key": "petId", + "value": "" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "" + }, + "response": [ + { + "id": "05486047-439f-408d-97e7-a9925107a2a1", + "name": "get-2", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.staging.apicast.io:443/v2/pet/:petId?user_key=70f735676ec46351c6699c4bb767878a", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "staging", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId", + "" + ], + "query": [ + { + "key": "user_key", + "value": "70f735676ec46351c6699c4bb767878a", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "key": "petId", + "value": "2" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "" + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "" + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "" + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "" + }, + { + "name": "Content-Length", + "key": "Content-Length", + "value": "137", + "description": "" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:18 GMT", + "description": "" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "" + } + ], + "cookie": [], + "responseTime": 233, + "body": "{\"id\":2,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"}" + }, + { + "id": "187fb73f-edbb-47d4-89ae-7e3e4a8cfcbd", + "name": "get-1", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/:petId?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "key": "petId", + "value": "1" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "", + "_postman_previewtype": "parsed", + "header": [ + { + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Headers" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Methods" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Origin" + }, + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "" + }, + { + "key": "Content-Length", + "value": "0", + "description": "", + "type": "text", + "name": "Content-Length" + }, + { + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "", + "type": "text", + "name": "Server" + }, + { + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:22 GMT", + "description": "", + "type": "text", + "name": "Date" + }, + { + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "", + "type": "text", + "name": "X-Powered-By" + }, + { + "key": "Connection", + "value": "keep-alive", + "description": "", + "type": "text", + "name": "Connection" + } + ], + "cookie": [], + "responseTime": 0, + "body": "" + } + ] + } + ] + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/PetstoreAPI-swagger.json b/jdk_21_maven/cs/rest-gui/microcks/samples/PetstoreAPI-swagger.json new file mode 100644 index 000000000..7ed14107e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/PetstoreAPI-swagger.json @@ -0,0 +1,382 @@ +{ + "swagger":"2.0", + "info":{ + "description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "version":"1.0.0", + "title":"Swagger Petstore", + "termsOfService":"http://swagger.io/terms/", + "contact":{ + "email":"apiteam@swagger.io" + }, + "license":{ + "name":"Apache 2.0", + "url":"http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host":"petstore.swagger.io", + "basePath":"/v2", + "tags":[ + { + "name":"pet", + "description":"Everything about your Pets", + "externalDocs":{ + "description":"Find out more", + "url":"http://swagger.io" + } + }, + { + "name":"store", + "description":"Access to Petstore orders" + }, + { + "name":"user", + "description":"Operations about user", + "externalDocs":{ + "description":"Find out more about our store", + "url":"http://swagger.io" + } + } + ], + "schemes":["http"], + "paths":{ + "/pet":{ + "post":{ + "tags":["pet"], + "summary":"Add a new pet to the store", + "description":"", + "operationId":"addPet", + "consumes":["application/json","application/xml"], + "produces":["application/xml","application/json"], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Pet object that needs to be added to the store", + "required":true, + "schema":{"$ref":"#/definitions/Pet"} + } + ], + "responses":{ + "405":{ + "description":"Invalid input" + } + }, + "security":[{"petstore_auth":["write:pets","read:pets"]}] + }, + "put":{ + "tags":["pet"], + "summary":"Update an existing pet", + "description":"", + "operationId":"updatePet", + "consumes":["application/json","application/xml"], + "produces":["application/xml","application/json"], + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Pet object that needs to be added to the store", + "required":true, + "schema":{"$ref":"#/definitions/Pet"} + } + ], + "responses":{ + "400":{ + "description":"Invalid ID supplied" + }, + "404":{ + "description":"Pet not found" + }, + "405":{ + "description":"Validation exception" + } + }, + "security":[{"petstore_auth":["write:pets","read:pets"]}] + } + }, + "/pet/findByStatus":{ + "get":{ + "tags":["pet"],"summary":"Finds Pets by status", + "description":"Multiple status values can be provided with comma separated strings", + "operationId":"findPetsByStatus", + "produces":["application/xml","application/json"], + "parameters":[{"name":"status","in":"query","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"type":"string","enum":["available","pending","sold"],"default":"available"},"collectionFormat":"multi"}], + "responses":{ + "200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}}, + "400":{"description":"Invalid status value"} + }, + "security":[{"petstore_auth":["write:pets","read:pets"]}] + } + }, + "/pet/findByTags":{ + "get":{ + "tags":["pet"],"summary":"Finds Pets by tags", + "description":"Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId":"findPetsByTags", + "produces":["application/xml","application/json"], + "parameters":[{"name":"tags","in":"query","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"},"collectionFormat":"multi"}], + "responses":{ + "200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}}, + "400":{"description":"Invalid tag value"} + }, + "security":[{"petstore_auth":["write:pets","read:pets"]}],"deprecated":true} + }, + "/pet/{petId}":{ + "get":{ + "tags":["pet"],"summary":"Find pet by ID","description":"Returns a single pet", + "operationId":"getPetById", + "produces":["application/xml","application/json"], + "parameters":[{"name":"petId","in":"path","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}], + "responses":{ + "200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}}, + "400":{"description":"Invalid ID supplied"}, + "404":{"description":"Pet not found"} + }, + "security":[{"api_key":[]}] + }, + "post":{ + "tags":["pet"],"summary":"Updates a pet in the store with form data","description":"", + "operationId":"updatePetWithForm", + "consumes":["application/x-www-form-urlencoded"], + "produces":["application/xml","application/json"], + "parameters":[{"name":"petId","in":"path","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"name":"name","in":"formData","description":"Updated name of the pet","required":false,"type":"string"},{"name":"status","in":"formData","description":"Updated status of the pet","required":false,"type":"string"}], + "responses":{ + "405":{"description":"Invalid input"} + }, + "security":[{"petstore_auth":["write:pets","read:pets"]}] + }, + "delete":{ + "tags":["pet"],"summary":"Deletes a pet","description":"", + "operationId":"deletePet", + "produces":["application/xml","application/json"], + "parameters":[{"name":"api_key","in":"header","required":false,"type":"string"},{"name":"petId","in":"path","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}], + "responses":{ + "400":{"description":"Invalid ID supplied"}, + "404":{"description":"Pet not found"} + }, + "security":[{"petstore_auth":["write:pets","read:pets"]}] + } + }, + "/pet/{petId}/uploadImage":{ + "post":{ + "tags":["pet"],"summary":"uploads an image","description":"","operationId":"uploadFile", + "consumes":["multipart/form-data"], + "produces":["application/json"], + "parameters":[{"name":"petId","in":"path","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"name":"additionalMetadata","in":"formData","description":"Additional data to pass to server","required":false,"type":"string"},{"name":"file","in":"formData","description":"file to upload","required":false,"type":"file"}], + "responses":{ + "200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}} + }, + "security":[{"petstore_auth":["write:pets","read:pets"]}] + } + }, + "/store/inventory":{ + "get":{ + "tags":["store"],"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities", + "operationId":"getInventory", + "produces":["application/json"], + "parameters":[], + "responses":{ + "200":{"description":"successful operation","schema":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}}} + }, + "security":[{"api_key":[]}] + } + }, + "/store/order":{ + "post":{ + "tags":["store"],"summary":"Place an order for a pet","description":"","operationId":"placeOrder", + "produces":["application/xml","application/json"], + "parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}], + "responses":{ + "200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}}, + "400":{"description":"Invalid Order"} + } + } + }, + "/store/order/{orderId}":{ + "get":{ + "tags":["store"],"summary":"Find purchase order by ID", + "description":"For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId":"getOrderById", + "produces":["application/xml","application/json"], + "parameters":[{"name":"orderId","in":"path","description":"ID of pet that needs to be fetched","required":true,"type":"integer","maximum":10.0,"minimum":1.0,"format":"int64"}], + "responses":{ + "200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}}, + "400":{"description":"Invalid ID supplied"}, + "404":{"description":"Order not found"} + } + }, + "delete":{ + "tags":["store"],"summary":"Delete purchase order by ID", + "description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId":"deleteOrder", + "produces":["application/xml","application/json"], + "parameters":[{"name":"orderId","in":"path","description":"ID of the order that needs to be deleted","required":true,"type":"integer","minimum":1.0,"format":"int64"}], + "responses":{ + "400":{"description":"Invalid ID supplied"}, + "404":{"description":"Order not found"} + } + } + }, + "/user":{ + "post":{ + "tags":["user"],"summary":"Create user", + "description":"This can only be done by the logged in user.","operationId":"createUser", + "produces":["application/xml","application/json"], + "parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}], + "responses":{ + "default":{"description":"successful operation"} + } + } + }, + "/user/createWithArray":{ + "post":{ + "tags":["user"],"summary":"Creates list of users with given input array","description":"","operationId":"createUsersWithArrayInput", + "produces":["application/xml","application/json"], + "parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"type":"array","items":{"$ref":"#/definitions/User"}}}], + "responses":{ + "default":{"description":"successful operation"} + } + } + }, + "/user/createWithList":{ + "post":{ + "tags":["user"],"summary":"Creates list of users with given input array","description":"","operationId":"createUsersWithListInput", + "produces":["application/xml","application/json"], + "parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"type":"array","items":{"$ref":"#/definitions/User"}}}], + "responses":{ + "default":{"description":"successful operation"} + } + } + }, + "/user/login":{ + "get":{ + "tags":["user"],"summary":"Logs user into the system","description":"","operationId":"loginUser", + "produces":["application/xml","application/json"], + "parameters":[{"name":"username","in":"query","description":"The user name for login","required":true,"type":"string"},{"name":"password","in":"query","description":"The password for login in clear text","required":true,"type":"string"}], + "responses":{ + "200":{"description":"successful operation","schema":{"type":"string"},"headers":{"X-Rate-Limit":{"type":"integer","format":"int32","description":"calls per hour allowed by the user"},"X-Expires-After":{"type":"string","format":"date-time","description":"date in UTC when token expires"}}}, + "400":{"description":"Invalid username/password supplied"} + } + } + }, + "/user/logout":{ + "get":{ + "tags":["user"],"summary":"Logs out current logged in user session","description":"","operationId":"logoutUser", + "produces":["application/xml","application/json"], + "parameters":[], + "responses":{ + "default":{"description":"successful operation"} + } + } + }, + "/user/{username}":{ + "get":{ + "tags":["user"],"summary":"Get user by user name","description":"","operationId":"getUserByName", + "produces":["application/xml","application/json"], + "parameters":[{"name":"username","in":"path","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}], + "responses":{ + "200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}}, + "400":{"description":"Invalid username supplied"}, + "404":{"description":"User not found"} + } + }, + "put":{ + "tags":["user"],"summary":"Updated user","description":"This can only be done by the logged in user.","operationId":"updateUser", + "produces":["application/xml","application/json"], + "parameters":[{"name":"username","in":"path","description":"name that need to be updated","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}], + "responses":{ + "400":{"description":"Invalid user supplied"}, + "404":{"description":"User not found"} + } + }, + "delete":{ + "tags":["user"],"summary":"Delete user","description":"This can only be done by the logged in user.","operationId":"deleteUser", + "produces":["application/xml","application/json"], + "parameters":[{"name":"username","in":"path","description":"The name that needs to be deleted","required":true,"type":"string"}], + "responses":{ + "400":{"description":"Invalid username supplied"}, + "404":{"description":"User not found"} + } + } + } + }, + "securityDefinitions":{ + "petstore_auth":{ + "type":"oauth2","authorizationUrl":"http://petstore.swagger.io/oauth/dialog","flow":"implicit", + "scopes":{"write:pets":"modify pets in your account","read:pets":"read your pets"} + }, + "api_key":{"type":"apiKey","name":"api_key","in":"header"} + }, + "definitions":{ + "Order":{ + "type":"object", + "properties":{ + "id":{"type":"integer","format":"int64"}, + "petId":{"type":"integer","format":"int64"}, + "quantity":{"type":"integer","format":"int32"}, + "shipDate":{"type":"string","format":"date-time"}, + "status":{"type":"string","description":"Order Status","enum":["placed","approved","delivered"]}, + "complete":{"type":"boolean","default":false} + }, + "xml":{"name":"Order"} + }, + "Category":{ + "type":"object", + "properties":{ + "id":{"type":"integer","format":"int64"}, + "name":{"type":"string"} + }, + "xml":{"name":"Category"} + }, + "User":{ + "type":"object", + "properties":{ + "id":{"type":"integer","format":"int64"}, + "username":{"type":"string"}, + "firstName":{"type":"string"}, + "lastName":{"type":"string"}, + "email":{"type":"string"}, + "password":{"type":"string"}, + "phone":{"type":"string"}, + "userStatus":{"type":"integer","format":"int32","description":"User Status"} + }, + "xml":{"name":"User"} + }, + "Tag":{ + "type":"object", + "properties":{ + "id":{"type":"integer","format":"int64"}, + "name":{"type":"string"} + }, + "xml":{"name":"Tag"} + }, + "Pet":{ + "type":"object","required":["name","photoUrls"], + "properties":{ + "id":{"type":"integer","format":"int64"}, + "category":{"$ref":"#/definitions/Category"}, + "name":{"type":"string","example":"doggie"}, + "photoUrls":{ + "type":"array", + "xml":{"name":"photoUrl","wrapped":true}, + "items":{"type":"string"} + }, + "tags":{ + "type":"array", + "xml":{"name":"tag","wrapped":true}, + "items":{"$ref":"#/definitions/Tag"} + }, + "status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]} + }, + "xml":{"name":"Pet"} + }, + "ApiResponse":{ + "type":"object", + "properties":{ + "code":{"type":"integer","format":"int32"}, + "type":{"type":"string"}, + "message":{"type":"string"} + } + } + }, + "externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"} +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-googlepubsub.yml b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-googlepubsub.yml new file mode 100644 index 000000000..ca1db0257 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-googlepubsub.yml @@ -0,0 +1,74 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.20 + description: Sample AsyncAPI for user signedup events +defaultContentType: application/json +channels: + userSignedUpChannel: + address: user/signedup + messages: + receivedUserSignedUp.message: + description: An event describing that a user just signed up. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + type: object + additionalProperties: false + properties: + id: + type: string + sendAt: + type: string + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + {"my-app-header": 23} + payload: + {"id": "{{randomString(32)}}", "sendAt": "{{now()}}", + "fullName": "Laurent Broudoux", "email": "laurent@microcks.io", + "age": 41} + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + payload: + id: '{{randomString(32)}}' + sendAt: '{{now()}}' + fullName: John Doe + email: john@microcks.io + age: 36 + description: The topic on which user signed up events may be consumed + bindings: + googlepubsub: + schemaSettings: + encoding: json + name: projects/my-project/topics/my-topic +operations: + receivedUserSignedUp: + action: receive + channel: + $ref: '#/channels/userSignedUpChannel' + summary: Receive informations about user signed up + messages: + - $ref: '#/channels/userSignedUpChannel/messages/receivedUserSignedUp.message' +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-nats.yml b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-nats.yml new file mode 100644 index 000000000..8b79cc3e6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-nats.yml @@ -0,0 +1,72 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup-v3' +info: + title: User signed-up API + version: 0.1.30 + description: Sample AsyncAPI for user signedup events +defaultContentType: application/json +channels: + userSignedUpChannel: + address: user/signedup + messages: + receivedUserSignedUp.message: + description: An event describing that a user just signed up. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + type: object + additionalProperties: false + properties: + id: + type: string + sendAt: + type: string + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + {"my-app-header": 23} + payload: + {"id": "{{randomString(32)}}", "sendAt": "{{now()}}", + "fullName": "Laurent Broudoux", "email": "laurent@microcks.io", + "age": 41} + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + payload: + id: '{{randomString(32)}}' + sendAt: '{{now()}}' + fullName: John Doe + email: john@microcks.io + age: 36 + description: The topic on which user signed up events may be consumed +operations: + receivedUserSignedUp: + action: receive + bindings: + nats: + queue: my-nats-queue + channel: + $ref: '#/channels/userSignedUpChannel' + summary: Receive informations about user signed up + messages: + - $ref: '#/channels/userSignedUpChannel/messages/receivedUserSignedUp.message' +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-sns.yml b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-sns.yml new file mode 100644 index 000000000..e059a689b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-sns.yml @@ -0,0 +1,78 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.50 + description: Sample AsyncAPI for user signedup events +defaultContentType: application/json +channels: + userSignedUpChannel: + address: user/signedup + messages: + receivedUserSignedUp.message: + description: An event describing that a user just signed up. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + type: object + additionalProperties: false + properties: + id: + type: string + sendAt: + type: string + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + {"my-app-header": 23} + payload: + {"id": "{{randomString(32)}}", "sendAt": "{{now()}}", + "fullName": "Laurent Broudoux", "email": "laurent@microcks.io", + "age": 41} + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + payload: + id: '{{randomString(32)}}' + sendAt: '{{now()}}' + fullName: John Doe + email: john@microcks.io + age: 36 + description: The topic on which user signed up events may be consumed +operations: + receivedUserSignedUp: + action: receive + channel: + $ref: '#/channels/userSignedUpChannel' + summary: Receive informations about user signed up + messages: + - $ref: '#/channels/userSignedUpChannel/messages/receivedUserSignedUp.message' + bindings: + sns: + topic: + name: my-sns-topic + consumers: + - protocol: http + endpoint: + url: http://my-endpoint + rawMessageDelivery: true +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-sqs.yml b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-sqs.yml new file mode 100644 index 000000000..ba9da2646 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-sqs.yml @@ -0,0 +1,73 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.40 + description: Sample AsyncAPI for user signedup events +defaultContentType: application/json +channels: + userSignedUpChannel: + address: user/signedup + messages: + receivedUserSignedUp.message: + description: An event describing that a user just signed up. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + type: object + additionalProperties: false + properties: + id: + type: string + sendAt: + type: string + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + {"my-app-header": 23} + payload: + {"id": "{{randomString(32)}}", "sendAt": "{{now()}}", + "fullName": "Laurent Broudoux", "email": "laurent@microcks.io", + "age": 41} + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + payload: + id: '{{randomString(32)}}' + sendAt: '{{now()}}' + fullName: John Doe + email: john@microcks.io + age: 36 + description: The topic on which user signed up events may be consumed +operations: + receivedUserSignedUp: + action: receive + channel: + $ref: '#/channels/userSignedUpChannel' + summary: Receive informations about user signed up + bindings: + sqs: + queues: + - name: my-sqs-queue + messages: + - $ref: '#/channels/userSignedUpChannel/messages/receivedUserSignedUp.message' +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-ws.yml b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-ws.yml new file mode 100644 index 000000000..a005929cf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi-ws.yml @@ -0,0 +1,72 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.50 + description: Sample AsyncAPI for user signedup events +defaultContentType: application/json +channels: + userSignedUpChannel: + address: user/signedup + description: The topic on which user signed up events may be consumed + bindings: + ws: + method: POST + messages: + receivedUserSignedUp.message: + description: An event describing that a user just signed up. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + type: object + additionalProperties: false + properties: + id: + type: string + sendAt: + type: string + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + {"my-app-header": 23} + payload: + {"id": "{{randomString(32)}}", "sendAt": "{{now()}}", + "fullName": "Laurent Broudoux", "email": "laurent@microcks.io", + "age": 41} + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + payload: + id: '{{randomString(32)}}' + sendAt: '{{now()}}' + fullName: John Doe + email: john@microcks.io + age: 36 +operations: + consumeUserSignedUp: + action: receive + channel: + $ref: '#/channels/userSignedUpChannel' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/userSignedUpChannel/messages/receivedUserSignedUp.message' +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi.yml b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi.yml new file mode 100644 index 000000000..0b84c0df8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/UserSignedUpAPI-asyncapi.yml @@ -0,0 +1,69 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.1 + description: Sample AsyncAPI for user signedup events +defaultContentType: application/json +channels: + userSignedUpChannel: + address: user/signedup + description: The topic on which user signed up events may be consumed + messages: + receivedUserSignedUp.message: + description: An event describing that a user just signed up. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + type: object + additionalProperties: false + properties: + id: + type: string + sendAt: + type: string + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + {"my-app-header": 23} + payload: + {"id": "{{randomString(32)}}", "sendAt": "{{now()}}", + "fullName": "Laurent Broudoux", "email": "laurent@microcks.io", + "age": 41} + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + payload: + id: '{{randomString(32)}}' + sendAt: '{{now()}}' + fullName: John Doe + email: john@microcks.io + age: 36 +operations: + consumeUserSignedUp: + action: receive + channel: + $ref: '#/channels/userSignedUpChannel' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/userSignedUpChannel/messages/receivedUserSignedUp.message' +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/api-pastries-0.0.1.har b/jdk_21_maven/cs/rest-gui/microcks/samples/api-pastries-0.0.1.har new file mode 100644 index 000000000..4890b176e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/api-pastries-0.0.1.har @@ -0,0 +1,929 @@ +{ + "log": { + "version": "1.2", + "comment": "microcksId: API Pastries:0.0.2 \n apiPrefix: /rest/API+Pastries/0.0.1", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [ + { + "startedDateTime": "2023-08-16T13:03:11.877Z", + "id": "page_6", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=L", + "pageTimings": { + "onContentLoad": 39.11599999992177, + "onLoad": 38.55900000780821 + } + }, + { + "startedDateTime": "2023-08-17T08:42:25.550Z", + "id": "page_7", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=S", + "pageTimings": { + "onContentLoad": 47.222999986843206, + "onLoad": 47.03399998834357 + } + }, + { + "startedDateTime": "2023-08-17T08:43:27.255Z", + "id": "page_8", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=M", + "pageTimings": { + "onContentLoad": 32.441999996080995, + "onLoad": 32.24200000113342 + } + }, + { + "startedDateTime": "2023-08-17T08:43:38.923Z", + "id": "page_9", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Millefeuille", + "pageTimings": { + "onContentLoad": 32.896999997319654, + "onLoad": 32.69799999543466 + } + }, + { + "startedDateTime": "2023-08-17T08:43:48.176Z", + "id": "page_10", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Eclair+Cafe", + "pageTimings": { + "onContentLoad": 30.78599998843856, + "onLoad": 30.63899998960551 + } + } + ], + "entries": [ + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "275560", + "pageref": "page_6", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=L", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "L" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "256" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Wed, 16 Aug 2023 13:03:11 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 256, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Baba Rhum\",\"description\":\"Delicieux Baba au Rhum pas calorique du tout\",\"size\":\"L\",\"price\":3.2,\"status\":\"available\"},{\"name\":\"Millefeuille\",\"description\":\"Delicieux Millefeuille pas calorique du tout\",\"size\":\"L\",\"price\":4.4,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 257, + "_transferSize": 514, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T13:03:11.873Z", + "time": 25.34499999912642, + "timings": { + "blocked": 6.360000002148562, + "dns": 0.008000000000000007, + "ssl": -1, + "connect": 0.43100000000000005, + "send": 0.10299999999999976, + "wait": 17.997999999124556, + "receive": 0.44499999785330147, + "_blocked_queueing": 3.832000002148561 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "394649", + "pageref": "page_7", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=S", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "S" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "131" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:42:25 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 131, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Tartelette Fraise\",\"description\":\"Delicieuse Tartelette aux Fraises fraiches\",\"size\":\"S\",\"price\":2,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 132, + "_transferSize": 389, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:42:25.547Z", + "time": 33.43599999586027, + "timings": { + "blocked": 3.919000000181608, + "dns": 0.006000000000000005, + "ssl": -1, + "connect": 0.8, + "send": 0.12399999999999989, + "wait": 28.34099999971036, + "receive": 0.24599999596830457, + "_blocked_queueing": 2.945000000181608 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_8", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=M", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "M" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "252" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:27 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 252, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Divorces\",\"description\":\"Delicieux Divorces pas calorique du tout\",\"size\":\"M\",\"price\":2.8,\"status\":\"available\"},{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 253, + "_transferSize": 510, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:27.244Z", + "time": 27.83599999248609, + "timings": { + "blocked": 14.010999996269122, + "dns": 0.0040000000000000036, + "ssl": -1, + "connect": 0.2999999999999998, + "send": 0.1200000000000001, + "wait": 12.966999994811601, + "receive": 0.43400000140536577, + "_blocked_queueing": 11.293999996269122 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_9", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Millefeuille", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 663, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "128" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:38 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 128, + "mimeType": "application/json", + "compression": -1, + "text": "{\"name\":\"Millefeuille\",\"description\":\"Delicieux Millefeuille pas calorique du tout\",\"size\":\"L\",\"price\":4.4,\"status\":\"available\"}" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 129, + "_transferSize": 386, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:38.913Z", + "time": 26.883999991696328, + "timings": { + "blocked": 11.270999997854233, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.06899999999999995, + "wait": 15.220000000571833, + "receive": 0.3239999932702631, + "_blocked_queueing": 10.395999997854233 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_10", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Eclair+Cafe", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 662, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "129" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:48 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 129, + "mimeType": "application/json", + "compression": -1, + "text": "{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"available\"}" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 130, + "_transferSize": 387, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:48.168Z", + "time": 22.693000006256625, + "timings": { + "blocked": 8.27700000374578, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.09899999999999998, + "wait": 14.092000006375834, + "receive": 0.22499999613501132, + "_blocked_queueing": 7.297000003745779 + } + }, + { + "time": 48.992874145507812, + "_isHTTPS": false, + "_webSocketMessages": null, + "_remoteDeviceIP": null, + "timings": { + "connect": 1, + "send": -1, + "dns": -1, + "ssl": -1, + "wait": -1, + "blocked": -1, + "receive": -1 + }, + "_serverAddress": "127.0.0.1", + "_isIntercepted": true, + "_id": "3764", + "serverIPAddress": "127.0.0.1", + "_name": "3764", + "_clientAddress": "127.0.0.1", + "_clientBundlePath": "\/Applications\/Postman.app", + "request": { + "method": "PATCH", + "bodySize": 13, + "headersSize": 296, + "postData": { + "params": [ + { + "name": "{\"price\":2.6}", + "value": "" + } + ], + "text": "{\"price\":2.6}", + "mimeType": "application\/json" + }, + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application\/json" + }, + { + "name": "User-Agent", + "value": "PostmanRuntime\/7.32.3" + }, + { + "name": "Accept", + "value": "*\/*" + }, + { + "name": "Postman-Token", + "value": "ecd3b816-8156-4409-8f94-56413aeab446" + }, + { + "name": "Host", + "value": "localhost.proxyman.io:8080" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "13" + } + ], + "queryString": [], + "httpVersion": "HTTP\/1.1", + "url": "http:\/\/localhost:8080\/rest\/API+Pastries\/0.0.1\/pastries\/Eclair+Cafe" + }, + "_serverPort": 8080, + "_clientName": "Postman", + "_clientPort": 56468, + "response": { + "status": 200, + "content": { + "size": 129, + "mimeType": "application\/json;charset=UTF-8", + "encoding": "base64", + "text": "eyJuYW1lIjoiRWNsYWlyIENhZmUiLCJkZXNjcmlwdGlvbiI6IkRlbGljaWV1eCBFY2xhaXIgYXUgQ2FmZSBwYXMgY2Fsb3JpcXVlIGR1IHRvdXQiLCJzaXplIjoiTSIsInByaWNlIjoyLjYsInN0YXR1cyI6ImF2YWlsYWJsZSJ9" + }, + "bodySize": 129, + "headersSize": 260, + "cookies": [], + "statusText": "OK", + "headers": [ + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + }, + { + "name": "Content-Type", + "value": "application\/json;charset=UTF-8" + }, + { + "name": "Content-Length", + "value": "129" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 09:05:53 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "httpVersion": "HTTP\/1.1", + "redirectURL": "" + }, + "comment": "", + "startedDateTime": "2023-08-17T11:05:53.434+02:00", + "cache": {} + } + ] + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/films-metadata.yml b/jdk_21_maven/cs/rest-gui/microcks/samples/films-metadata.yml new file mode 100644 index 000000000..5b01825d5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/films-metadata.yml @@ -0,0 +1,21 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIMetadata +metadata: + name: Movie Graph API + version: 1.0 + labels: + domain: movie + status: stable + team: Team A +operations: + 'addReview': + dispatcher: JSON_BODY + dispatcherRules: |- + { + "exp": "/filmId", + "operator": "equals", + "cases": { + "ZmlsbXM6Mg==": "addReview to ZmlsbXM6Mg==", + "default": "ZmlsbXM6Mg==" + } + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/films-postman.json b/jdk_21_maven/cs/rest-gui/microcks/samples/films-postman.json new file mode 100644 index 000000000..50474bc00 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/films-postman.json @@ -0,0 +1,245 @@ +{ + "info": { + "_postman_id": "7c00b563-d454-421a-a3b0-17c4e3254cb9", + "name": "Movie Graph API", + "description": "version=1.0 - A Graph API for querying movies", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "queries", + "item": [ + { + "name": "allFilms", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query allFilms {\n allFilms {\n totalCount\n films {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n }\n}", + "variables": "{}" + } + }, + "url": { + "raw": "http://allFilms", + "protocol": "http", + "host": [ + "allFilms" + ] + } + }, + "response": [ + { + "name": "allFilms", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query allFilms {\n allFilms {\n films {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n }\n}", + "variables": "{}" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"allFilms\": {\n \"films\": [\n {\n \"id\": \"ZmlsbXM6MQ==\",\n \"title\": \"A New Hope\",\n \"episodeID\": 4,\n \"director\": \"George Lucas\",\n \"starCount\": 432,\n \"rating\": 4.3\n },\n {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 433,\n \"rating\": 4.3\n },\n {\n \"id\": \"ZmlsbXM6Mw==\",\n \"title\": \"Return of the Jedi\",\n \"episodeID\": 6,\n \"director\": \"Richard Marquand\",\n \"starCount\": 434,\n \"rating\": 4.3\n },\n {\n \"id\": \"ZmlsbXM6NA==\",\n \"title\": \"The Phantom Menace\",\n \"episodeID\": 1,\n \"director\": \"George Lucas\",\n \"starCount\": 252,\n \"rating\": 3.2\n },\n {\n \"id\": \"ZmlsbXM6NQ==\",\n \"title\": \"Attack of the Clones\",\n \"episodeID\": 2,\n \"director\": \"George Lucas\",\n \"starCount\": 320,\n \"rating\": 3.9\n },\n {\n \"id\": \"ZmlsbXM6Ng==\",\n \"title\": \"Revenge of the Sith\",\n \"episodeID\": 3,\n \"director\": \"George Lucas\",\n \"starCount\": 410,\n \"rating\": 4.1\n }\n ]\n }\n }\n}" + } + ] + }, + { + "name": "film", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"id\": \"\"\n}" + } + }, + "url": { + "raw": "http://film", + "protocol": "http", + "host": [ + "film" + ] + } + }, + "response": [ + { + "name": "film ZmlsbXM6MQ==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"id\": \"ZmlsbXM6MQ==\"\n}" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"film\": {\n \"id\": \"ZmlsbXM6MQ==\",\n \"title\": \"A New Hope\",\n \"episodeID\": 4,\n \"director\": \"George Lucas\",\n \"starCount\": 432,\n \"rating\": 4.3\n }\n }\n}" + }, + { + "name": "film ZmlsbXM6Mg==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"id\": \"ZmlsbXM6Mg==\"\n}" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"film\": {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 433,\n \"rating\": 4.3\n }\n }\n}" + } + ] + } + ] + }, + { + "name": "mutations", + "item": [ + { + "name": "addStar", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation AddStar($filmId: String) {\n addStar(filmId: $filmId) {\n id\n starCount\n rating\n }\n}", + "variables": "{\n \"filmId\": \"\"\n}" + } + }, + "url": { + "raw": "http://addStar", + "protocol": "http", + "host": [ + "addStar" + ] + } + }, + "response": [ + { + "name": "addStar to ZmlsbXM6Mg==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation AddStar($filmId: String) {\n addStar(filmId: $filmId) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"filmId\": \"ZmlsbXM6Mg==\"\n}" + } + }, + "url": { + "raw": "http://addStar", + "protocol": "http", + "host": [ + "addStar" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"addStar\": {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 434,\n \"rating\": 4.3\n }\n }\n}" + } + ] + }, + { + "name": "addReview", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://addReview", + "protocol": "http", + "host": [ + "addReview" + ] + } + }, + "response": [ + { + "name": "addReview to ZmlsbXM6Mg==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation AddReview($filmId: String, $review: Review) {\n addReview(filmId: $filmId, review: $review) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}\n", + "variables": "{\n \"filmId\": \"ZmlsbXM6Mg==\",\n \"review\": {\n \"comment\": \"Awesome!\",\n \"rating\": 5\n }\n}\n" + } + }, + "url": { + "raw": "http://addReview", + "protocol": "http", + "host": [ + "addReview" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"addReview\": {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 433,\n \"rating\": 4.4\n }\n }\n}" + } + ] + } + ] + } + ], + "variable": [ + { + "key": "url", + "value": "", + "type": "any", + "description": "URL for the request." + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/films.graphql b/jdk_21_maven/cs/rest-gui/microcks/samples/films.graphql new file mode 100644 index 000000000..f6cd1f0ae --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/films.graphql @@ -0,0 +1,34 @@ +# microcksId: Movie Graph API : 1.0 +schema { + query: Query + mutation: Mutation +} +type Film { + id: String! + title: String! + episodeID: Int! + director: String! + starCount: Int! + rating: Float! +} + +type FilmsConnection { + totalCount: Int! + films: [Film] +} + +input Review { + comment: String + rating: Int +} + +type Query { + allFilms: FilmsConnection + film(id: String): Film +} + +type Mutation { + addStar(filmId: String): Film + addReview(filmId: String, review: Review): Film +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/samples/hello-v1.proto b/jdk_21_maven/cs/rest-gui/microcks/samples/hello-v1.proto new file mode 100644 index 000000000..b15ca5b69 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/samples/hello-v1.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package io.github.microcks.grpc.hello.v1; + +option java_multiple_files = true; + +message HelloRequest { + string firstname = 1; + string lastname = 2; +} + +message HelloResponse { + string greeting = 1; +} + +service HelloService { + rpc greeting(HelloRequest) returns (HelloResponse); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/README.md b/jdk_21_maven/cs/rest-gui/microcks/testsuite/README.md new file mode 100644 index 000000000..95d17f134 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/README.md @@ -0,0 +1,99 @@ +# Function Testsuite for Microcks instance + +This collection of scripts and test cases is designed to validate the installation methods and the functionalities of a Microcks instance. +It is designed so that same scripts and tests can/should be applied to different methods and to validate different Microcks distribution and flavors. + +The tests are organized in two main categories: +* **Installation Tests**: Validate the installation methods and the Microcks instance itself. This is what we called the "health checks" below, +* **Functional Tests**: Validate the functionalities of Microcks instance by running a set of tests that cover the following aspects: + * Microcks own APIs, + * Mocking & Testing REST APIs, + * Mocking & Testing GraphQL APIs, + * Mocking & Testing gRPC APIs, + * Mocking & Testing SOAP APIs, + * Mocking & Testing AsyncAPI APIs. + +The Testsuite is intended to be run on the current development branch of Microcks to validate the latest changes of the installation scripts +and the features embedded into the `nighlty` tagged container images. + +## Health Checks + +Health checks consist in installing a Microcks instance using the provided Docker or Podman compose scripts as well as the Helm chart +provided in a Microcks release. + +The installations can be driven via the `install-compose.sh` or `install-helm.sh` scripts that provides wrappers around the Docker Compose and Helm commands. + +Example: +```shell +./install-compose.sh --method docker --mode default --addons async +``` + +The validation is actually done using the `health-check.sh` script that will check the readiness probes of the different containers started +by the different installation methods. + +Example: +```shell +./check-health.sh --method docker +``` + + +All those checks are intended to be scheduled and embedded into a [GitHub Action matrix job](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow). + +## Functional Tests + +The goal is to validate the functionalities of Microcks instances using different distribution and flavours. + +The different configuration/combinations to consider are: +* Microcks Regular distribution with authentication enabled (`quay.io/microcks/microcks:nightly`), +* Microcks Regular distribution without authentication (aka `devmode`), +* Microcks Uber distribution using the JVM based image (`quay.io/microcks/microcks-uber:nightly`), +* Microcks Uber distribution using the native image (`quay.io/microcks/microcks-uber:nightly-native`), + +Based on our experience, the following APIs and the features listed below are the most used and the most important to validate +for each different combination (unchecked boxes are not yet implemented). + +### Regular distribution with authentication + +#### Microcks own APIs + +* [ ] Getting the configuration (`GET /api/features/config`) should return 200 when not authenticated, +* [ ] Getting the Keycloak configuration (`GET /api/keycloak/config`) should return 200 when not authenticated, +* [ ] Getting the list of services (`GET /api/services`) should return 403 when not authenticated, +* [ ] Authentication (with default _Service Account_) should return an `AccessToken` when successful, +* [ ] Getting the list of services (`GET /api/services`) should return 200 when authenticated (using `AcessToken` as a `Bearer` token), +* [ ] Getting the list of jobs (`GET /api/jobs`) should return 403 when not authenticated or authenticated as user, +* [ ] Getting the list of jobs (`GET /api/jobs`) should return 200 when authenticated as manager or admin, + +#### Mocking APIs + +* [ ] REST Mocks for "Pastry API 2.0 - 2.0.0" should return 200, +* [ ] GraphQL Mocks for "Movie Graph API - 1.0" should return 200, +* [ ] SOAP Mocks for "HelloService Mock - 0.9" should return 200, +* [ ] gRPC Mocks for "org.acme.petstore.v1.PetstoreService - v1" should return OK, + +### Regular distribution without authentication (devmode) + +#### Microcks own APIs + +* [ ] Getting the configuration (`GET /api/features/config`) should return 200 when not authenticated, +* [ ] Getting the Keycloak configuration (`GET /api/keycloak/config`) should return 200 when not authenticated, +* [ ] Getting the list of services (`GET /api/services`) should return 200 when not authenticated, +* [ ] Getting the list of jobs (`GET /api/jobs`) should return 200 when not authenticated, + +#### Mocking APIs + +Embed the same tests as the _Regular distribution with authentication_. + +### Uber distribution using the JVM based image + +Embed the same tests as the _Regular distribution without authentication (devmode)_ for both the Microcks own APIs and the Mocking APIs. + +### Uber distribution using the native image + +Embed the same tests as the _Uber distribution using the JVM based image_ + these one in addition: + +* [ ] REST Mocks using `JSON_BODY`, `FALLBACK` and `PROXY_FALLBACK` dispatcher should return 200, + +The following tests must be excluded: + +* [ ] SOAP Mocks for "HelloService Mock - 0.9" (as the native image is not able to execute Groovy `SCRIPT` dispatcher), \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/api-tests.js b/jdk_21_maven/cs/rest-gui/microcks/testsuite/api-tests.js new file mode 100644 index 000000000..6b2b5df99 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/api-tests.js @@ -0,0 +1,22 @@ +import * as tests from './commons.js'; +import { flavorConfig } from './flavor-config.js'; +import { sleep } from 'k6'; + +const FLAVOR = __ENV.FLAVOR || 'regular-auth'; + +export default function () { + const toRun = flavorConfig[FLAVOR]; + if (!toRun) { + console.error(`Unknown flavor "${FLAVOR}"`); + return; + } + for (const fn of toRun) { + const testFn = tests[fn]; + if (typeof testFn !== 'function') { + console.error(`Test function "${fn}" not found in commons.js`); + continue; + } + console.log(`Running ${fn}`); + testFn(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/check-health.sh b/jdk_21_maven/cs/rest-gui/microcks/testsuite/check-health.sh new file mode 100644 index 000000000..ef074c707 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/check-health.sh @@ -0,0 +1,133 @@ +#!/bin/bash + +set -e + +METHOD="docker" +NAMESPACE="microcks" + +usage() { + echo "Usage: $0 [--method docker|podman|helm]" + exit 1 +} + +# Parse flags +while [[ "$#" -gt 0 ]]; do + case "$1" in + --method) + METHOD="$2" + shift 2 + ;; + *) + usage + ;; + esac +done + +if [[ "$METHOD" != "docker" && "$METHOD" != "podman" && "$METHOD" != "helm" ]]; then + echo "Error: Invalid method '$METHOD'. Use 'docker','podman' or 'helm." + usage +fi + +if [[ "$METHOD" == "docker" || "$METHOD" == "podman" ]]; then + INSPECT_CMD="$METHOD" + + unhealthy=() + + to_seconds() { + local value="$1" + echo "${value//[a-zA-Z]/}" + } + + containers=($($INSPECT_CMD ps --format '{{.Names}}')) + + if [[ ${#containers[@]} -eq 0 ]]; then + echo "Error: No containers found" + exit 1 + fi + + for cname in "${containers[@]}"; do + echo "Inspecting container: $cname" + + interval=$($INSPECT_CMD inspect -f '{{.Config.Healthcheck.Interval}}' "$cname") + timeout=$($INSPECT_CMD inspect -f '{{.Config.Healthcheck.Timeout}}' "$cname") + start_period=$($INSPECT_CMD inspect -f '{{.Config.Healthcheck.StartPeriod}}' "$cname") + retries=$($INSPECT_CMD inspect -f '{{.Config.Healthcheck.Retries}}' "$cname") + + echo "Interval=${interval}, timeout=${timeout}, start-period=${start_period}, retries=${retries}" + + try=0 + elapsed=0 + sleep $start_period + timeout_val=$(to_seconds "$timeout") + interval_val=$(to_seconds "$interval") + while [[ $elapsed -lt $timeout_val ]] || [[ $try -lt $retries ]]; do + status=$($INSPECT_CMD inspect -f '{{.State.Health.Status}}' "$cname") + echo "Status after ${elapsed}s: $status" + + if [[ "$status" == "healthy" ]]; then + echo "$cname is healthy!" + break + fi + + sleep "$interval" + try=$((try + 1)) + elapsed=$((elapsed + interval_val)) + done + + if [[ "$status" != "healthy" ]]; then + unhealthy+=("$cname:$status") + fi + done +else + echo "Checking Helm in namespace '$NAMESPACE'" + + mapfile -t deployments < <( + kubectl get deployments -n "$NAMESPACE" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' \ + | sort -u | grep -v '^microcks-async-minion$' + ) + # Append async one at the end if it exists + if kubectl get deployment microcks-async-minion -n "$NAMESPACE" &>/dev/null; then + deployments+=("microcks-async-minion") + fi + + if [[ ${#deployments[@]} -eq 0 ]]; then + echo "Error: No deployments found in namespace '$NAMESPACE'" + exit 1 + fi + + for dep in "${deployments[@]}"; do + echo "Waiting for deployment '$dep' to roll out (timeout: 60s)..." + retries=3 + attempt=1 + success=false + while [[ $attempt -le $retries ]]; do + if kubectl rollout status deployment/"$dep" -n "$NAMESPACE" --timeout=60s; then + success=true + break + else + echo "Attempt $attempt for deployment '$dep' failed." + ((attempt++)) + if [[ $attempt -le $retries ]]; then + echo "Retrying in 5 seconds..." + sleep 5 + fi + fi + done + + if ! $success; then + echo "Deployment '$dep' failed to roll out after $retries attempts." + unhealthy+=("$dep") + fi + done +fi + +if [[ ${#unhealthy[@]} -eq 0 ]]; then + echo "All containers are healthy!" + exit 0 +else + echo "Some containers failed health checks:" + for entry in "${unhealthy[@]}"; do + echo "$entry" + done + exit 1 +fi diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/commons.js b/jdk_21_maven/cs/rest-gui/microcks/testsuite/commons.js new file mode 100644 index 000000000..5bd7a9754 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/commons.js @@ -0,0 +1,453 @@ +import http from 'k6/http'; +import grpc from 'k6/net/grpc'; +import ws from 'k6/ws'; +import { check, sleep, group } from 'k6'; + +// Define the wait time of browse scenario +const WAIT_TIME = parseFloat(__ENV.WAIT_TIME) || 0.5; + +// Define the base URL for Microcks (adjust as needed) +const HOST = __ENV.HOST || 'localhost'; +const PORT = __ENV.PORT || '8080'; +const BASE_URL = __ENV.BASE_URL || `http://${HOST}:${PORT}`; +const GRPC_PORT = __ENV.GRPC_PORT || '9090'; +const KEYCLOAK_URL = __ENV.KEYCLOAK_URL || `http://${HOST}:18080`; + +const only500Callback = http.expectedStatuses(500); + +const client = new grpc.Client(); +client.load(['../samples/'], 'hello-v1.proto'); + +/* Simulate users browsing the API repository and getting details. */ +export function browse() { + const servicesRes = http.get(`${BASE_URL}/api/services`); + check(servicesRes, { + "status code should be 200": servicesRes => servicesRes.status === 200, + }); + + const services = servicesRes.json(); + sleep(WAIT_TIME); + + services.forEach(service => { + const serviceViewRes = http.get(`${BASE_URL}/api/services/` + service.id + '?messages=true'); + sleep(WAIT_TIME); + const serviceTestsRes = http.get(`${BASE_URL}/api/tests/service/` + service.id + '?page=0&size=20'); + sleep(WAIT_TIME); + }); +} + +// Function to test REST API endpoints +export function invokeRESTMocks() { + group('REST API Tests', function () { + // Test fetching all pastries + let pastryCall = http.get(`${BASE_URL}/rest/API+Pastry+-+2.0/2.0.0/pastry`); + let ok = check(pastryCall, { + 'pastryCall status is 200': (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${pastryCall.status} from pastryCall:\n${pastryCall.body}`); + } + + // Test fetching a specific pastry in JSON + let eclairCall = http.get(`${BASE_URL}/rest/API+Pastry+-+2.0/2.0.0/pastry/Eclair%20Cafe`); + ok = check(eclairCall, { + 'eclairCall status is 200': (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${eclairCall.status} from eclairCall:\n${eclairCall.body}`); + } + + // Test fetching the same pastry in XML + let eclairXmlCall = http.get(`${BASE_URL}/rest/API+Pastry+-+2.0/2.0.0/pastry/Eclair%20Cafe`, { headers: {'Accept': 'text/xml'} }); + ok = check(eclairXmlCall, { + 'eclairXmlCall status is 200': (r) => r.status === 200, + 'eclairXmlCall response is XML': (r) => r.body.includes(""), + }); + if (!ok) { + console.error(`Unexpected status ${eclairXmlCall.status} from eclairXmlCall:\n${eclairXmlCall.body}`); + } + + // Test fetching another pastry + let millefeuilleCall = http.get(`${BASE_URL}/rest/API+Pastry+-+2.0/2.0.0/pastry/Millefeuille`); + ok = check(millefeuilleCall, { + 'millefeuilleCall status is 200': (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${millefeuilleCall.status} from millefeuilleCall:\n${millefeuilleCall.body}`); + } + }); +} + +// Function to test GraphQL endpoints +export function invokeGraphQLMocks() { + group('GraphQL API Tests', function () { + const jsonHeaders = { 'Content-Type': 'application/json' }; + + // Test a query to fetch all films + const allFilmsQuery = `query allFilms { + allFilms { + films { + id + title + } + } + }`; + const allFilmsBody = { query: allFilmsQuery }; + let allFilmsCall = http.post(`${BASE_URL}/graphql/Movie+Graph+API/1.0`, JSON.stringify(allFilmsBody), { headers: jsonHeaders }); + let ok = check(allFilmsCall, { + 'allFilmsCall status is 200': (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${allFilmsCall.status} from allFilmsCall:\n${allFilmsCall.body}`); + } + + // Test a query to fetch a specific film + const aFilmQuery = `query film($id: String) { + film(id: "ZmlsbXM6MQ==") { + id + title + episodeId + } + }`; + const aFilmBody = { query: aFilmQuery }; + let aFilmCall = http.post(`${BASE_URL}/graphql/Movie+Graph+API/1.0`, JSON.stringify(aFilmBody), { headers: jsonHeaders }); + ok = check(aFilmCall, { + 'aFilmCall status is 200': (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${aFilmCall.status} from aFilmCall:\n${aFilmCall.body}`); + } + + // Test a query using fragments + const aFilmFragmentsQuery = `query film($id: String) { + film(id: "ZmlsbXM6MQ==") { + ...filmFields + } + } + fragment filmFields on Film { + id + title + episodeId + starCount + }`; + const aFilmFragmentBody = { query: aFilmFragmentsQuery }; + let aFilmFragmentCall = http.post(`${BASE_URL}/graphql/Movie+Graph+API/1.0`, JSON.stringify(aFilmFragmentBody), { headers: jsonHeaders }); + ok = check(aFilmFragmentCall, { + 'aFilmFragmentCall status is 200': (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${aFilmFragmentCall.status} from aFilmFragmentCall:\n${aFilmFragmentCall.body}`); + } + }); +} + +// Function to test SOAP endpoints +export function invokeSOAPMocks() { + group('SOAP API Tests', function () { + // Define a SOAP envelope for "Andrew" + const andrewBody = ` + + + + Andrew + + + `; + // Set appropriate headers for SOAP 1.1 + const andrewHeaders = { + 'Content-Type': 'text/xml; charset=utf-8', + 'SOAPAction': 'sayHello' + }; + let andrewCall = http.post(`${BASE_URL}/soap/HelloService+Mock/0.9`, andrewBody, { headers: andrewHeaders }); + let ok = check(andrewCall, { + 'andrewCall status is 200': (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${andrewCall.status} from andrewCall:\n${andrewCall.body}`); + } + sleep(1); + + // Define a SOAP envelope for "Karla" with SOAP 1.2 headers + const karlaBody = ` + + + + Karla + + + `; + const karlaHeaders = { + 'Content-Type': 'text/xml; charset=utf-8; action=sayHello' + }; + let karlaCall = http.post(`${BASE_URL}/soap/HelloService+Mock/0.9`, karlaBody, { headers: karlaHeaders }); + ok = check(karlaCall, { + 'karlaCall status is 200': (r) => r.status === 200, + 'karlaCall body contains expected fault or message': (r) => r.body.includes("Hello Karla") || r.body.includes("Fault"), + }); + if (!ok) { + console.error(`Unexpected status ${karlaCall.status} from karlaCall:\n${karlaCall.body}`); + } + sleep(1); + + // Define a SOAP envelope for "Laurent" expecting a fault (500) + const laurentHeaders = { + 'Content-Type': 'application/soap+xml; charset=utf-8; action=sayHello' + }; + const laurentBody = ` + + + + Laurent + + + `; + let laurentCall = http.post(`${BASE_URL}/soap/HelloService+Mock/0.9`, laurentBody, { headers: laurentHeaders, responseCallback: only500Callback }) + ok = check(laurentCall, { + 'laurentCall status is 500': (r) => r.status === 500, + 'laurentCall body contains a Fault element': (r) => r.body.includes(""), + }); + if (!ok) { + console.error(`Unexpected status ${laurentCall.status} from laurentCall:\n${laurentCall.body}`); + } + sleep(1); + }); +} + +// Function to test GRPC endpoints +export function invokeGRPCMocks() { + client.connect(`${HOST}:${GRPC_PORT}`, { plaintext: true }); + + const payloads = [ + { firstname: 'Laurent', lastname: 'Broudoux' }, + { firstname: 'John', lastname: 'Doe' }, + ]; + + payloads.forEach((payload) => { + const response = client.invoke( + 'io.github.microcks.grpc.hello.v1.HelloService/greeting', + payload + ); + + let ok = check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + 'response contains greeting': (r) => + r && r.message && r.message.greeting.includes(payload.firstname), + }); + if (!ok) { + console.error(`Unexpected status ${response.status} from ${payload.firstname}`); + } + }); + + client.close(); + sleep(1); +}; + +// Function to test REST API endpoints for HelloAPIMock +export function invokeREST_HelloAPIMocks() { + group('Hello API REST Mocks', () => { + const MOCK_NAME = 'Hello%20API%20Mock'; + const VERSION = '0.8'; + const RESOURCE = 'v1/hello'; + + const TEST_CASES = [ + { name: 'David', expStatus: 200, expGreeting: 'Hello David !' }, + { name: 'Gavin', expStatus: 200, expGreeting: 'Hello Gavin !' }, + { name: 'Nobody', expStatus: 400, expGreeting: null }, + ]; + TEST_CASES.forEach(({ name, expStatus, expGreeting }) => { + const url = `${BASE_URL}/rest/${MOCK_NAME}/${VERSION}/${RESOURCE}?name=${encodeURIComponent(name)}`; + const res = http.get(url); + + // Status code + let ok = check(res, { + [`${name}Call status is ${expStatus}`]: (r) => r.status === expStatus, + }); + if (!ok) { + console.error(`Unexpected status ${res.status} for ${name}:\n${res.body}`); + } + + // Header + if (expStatus === 200) { + check(res, { + [`${name}Call response is JSON`]: (r) => + r.headers['Content-Type'] && + r.headers['Content-Type'].includes('application/json'), + }); + } + + // Body assertion by substring + if (expGreeting) { + check(res, { + [`${name}Call body contains "${expGreeting}"`]: (r) => + r.body.includes(expGreeting), + }); + } + sleep(1); + }); + + const url = `${BASE_URL}/rest/${MOCK_NAME}/${VERSION}/`; + const res = http.get(url); + let ok = check(res, { + [`Empty body`]: (r) => + !r.body || r.body.trim().length === 0, + }); + if (!ok) { + console.error(`Unexpected status ${res.status} for Empty Body:\n${res.body}`); + } + }); +} + +export function invokeREST_PetStoreAPI() { + group('Petstore API', () => { + const userKeys = { + 1: '998bac0775b1d5f588e0a6ca7c11b852', + 2: '70f735676ec46351c6699c4bb767878a', + }; + + // Test for petId = 1 (expected 404) + const petRes1 = http.get(`${BASE_URL}/rest/Petstore+API/1.0/v2/pet/1?user_key=${userKeys[1]}`); + let ok = check(petRes1, { + 'GET /v2/pet/1 - status is 404': (r) => r.status === 404, + }); + if (!ok) { + console.error(`Unexpected status ${petRes1.status} for Pet 1:\n${petRes1.body}`); + } + sleep(1); + + // Test for petId = 2 (expected 200 + content validation) + const petRes2 = http.get(`${BASE_URL}/rest/Petstore+API/1.0/v2/pet/2?user_key=${userKeys[2]}`); + ok = check(petRes2, { + 'GET /v2/pet/2 - status is 200': (r) => r.status === 200, + 'GET /v2/pet/2 - has name "cat"': (r) => r.json().name === 'cat', + }); + if (!ok) { + console.error(`Unexpected status ${petRes2.status} for Pet 2:\n${petRes2.body}`); + } + sleep(1); + + // Test GET /v2/pet/findByStatus for status=available + const status = 'available'; + const response = http.get(`${BASE_URL}/rest/Petstore+API/1.0/v2/pet/findByStatus?status=${status}&user_key=70f735676ec46351c6699c4bb767878a`); + ok = check(response, { + 'status is 200': (r) => r.status === 200, + 'response is non-empty array': (r) => Array.isArray(r.json()) && r.json().length > 0, + 'first pet has id and name': (r) => { + const data = r.json(); + return data.length > 0 && data[0].id !== undefined && data[0].name !== undefined; + }, + }); + if (!ok) { + console.error(`Unexpected status ${response.status} from /v2/pet/findByStatus:\n${response.body}`); + } + sleep(1); + }); +} + +export function authenticate() { + const url = `${KEYCLOAK_URL}/realms/microcks/protocol/openid-connect/token`; + const authHeader = 'Basic bWljcm9ja3Mtc2VydmljZWFjY291bnQ6YWI1NGQzMjktZTQzNS00MWFlLWE5MDAtZWM2YjNmZTE1YzU0Cg='; + + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': authHeader, + }; + + const payload = { + grant_type: 'client_credentials', + }; + + const response = http.post(url, payload, { headers: headers }); + + let ok = check(response, { + 'authentication successful': (r) => r.status === 200, + 'access token is present': (r) => r.json('access_token') !== '', + }); + if (!ok) { + console.error(`Unexpected status ${response.status} from authentication:\n${response.body}`); + } + + return response.json('access_token'); +} + +const TESTS = [ + { path: '/api/features/config', expect: 200 }, + { path: '/api/keycloak/config', expect: 200 }, + { path: '/api/services', expect: 401 }, + { path: '/api/jobs', expect: 401 }, +]; + +export function ownAPIsNoAuth () { + group("Microcks' own APIs without authentication", () => { + const responses = http.batch( + TESTS.map((t) => ['GET', BASE_URL + t.path]) + ); + + TESTS.forEach((t, i) => { + let ok = check(responses[i], { + [`GET ${t.path} returns 200`]: (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${responses[i].status} from ${t.path}:\n${responses[i].body}`); + } + }); + }); +} + +export function ownAPIsAuth () { + group("Microcks' own APIs with authentication", () => { + const responses = http.batch( + TESTS.map((t) => ['GET', BASE_URL + t.path]) + ); + + TESTS.forEach((t, i) => { + let ok = check(responses[i], { + [`GET ${t.path} returns ${t.expect}`]: (r) => r.status === t.expect, + }); + if (!ok) { + console.error(`Unexpected status ${responses[i].status} from ${t.path}:\n${responses[i].body}`); + } + }); + + const token = authenticate(); + const authHeaders = { headers: { Authorization: `Bearer ${token}` } }; + + const auth_responses = http.batch( + TESTS.map((t) => ['GET', `${BASE_URL}${t.path}`, null, authHeaders]) + ); + TESTS.forEach((t, i) => { + let ok = check(auth_responses[i], { + [`GET ${t.path} auth returns 200`]: (r) => r.status === 200, + }); + if (!ok) { + console.error(`Unexpected status ${auth_responses[i].status} from ${t.path}:\n${auth_responses[i].body}`); + } + }); + }); +} + +export function asyncAPI_websocketMocks() { + group('User Signed-Up WebSocket Test', () => { + const url = `ws://${HOST}:8081/api/ws/User+signed-up+API/0.1.50/consumeUserSignedUp`; + let messages = []; + + const res = ws.connect(url, {}, (socket) => { + socket.on('message', (m) => { + messages.push(m); + }); + + socket.setTimeout(function () { + console.log(`Closing the socket forcefully`); + socket.close(); + }, 3000); + }); + + let ok = check(res, { 'handshake 101': (r) => r && r.status === 101 }); + if (!ok) { + console.error(`Unexpected status ${res.status} from handshake:\n${res.body}`); + } + check(messages, { + 'contains Laurent Broudoux': (arr) => arr.some(m => m.includes('Laurent Broudoux')), + 'contains John Doe': (arr) => arr.some(m => m.includes('John Doe')), + }); + }); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/flavor-config.js b/jdk_21_maven/cs/rest-gui/microcks/testsuite/flavor-config.js new file mode 100644 index 000000000..13260cd8c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/flavor-config.js @@ -0,0 +1,20 @@ +const baseMocks = [ + 'invokeRESTMocks', + 'invokeSOAPMocks', + 'invokeGraphQLMocks', + 'invokeGRPCMocks', + 'invokeREST_HelloAPIMocks', + 'invokeREST_PetStoreAPI', + 'asyncAPI_websocketMocks', +] +var flavorConfig = { + 'regular-auth': ['ownAPIsAuth'].concat(baseMocks), + 'regular-noauth': ['ownAPIsNoAuth'].concat(baseMocks), + 'uber-jvm': ['ownAPIsNoAuth'].concat(baseMocks), + 'uber-native': ['ownAPIsNoAuth'].concat( + baseMocks.filter(function (fn) { return fn !== 'invokeSOAPMocks' && + fn !== 'invokeREST_HelloAPIMocks'; }) + ), + // add more flavors as needed… +}; +export { flavorConfig }; diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-compose.sh b/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-compose.sh new file mode 100644 index 000000000..e689e8b80 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-compose.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +usage() { + echo "Usage: $0 [--method {docker|podman}] [--mode {default|proxy|devmode}] [--addons addon1,addon2,...]" + exit 1 +} + +MODE="default" +METHOD="docker" +ADDONS=() + +# Parse command-line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --mode) + MODE="$2" + shift + ;; + --addons) + IFS=',' read -r -a ADDONS <<< "$2" + shift + ;; + --method) + METHOD="$2" + shift + ;; + *) + usage + ;; + esac + shift +done + +# Validate method flag +if [[ "$METHOD" != "docker" && "$METHOD" != "podman" ]]; then + echo "Error: Invalid method. Choose 'docker' or 'podman'." + usage +fi + +# Append -compose to form the command name (e.g. docker-compose or podman-compose) +METHOD+="-compose" + +# Get the directory where this script is located. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/../install/${METHOD}" || { echo "Failed to change directory to ${SCRIPT_DIR}"; exit 1; } + +# Validate that proxy mode is not allowed when using podman +if [[ "$METHOD" == "podman-compose" && "$MODE" == "proxy" ]]; then + echo "Error: Proxy mode is not supported when using podman." + exit 1 +fi + +# Determine main compose file based on the mode +case $MODE in + default) + MAIN_COMPOSE="${METHOD}.yml" + ;; + proxy) + MAIN_COMPOSE="${METHOD}-with-proxy.yml" + ;; + devmode) + MAIN_COMPOSE="${METHOD}-devmode.yml" + ;; + *) + echo "Error: Invalid mode. Choose from default, proxy, or devmode." + usage + ;; +esac + +# Start building the command with the main compose file +CMD="${METHOD} -f ${MAIN_COMPOSE}" + +for addon in "${ADDONS[@]}"; do + addon=$(echo "$addon" | xargs) # trim whitespace + ADDON_FILE="${addon}-addon.yml" + if [[ -f "$ADDON_FILE" ]]; then + CMD+=" -f ${ADDON_FILE}" + else + echo "Error: ${ADDON_FILE} not found, installation aborted." + exit 1 + fi +done + +CMD+=" up -d" + +echo "Running command: ${CMD}" + +if eval ${CMD}; then + echo "Microcks installation complete." +else + echo "Error: ${METHOD} encountered an error, installation aborted." + exit 1 +fi diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-helm.sh b/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-helm.sh new file mode 100644 index 000000000..60b8aca7d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-helm.sh @@ -0,0 +1,89 @@ +#!/bin/bash +set -e + +# Default value for async flag +ASYNC=false + +# Parse command line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --async) ASYNC=true; shift ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac +done + +# Check if minikube is running +MINIKUBE_STATUS=$(minikube status --format '{{.Host}}' 2>/dev/null || true) +if [ "$MINIKUBE_STATUS" != "Running" ]; then + echo "[ERROR] Minikube is not running. Please start your cluster (e.g., run 'minikube start') and try again." + exit 1 +fi + +MINIKUBE_IP=$(minikube ip) +echo "[INFO] Minikube IP is: $MINIKUBE_IP" + +# Enable the ingress addon if it's not already enabled. +echo "[INFO] Enabling ingress addon..." +minikube addons enable ingress + +# Wait for the ingress controller pod to be ready +echo "Waiting for the ingress controller to be ready..." +kubectl wait --for=condition=Ready pod -n ingress-nginx -l app.kubernetes.io/component=controller --timeout=2m + +# Create the microcks namespace if it doesn't exist. +NAMESPACE="microcks" +if ! kubectl get namespace $NAMESPACE >/dev/null 2>&1; then + echo "[INFO] Creating namespace '$NAMESPACE'..." + kubectl create namespace $NAMESPACE +fi + +# Get the directory where this config is located. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/../install/kubernetes" || { echo "Failed to change directory to ${SCRIPT_DIR}"; exit 1; } + +# Add the Microcks Helm repository and update it. +echo "[INFO] Adding Microcks Helm repository..." +helm repo add microcks https://microcks.io/helm +if $ASYNC; then + helm repo add strimzi https://strimzi.io/charts/ +fi +helm repo update + +# Install Microcks using Helm with dynamic nip.io URLs based on the minikube IP. +echo "[INFO] Installing Microcks..." +if $ASYNC; then + helm install strimzi strimzi/strimzi-kafka-operator --namespace microcks + helm install microcks ./microcks --namespace=microcks \ + --set appName=microcks --set features.async.enabled=true \ + --set microcks.url=microcks.${MINIKUBE_IP}.nip.io \ + --set keycloak.url=keycloak.${MINIKUBE_IP}.nip.io \ + --set keycloak.privateUrl=http://microcks-keycloak.microcks.svc.cluster.local:8080 \ + --set features.async.kafka.url=${MINIKUBE_IP}.nip.io +else + helm install microcks ./microcks --namespace microcks \ + --set microcks.url=microcks.${MINIKUBE_IP}.nip.io \ + --set keycloak.url=keycloak.${MINIKUBE_IP}.nip.io \ + --set keycloak.privateUrl=http://microcks-keycloak.microcks.svc.cluster.local:8080 +fi + + +# Wait for the Microcks pods to become ready. +echo "[INFO] Waiting for Microcks pods to be ready..." + +# Wait for all microcks pods except async-minion +pods=$(kubectl get pods -n "$NAMESPACE" -l app=microcks -o jsonpath='{.items[?(@.metadata.name!="microcks-async-minion")].metadata.name}') + +if ! kubectl wait --for=condition=Ready pod -n "$NAMESPACE" $pods --timeout=120s; then + echo "[WARN] Some Microcks pods (except async-minion) did not become ready within 300s. Continuing anyway." +fi + +# Wait for async-minion pod last +if ! kubectl wait --for=condition=Ready pod microcks-async-minion -n "$NAMESPACE" --timeout=120s; then + echo "[WARN] Async-minion pod did not become ready within timeout. Continuing anyway." +fi + +echo "------------------------------------------------------" +echo "Microcks installation is complete!" +echo "Microcks is available at: https://microcks.${MINIKUBE_IP}.nip.io" +echo "Keycloak is available at: https://keycloak.${MINIKUBE_IP}.nip.io" +echo "------------------------------------------------------" diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-microcks.sh b/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-microcks.sh new file mode 100644 index 000000000..ab088f43c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/install-microcks.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <] [--tag ] + +Options: + --image Microcks image to run (default: microcks-uber) + e.g. microcks-uber, microcks-uber-async-minion, etc. + + --tag Image tag to use (default: nightly) + e.g. nightly, nightly-native, + +Note: Supported images and tags can be found at: + https://quay.io/organization/microcks +EOF + exit 1 +} + +error_exit() { + echo "Error: $1" >&2 + exit "${2:-1}" +} + +# Defaults +IMAGE="microcks-uber" +TAG="nightly" + +# Parse flags +while [[ $# -gt 0 ]]; do + case $1 in + --image) + IMAGE="$2"; shift ;; + --tag) + TAG="$2"; shift ;; + *) + usage ;; + esac + shift +done + +FULL_IMAGE="quay.io/microcks/${IMAGE}:${TAG}" + +pull_image() { + echo "Pulling image ${FULL_IMAGE}..." + if ! docker pull "${FULL_IMAGE}"; then + error_exit "Failed to pull image ${FULL_IMAGE}" + fi +} + +run_container() { + echo "Starting container in detached mode (host:8585 → container:8080)..." + if ! docker run -d --rm -p 8585:8080 "${FULL_IMAGE}"; then + error_exit "Failed to start container from image ${FULL_IMAGE}" + fi +} + +pull_image +run_container + +echo "Microcks is up at http://localhost:8585 (image=${IMAGE}, tag=${TAG})" diff --git a/jdk_21_maven/cs/rest-gui/microcks/testsuite/upload-artifacts.sh b/jdk_21_maven/cs/rest-gui/microcks/testsuite/upload-artifacts.sh new file mode 100644 index 000000000..782955797 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/testsuite/upload-artifacts.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +set -euo pipefail + +: "${BASE_URL:?Missing BASE_URL}" +: "${MICROCKS_TOKEN:?Missing MICROCKS_TOKEN}" + +upload_file() { + local file_path="$1" + local main_artifact="${2:-false}" + + if [[ ! -f "$file_path" ]]; then + echo "File not found: $file_path" + return 1 + fi + + echo "Uploading: $file_path (mainArtifact=$main_artifact)" + + status_code=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST "${BASE_URL}/api/artifact/upload?mainArtifact=${main_artifact}" \ + -H "Authorization: Bearer ${MICROCKS_TOKEN}" \ + -F "file=@${file_path}" -k) + + if [[ "$status_code" -ne 201 ]]; then + echo "Upload failed for $file_path — HTTP status code: $status_code" + return 1 + fi + + echo "Upload succeeded for $file_path" +} + +# Hello REST API Soapui +upload_file "/samples/HelloAPI-soapui-project.xml" true + +# Hello Service gRPC +upload_file "/samples/hello-v1.proto" true +upload_file "/samples/HelloService.postman.json" false +upload_file "/samples/HelloService.metadata.yml" false + +# HelloService Soapui API +upload_file "/samples/HelloService-soapui-project.xml" true + +# Petstore API +upload_file "/samples/PetstoreAPI-collection.json" true + +# User SignedUp API +upload_file "/samples/UserSignedUpAPI-asyncapi.yml" true + +# GraphQL API +upload_file "/samples/films.graphql" true +upload_file "/samples/films-postman.json" false +upload_file "/samples/films-metadata.yml" false + +# REST API +upload_file "/samples/APIPastry-openapi.yaml" true + +echo "All files uploaded successfully!" diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/README.md b/jdk_21_maven/cs/rest-gui/microcks/webapp/README.md new file mode 100644 index 000000000..085bba539 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/README.md @@ -0,0 +1,74 @@ +## Setup + +For development purposes, frontend GUI and backend APIs have been separated and runs onto 2 different runtime servers. +* Frontend is an Angular 8 application served by `ng serve` with livereload enabled, +* Backend is a Spring Boot application served by Boot internal server + +To run the dependencies we rely on ([MongoDB](https://mongodb.com) and [Keycloak](https://keycloak.org)) +we recommend using [Docker](https://www.docker.com/). We provide some useful scripts in the `/dev` folder of the +repository root that pull the correct versions and auto-configure them. + +### Pre-requisites + +* NodeJS (version >= 16.0) and associated tools : NPM and ng-cli (`npm i -g @angular/cli`) +* Java Development Kit (version >= 17) and [Apache Maven](https://maven.apache.org) (version >= 3.5) + +### Start dependencies + +If you chose to run [MongoDB](https://mongodb.com) and [Keycloak](https://keycloak.org) via containers, you'll +need to open a first terminal and run: + +```shell +$ cd dev +$ ./start-mongodb-docker.sh +# or ./start-mongodb-podman.sh if you prefer Podman +``` + +MongoDB is started on port `27017`. + +Keycloak is optional depending on your will to try out authentication and authorization features. +If you need Keycloak, open a second terminal and run: + +```shell +$ cd dev +$ ./start-keycloak.sh +# or ./start-keycloak-podman.sh if you prefer Podman +``` + +Keycloak is started on port `8180`. + +### Start servers + +In a terminal, start frontend GUI server using NG : + +```shell +$ cd src/main/webapp +$ npm install --legacy-peer-deps +$ ng serve +``` + +Server is started on port `4200`. Open a new browser tab pointing to `http://localhost:4200` where application is hosted. + +with Keycloak: + +```shell +$ mvn spring-boot:run +``` + +with Keycloak disabled: + +```shell +$ KEYCLOAK_ENABLED=false mvn spring-boot:run +``` + +Server is started on port `8080` and will be used as API endpoints root by frontend GUI (URLs starting by `http://localhost:4200/api` will be in fact proxied to port `8080`). + +### Formatting and linting + +We use [Spotless](https://github.com/diffplug/spotless) to format our Java code. Spotless is configured using an Eclipse +code style file, which is located at `eclipse-formatter.xml` in the root folder of this repository. Spotless check is automatically +triggered during the build process, but you can also run it manually using the following command: + +```shell +$ mvn spotless:apply +``` \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/pom.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/pom.xml new file mode 100644 index 000000000..da19e3e00 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/pom.xml @@ -0,0 +1,581 @@ + + + + microcks + io.github.microcks + 1.12.2-SNAPSHOT + + 4.0.0 + + Microcks App + microcks-app + + + UTF-8 + .. + microcks + microcks + 21 + 1.78 + 2.2 + 1.45.0 + 5.3 + 3.18.0 + 2.16.1 + 1.17.1 + 4.0.26 + 3.25.5 + 1.70.0 + 3.21.8 + 0.18.2 + 1.33.6 + + + + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + + + + io.github.microcks + microcks-util + ${project.version} + + + io.github.microcks + microcks-model + ${project.version} + + + io.github.microcks + microcks-el + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.micrometer + micrometer-registry-prometheus + + + + + io.opentelemetry.javaagent + opentelemetry-javaagent + ${opentelemetry.version} + provided + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-annotations + ${opentelemetry.version} + + + + org.springframework.kafka + spring-kafka + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.bouncycastle + bcpkix-jdk15to18 + ${bouncycastle.version} + + + commons-codec + commons-codec + ${commons-codec.version} + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + + org.apache.httpcomponents.client5 + httpclient5 + ${apache-httpclient.version} + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + commons-io + commons-io + ${commons-io.version} + + + + org.apache.groovy + groovy-xml + ${groovy.version} + + + org.apache.groovy + groovy-json + ${groovy.version} + + + org.apache.groovy + groovy-jsr223 + ${groovy.version} + + + org.apache.groovy + groovy-dateutil + ${groovy.version} + + + + io.grpc + grpc-netty-shaded + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + io.grpc + grpc-services + + + + org.apache.tomcat + annotations-api + 6.0.53 + provided + + + + com.google.protobuf + protobuf-java-util + ${protobuf.version} + + + io.github.microcks + protoc-jar + ${protoc-jar.version} + + + + com.theokanning.openai-gpt3-java + api + ${openai-gpt3-java.version} + + + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + test + + + de.bwaldvogel + mongo-java-server + ${mongo-java-server.version} + test + + + + org.skyscreamer + jsonassert + 1.5.1 + test + + + org.testcontainers + mongodb + 1.17.6 + test + + + junit + junit + + + + + com.github.dasniko + testcontainers-keycloak + 3.0.0 + test + + + org.testcontainers + junit-jupiter + 1.19.3 + test + + + + com.code-intelligence + jazzer-junit + 0.24.0 + test + + + + + + + src/main/resources + false + + + src/main/resources/config + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + -Xlint:deprecation + -Xlint:unchecked + -parameters + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/*Test.java + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + + + + + central + https://repo.maven.apache.org/maven2 + + + + + + + + + single + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + true + + + + + repackage + + + + + + + + + it + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/*IT.java + + false + + + + + + + fuzz + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/*Fuzz.java + + false + + 1 + + + + + + + fuzz + + + + coverage + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/*Test.java + **/*IT.java + + false + + + + + + + prod + + + + com.github.eirslett + frontend-maven-plugin + 1.15.1 + + + install node and npm + + install-node-and-npm + + generate-resources + + + npm install + + npm + + + install + + + + + ng build + + npm + + generate-resources + + run build + + + + + v18.20.5 + 10.8.2 + ${basedir}/src/main/webapp + target + + + + maven-clean-plugin + 2.5 + + + + src/main/webapp/dist + + + .tmp + + + + + + + maven-resources-plugin + 3.0.1 + + + copy-resources + generate-resources + + copy-resources + + + target/classes/public + + + src/main/webapp/dist/browser + false + + + ${basedir} + false + + bower_components/bootstrap/** + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + true + + prod + + + --spring.profiles.active=prod + + exec + + + + io.opentelemetry.javaagent + opentelemetry-javaagent + + + + + + + repackage + + + + + + com.spotify + docker-maven-plugin + 1.2.2 + + ${docker.image.prefix}/${docker.image.name} + src/main/docker + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + + + + + + + INFO + + prod + + + + dev-otel + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + -javaagent:${user.home}/Development/repository/io/opentelemetry/javaagent/opentelemetry-javaagent/${opentelemetry.version}/opentelemetry-javaagent-${opentelemetry.version}.jar + true + otlp + otlp + otlp + 10000 + 7000 + service.name=${project.artifactId},service.namespace=${project.artifactId}-ns,service.instance.id=${project.artifactId}-dev,service.version=${project.version} + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/Dockerfile b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/Dockerfile new file mode 100644 index 000000000..7b6ec72e2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/Dockerfile @@ -0,0 +1,60 @@ +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5-1742914212 + +# Some version information +LABEL maintainer="Laurent Broudoux " \ + org.opencontainers.image.authors="Laurent Broudoux " \ + org.opencontainers.image.title="Microcks Application" \ + org.opencontainers.image.description="Microcks is Open Source cloud-native native tool for API Mocking and Testing" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.documentation="https://github.com/microcks/microcks" \ + io.artifacthub.package.readme-url="https://raw.githubusercontent.com/microcks/microcks/master/README.md" + +# Install Java runtime +RUN microdnf install java-21-openjdk-headless openssl curl-minimal ca-certificates -y \ + && microdnf clean all \ + && rm /var/lib/rpm/rpmdb.sqlite \ + && mkdir -p /deployments + +# JAVA_APP_DIR is used by run-java.sh for finding the binaries +ENV JAVA_APP_DIR=/deployments \ + JAVA_MAJOR_VERSION=11 + +# Opentelemetry Agent Instrumentation +# By default we deactivate the agent with OTEL_JAVAAGENT_ENABLED=false +RUN curl https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.33.6/opentelemetry-javaagent.jar -Lo /opt/opentelemetry-javaagent.jar +ENV JAVA_TOOL_OPTIONS=-javaagent:/opt/opentelemetry-javaagent.jar \ + OTEL_JAVAAGENT_ENABLED=false \ + OTEL_EXPORTER_OTLP_ENDPOINT=http://o11y_otel:4317 \ + OTEL_METRICS_EXPORTER=otlp \ + OTEL_LOGS_EXPORTER=otlp \ + OTEL_TRACES_EXPORTER=otlp \ + OTEL_RESOURCE_ATTRIBUTES=service.name=microcks,service.namespace=microcks-ns,service.instance.id=microcks-cnt,service.version=1.11.0 + + +# Set working directory at /deployments +WORKDIR /deployments +VOLUME /deployments/config + +# Setup permissions for user '1001'. Necessary to permit running with a randomised UID +# Runtime user will need to be able to self-insert in /etc/passwd +# Also, use /dev/urandom to speed up startups +RUN chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && chmod g+rw /etc/passwd \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/1.3.8/run-java-sh-1.3.8-sh.sh \ + -o /deployments/run-java.sh \ + && mkdir -p /deployments/data \ + && chown 1001 /deployments/run-java.sh \ + && chmod 550 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /usr/lib/jvm/jre/lib/security/java.security + +# Gives uid +USER 1001 + +# Copy corresponding jar file +COPY target/*-exec.jar app.jar +EXPOSE 8080 + +# Run it +ENTRYPOINT [ "/deployments/run-java.sh" ] \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/agent-bond-opts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/agent-bond-opts new file mode 100644 index 000000000..24db9ad0f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/agent-bond-opts @@ -0,0 +1,123 @@ +#!/bin/sh + +# Parse options +while [ $# -gt 0 ] +do +key="$1" +case ${key} in + # Escape for scripts which eval the output of this script + -e|--escape) + escape_sep=1 + ;; +esac +shift +done + +# Check whether a given config is contained in AB_JOLOKIA_OPTS +is_in_jolokia_opts() { + local prop=$1 + if [ -n "${AB_JOLOKIA_OPTS:-}" ] && [ "${AB_JOLOKIA_OPTS}" != "${AB_JOLOKIA_OPTS/${prop}/}" ]; then + echo "yes" + else + echo "no" + fi +} + +dir=${AB_DIR:-/opt/agent-bond} +sep="=" + +# Options separators defined to avoid clash with fish-pepper templating +ab_open_del="{""{" +ab_close_del="}""}" +if [ -n "${escape_sep:-}" ]; then + ab_open_del='\{\{' + ab_close_del='\}\}' +fi + +if [ -z "${AB_OFF:-}" ]; then + opts="-javaagent:$dir/agent-bond.jar" + config="${AB_CONFIG:-$dir/agent-bond.properties}" + if [ -f "$config" ]; then + # Configuration takes precedence + opts="${opts}${sep}config=${config}" + sep="," + fi + if [ -z "${AB_ENABLED:-}" ] || [ "${AB_ENABLED}" != "${AB_ENABLED/jolokia/}" ]; then + # Direct options only if no configuration is found + jsep="" + jolokia_opts="" + if [ -n "${AB_JOLOKIA_CONFIG:-}" ] && [ -f "${AB_JOLOKIA_CONFIG}" ]; then + jolokia_opts="${jolokia_opts}${jsep}config=${AB_JOLOKIA_CONFIG}" + jsep="," + grep -q -e '^host' ${AB_JOLOKIA_CONFIG} && host_in_config=1 + fi + if [ -z "${AB_JOLOKIA_HOST:-}" ] && [ -z "${host_in_config:-}" ]; then + AB_JOLOKIA_HOST='0.0.0.0' + fi + if [ -n "${AB_JOLOKIA_HOST:-}" ]; then + jolokia_opts="${jolokia_opts}${jsep}host=${AB_JOLOKIA_HOST}" + jsep="," + fi + if [ -n "${AB_JOLOKIA_PORT:-}" ]; then + jolokia_opts="${jolokia_opts}${jsep}port=${AB_JOLOKIA_PORT}" + jsep="," + fi + if [ -n "${AB_JOLOKIA_USER:-}" ]; then + jolokia_opts="${jolokia_opts}${jsep}user=${AB_JOLOKIA_USER}" + jsep="," + fi + if [ -n "${AB_JOLOKIA_PASSWORD:-}" ]; then + jolokia_opts="${jolokia_opts}${jsep}password=${AB_JOLOKIA_PASSWORD}" + jsep="," + fi + if [ -n "${AB_JOLOKIA_HTTPS:-}" ]; then + jolokia_opts="${jolokia_opts}${jsep}protocol=https" + https_used=1 + jsep="," + fi + # Integration with OpenShift client cert auth + if [ -n "${AB_JOLOKIA_AUTH_OPENSHIFT:-}" ]; then + auth_opts="useSslClientAuthentication=true,extraClientCheck=true" + if [ -z "${https_used+x}" ]; then + auth_opts="${auth_opts},protocol=https" + fi + if [ $(is_in_jolokia_opts "caCert") != "yes" ]; then + auth_opts="${auth_opts},caCert=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + fi + if [ $(is_in_jolokia_opts "clientPrincipal") != "yes" ]; then + if [ "${AB_JOLOKIA_AUTH_OPENSHIFT}" != "${AB_JOLOKIA_AUTH_OPENSHIFT/=/}" ]; then + # Supposed to contain a principal name to check + auth_opts="${auth_opts},clientPrincipal=$(echo ${AB_JOLOKIA_AUTH_OPENSHIFT} | sed -e 's/ /\\\\ /g')" + else + auth_opts="${auth_opts},clientPrincipal=cn=system:master-proxy" + fi + fi + jolokia_opts="${jolokia_opts}${jsep}${auth_opts}" + jsep="," + fi + # Add extra opts to the end + if [ -n "${AB_JOLOKIA_OPTS:-}" ]; then + jolokia_opts="${jolokia_opts}${jsep}${AB_JOLOKIA_OPTS}" + jsep="," + fi + + opts="${opts}${sep}jolokia${ab_open_del}${jolokia_opts}${ab_close_del}" + sep="," + fi + if [ -z "${AB_ENABLED:-}" ] || [ "${AB_ENABLED}" != "${AB_ENABLED/jmx_exporter/}" ]; then + je_opts="" + jsep="" + if [ -n "${AB_JMX_EXPORTER_OPTS:-}" ]; then + opts="${opts}${sep}jmx_exporter${ab_open_del}${AB_JMX_EXPORTER_OPTS}${ab_close_del}" + sep="," + else + port=${AB_JMX_EXPORTER_PORT:-9779} + config=${AB_JMX_EXPORTER_CONFIG:-/opt/agent-bond/jmx_exporter_config.yml} + opts="${opts}${sep}jmx_exporter${ab_open_del}${port}:${config}${ab_close_del}" + sep="," + fi + fi + if [ "${sep:-}" != '=' ] ; then + echo ${opts} + fi +fi \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/jmx_exporter_config.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/jmx_exporter_config.yml new file mode 100644 index 000000000..ab9aeee83 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/docker/jmx_exporter_config.yml @@ -0,0 +1,28 @@ +--- +lowercaseOutputName: true +lowercaseOutputLabelNames: true +whitelistObjectNames: + - 'org.apache.camel:*' +rules: + - pattern: '^org.apache.camel<>((?:Min|Mean|Max|Last|Delta)(?:ProcessingTime)):' + name: camel_routes_$3 + labels: + name: $2 + context: $1 + - pattern: '^org.apache.camel<>(TotalProcessingTime):' + type: COUNTER + name: camel_routes_$3 + labels: + name: $2 + context: $1 + - pattern: '^org.apache.camel<>(ExchangesInflight|LastProcessingTime):' + name: camel_routes_$3 + labels: + name: $2 + context: $1 + - pattern: '^org.apache.camel<>((?:Exchanges(?:Completed|Failed|Total))|FailuresHandled):' + type: COUNTER + name: camel_routes_$3 + labels: + name: $2 + context: $1 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/MicrocksApplication.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/MicrocksApplication.java new file mode 100644 index 000000000..6da0ea862 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/MicrocksApplication.java @@ -0,0 +1,40 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * Startup class for Application. + * @author laurent + */ +@SpringBootApplication +@EnableAsync +@EnableScheduling +public class MicrocksApplication { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(MicrocksApplication.class); + + public static void main(String[] args) { + SpringApplication.run(MicrocksApplication.class, args); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/AICopilotConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/AICopilotConfiguration.java new file mode 100644 index 000000000..7570f1306 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/AICopilotConfiguration.java @@ -0,0 +1,93 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.util.ai.AICopilot; +import io.github.microcks.util.ai.OpenAICopilot; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Arrays; +import java.util.Map; + +/** + * Application configuration in charge of building AI Copilot instance. + * @author laurent + */ +@Configuration +@ConfigurationProperties("ai-copilot") +public class AICopilotConfiguration { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(AICopilotConfiguration.class); + + private boolean enabled = false; + + private String implementation; + + private Map openai; + + /** + * Enable the usage of AICopilot feature. Default is false. In that case, a null implementation will be provided. + * @param enabled Flag telling if AICopilot is activated. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Set the name of AICopilot implementation to later build. + * @param implementation THe name of implementation to build. + */ + public void setImplementation(String implementation) { + this.implementation = implementation; + } + + /** + * Set OpenAI implementation configuration properties. + * @param openai OpenAI configuration properties. + */ + public void setOpenai(Map openai) { + this.openai = openai; + } + + /** + * Build and configure a suitable AICopilot based on application properties. + * @return An AICopilot implementation configured as specified. + */ + @Bean + public AICopilot aiCopilotImplementation() { + if (enabled && implementation != null) { + log.info("AICopilot is enabled with implementation '{}'", implementation); + + if ("openai".equals(implementation)) { + // Check the presence of mandatory keys. + if (Arrays.stream(OpenAICopilot.getMandatoryConfigKeys()).allMatch(key -> openai.containsKey(key))) { + return new OpenAICopilot(openai); + } else { + log.warn("At least one mandatory configuration is missing for OpenAI AICopilot implementation"); + log.warn("Mandatory configuration keys are: {}", OpenAICopilot.getMandatoryConfigKeys()); + } + } + } + log.info("AICopilot is disabled"); + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ConfigurationConstants.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ConfigurationConstants.java new file mode 100644 index 000000000..3696f19cd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ConfigurationConstants.java @@ -0,0 +1,30 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +/** + * Definition of configuration constants. + * @author laurent + */ +public class ConfigurationConstants { + + private ConfigurationConstants() { + } + + public static final String PROFILE_DEVELOPMENT = "dev"; + public static final String PROFILE_PRODUCTION = "prod"; + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/GraphQLParserConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/GraphQLParserConfiguration.java new file mode 100644 index 000000000..3eb5588ff --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/GraphQLParserConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import graphql.parser.ParserOptions; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration class to set up GraphQL parser options. This configuration allows to set the maximum number of + * characters and tokens for the GraphQL parser. The values are read from application properties. + * @author laurent + */ +@Configuration +public class GraphQLParserConfiguration { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(GraphQLParserConfiguration.class); + + @Value("${graphql.parser.max-characters:#{null}}") + private Integer maxCharacters; + + @Value("${graphql.parser.max-tokens:#{null}}") + private Integer maxTokens; + + @PostConstruct + public void configureGraphQLParser() { + // Override the default ParserOptions with the ones defined in application.properties. + if (maxCharacters != null) { + log.info("Setting GraphQLParser maxChart to: {}", maxCharacters); + ParserOptions.setDefaultParserOptions( + ParserOptions.getDefaultParserOptions().transform(opts -> opts.maxCharacters(maxCharacters))); + } + if (maxTokens != null) { + log.info("Setting GraphQLParser maxTokens to: {}", maxTokens); + ParserOptions.setDefaultParserOptions( + ParserOptions.getDefaultParserOptions().transform(opts -> opts.maxTokens(maxTokens))); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/MicrometerConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/MicrometerConfiguration.java new file mode 100644 index 000000000..5d08bf8f4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/MicrometerConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import java.util.Map; + +import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.observation.ServerRequestObservationContext; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerMapping; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; + +/** + * Allows the configuration of the Micrometer observation, adding extra tags for tracking service and version in + * metrics. + */ +@Component +public class MicrometerConfiguration extends DefaultServerRequestObservationConvention { + + @Override + public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) { + // Here, we just want to have an additional KeyValue to the observation, keeping the default values. + return super.getLowCardinalityKeyValues(context).and(additionalTags(context)); + } + + protected KeyValues additionalTags(ServerRequestObservationContext context) { + KeyValues keyValues = KeyValues.empty(); + + Map pathVariables = (Map) context.getCarrier() + .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + if (pathVariables != null) { + String service = pathVariables.get("service"); + String version = pathVariables.get("version"); + if (service != null) { + keyValues = keyValues.and(KeyValue.of("service", service)); + } + if (version != null) { + keyValues = keyValues.and(KeyValue.of("version", version)); + } + } + return keyValues; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/MongoConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/MongoConfiguration.java new file mode 100644 index 000000000..be32b657b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/MongoConfiguration.java @@ -0,0 +1,75 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.domain.ServiceState; + +import com.mongodb.WriteConcern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.WriteConcernResolver; +import org.springframework.data.mongodb.core.index.IndexOperations; +import org.springframework.data.mongodb.core.index.IndexResolver; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; + +/** + * Configuration object for MongoDB backend. + * @author laurent + */ +@Configuration +public class MongoConfiguration { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(MongoConfiguration.class); + + MongoTemplate mongoTemplate; + + /** + * Build a MongoConfiguration with required dependencies. + * @param mongoTemplate Template for updating Mongo indexes + */ + MongoConfiguration(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @EventListener(ContextRefreshedEvent.class) + public void initIndicesAfterStartup() { + log.info("Ensuring TTL index for ServiceState"); + MappingContext, MongoPersistentProperty> mappingContext = mongoTemplate + .getConverter().getMappingContext(); + + IndexResolver resolver = new MongoPersistentEntityIndexResolver(mappingContext); + IndexOperations indexOps = mongoTemplate.indexOps(ServiceState.class); + + resolver.resolveIndexFor(ServiceState.class).forEach(indexOps::ensureIndex); + } + + @Bean + public WriteConcernResolver writeConcernResolver() { + return action -> { + log.info("Using Write Concern of Acknowledged"); + return WriteConcern.ACKNOWLEDGED; + }; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ObjectMapperFactoryConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ObjectMapperFactoryConfiguration.java new file mode 100644 index 000000000..54370b9e9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ObjectMapperFactoryConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.util.ObjectMapperFactory; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +/** + * Allows the configuration of the ObjectMapperFactory at startup time of Microcks application. + * @author laurent + */ +@Configuration +public class ObjectMapperFactoryConfiguration { + + @Value("${spring.servlet.multipart.max-file-size}") + private String maxUploadedFileSize; + + @PostConstruct + public void configureObjectMapperFactory() { + ObjectMapperFactory.configureMaxFileSize(maxUploadedFileSize); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ProxyConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ProxyConfiguration.java new file mode 100644 index 000000000..a9dd8f153 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ProxyConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.util.UsernamePasswordProxyAuthenticator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import java.net.Authenticator; + +/** + * @author laurent + */ +@Component +public class ProxyConfiguration { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(ProxyConfiguration.class); + + @Value("${network.proxyHost}") + private String proxyHost; + + @Value("${network.nonProxyHosts}") + private String nonProxyHosts; + + @Value("${network.proxyPort}") + private Integer proxyPort; + + @Value("${network.proxyUsername}") + private String proxyUsername; + + @Value("${network.proxyPassword}") + private String proxyPassword; + + @Bean + public ProxySettings buildProxySettings() { + // Initialize ProxySettings if provided. + if (proxyHost != null && proxyPort != null) { + log.info("Configuring HTTP(S) proxy to {}:{}", proxyHost, proxyPort); + System.setProperty("http.proxyHost", proxyHost); + System.setProperty("http.proxyPort", proxyPort.toString()); + System.setProperty("http.nonProxyHosts", nonProxyHosts); + System.setProperty("https.proxyHost", proxyHost); + System.setProperty("https.proxyPort", proxyPort.toString()); + System.setProperty("https.nonProxyHosts", nonProxyHosts); + + // From jdk 8.111+ we also need to remove disabled schemes to allow Basic authentication + // for HTTPS for example (see http://www.oracle.com/technetwork/java/javase/8u111-relnotes-3124969.html). + if (proxyUsername != null && proxyPassword != null) { + System.setProperty("jdk.http.auth.tunneling.disabledSchemes", ""); + System.setProperty("jdk.http.auth.proxying.disabledSchemes", ""); + } + + // Build settings and set Authenticator for the whole JVM. + ProxySettings settings = new ProxySettings(proxyHost, proxyPort, proxyUsername, proxyPassword); + Authenticator.setDefault(new UsernamePasswordProxyAuthenticator(settings)); + + return settings; + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ProxySettings.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ProxySettings.java new file mode 100644 index 000000000..c4490d2a3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/ProxySettings.java @@ -0,0 +1,58 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +/** + * This is a bean for holding HTTP/HTTPS proxy authentication settings. + * @author laurent + */ +public class ProxySettings { + + private String host; + private Integer port; + private String username; + private String password; + + /** + * Build a new proxy settings bean. + * @param host The network proxy host + * @param port The network proxy port + * @param username The proxy username + * @param password The proxy credentials + */ + public ProxySettings(String host, Integer port, String username, String password) { + this.host = host; + this.port = port; + this.username = username; + this.password = password; + } + + public String getHost() { + return host; + } + + public Integer getPort() { + return port; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/SecurityConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/SecurityConfiguration.java new file mode 100644 index 000000000..7db52ea19 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/SecurityConfiguration.java @@ -0,0 +1,118 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.security.MicrocksJwtConverter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +import static io.github.microcks.security.AuthorizationChecker.*; + +/** + * A bean responsible for Security filter chain configuration using Spring Security ODIC adapters. + * @author laurent + */ +@Configuration +@EnableWebSecurity +@EnableAutoConfiguration(exclude = { OAuth2ClientAutoConfiguration.class }) +public class SecurityConfiguration { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + + @Value("${keycloak.enabled}") + private final Boolean keycloakEnabled = true; + + @Bean + public SecurityFilterChain configureMockSecurityFilterChain(HttpSecurity http) throws Exception { + log.info("Starting security configuration for mocks"); + + // State-less session (state in access-token only) + http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + // Disable CSRF because of state-less session-management + http.csrf(csrf -> csrf.disable()); + + // Disable CORS as we already have a filter in WebConfiguration that does the job + http.cors(cors -> cors.disable()); + + http.securityMatcher("/rest/**", "/graphql/**", "/soap/**") + .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()); + + // Disable the publication of X-Frame-Options to allow embedding the UI. + // See https://github.com/microcks/microcks/issues/952 + http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)); + + return http.build(); + } + + @Bean + public SecurityFilterChain configureAPISecurityFilterChain(HttpSecurity http) throws Exception { + log.info("Starting security configuration for APIs"); + + // State-less session (state in access-token only) + http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + // Disable CSRF because of state-less session-management + http.csrf(csrf -> csrf.disable()); + + // Disable CORS as we already have a filter in WebConfiguration that does the job + http.cors(cors -> cors.disable()); + if (Boolean.TRUE.equals(keycloakEnabled)) { + log.info("Keycloak is enabled, configuring oauth2 & request authorization"); + + // spotless:off + http.authorizeHttpRequests(registry -> registry + .requestMatchers(HttpMethod.GET, "/api/services/*").hasAnyRole(ROLE_USER) + .requestMatchers("/api/services", "/api/jobs", "/api/jobs/*").hasAnyRole(ROLE_USER, ROLE_MANAGER, ROLE_ADMIN) + .requestMatchers("/api/services/*").hasAnyRole(ROLE_MANAGER, ROLE_ADMIN) + .requestMatchers("/api/services/*/*").hasAnyRole(ROLE_MANAGER, ROLE_ADMIN) + .requestMatchers("/api/jobs/*/*").hasAnyRole(ROLE_MANAGER, ROLE_ADMIN) + .requestMatchers("/api/artifact/*").hasAnyRole(ROLE_MANAGER, ROLE_ADMIN) + .requestMatchers("/api/import", "/api/export").hasAnyRole(ROLE_ADMIN) + .requestMatchers(HttpMethod.GET, "/api/secrets").hasAnyRole(ROLE_USER, ROLE_MANAGER, ROLE_ADMIN) + .requestMatchers(HttpMethod.GET, "/api/secrets/*").hasAnyRole(ROLE_USER, ROLE_MANAGER, ROLE_ADMIN) + .requestMatchers(HttpMethod.POST, "/api/secrets").hasAnyRole(ROLE_ADMIN) + .requestMatchers(HttpMethod.PUT, "/api/secrets/*").hasAnyRole(ROLE_ADMIN) + .requestMatchers(HttpMethod.DELETE, "/api/secrets/*").hasAnyRole(ROLE_ADMIN).anyRequest().permitAll()); + // spotless:on + + http.oauth2ResourceServer(oauth2Configurer -> oauth2Configurer + .jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(new MicrocksJwtConverter()))); + } else { + log.info("Keycloak is disabled, permitting all requests"); + http.authorizeHttpRequests(registry -> registry.anyRequest().permitAll()); + } + + // Disable the publication of X-Frame-Options to allow embedding the UI. + // See https://github.com/microcks/microcks/issues/952 + http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)); + + return http.build(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/WebConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/WebConfiguration.java new file mode 100644 index 000000000..b433fffd0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/WebConfiguration.java @@ -0,0 +1,79 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.web.filter.CorsFilter; +import io.github.microcks.web.filter.DynamicCorsFilter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; + +import java.util.Arrays; +import java.util.EnumSet; + +/** + * Spring Web configuration class. + * @author laurent + */ +@Configuration +public class WebConfiguration implements ServletContextInitializer { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(WebConfiguration.class); + + @Autowired + private Environment env; + + @Value("${mocks.rest.enable-cors-policy}") + private Boolean enableCorsPolicy = null; + @Value("${mocks.rest.cors.allowedOrigins}") + private String corsAllowedOrigins; + @Value("${mocks.rest.cors.allowCredentials}") + private Boolean corsAllowCredentials; + + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + log.info("Starting web application configuration, using profiles: {}", Arrays.toString(env.getActiveProfiles())); + EnumSet disps = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC); + initCORSFilter(servletContext, disps); + log.info("Web application fully configured"); + } + + + /** Configure the CORS filter on API endpoints as well as on Mock endpoints. */ + private void initCORSFilter(ServletContext servletContext, EnumSet disps) { + FilterRegistration.Dynamic corsFilter = servletContext.addFilter("corsFilter", new CorsFilter()); + corsFilter.addMappingForUrlPatterns(disps, true, "/api/*"); + corsFilter.addMappingForUrlPatterns(disps, true, "/dynarest/*"); + corsFilter.setAsyncSupported(true); + if (Boolean.TRUE.equals(enableCorsPolicy)) { + FilterRegistration.Dynamic dynamicCorsFilter = servletContext.addFilter("dynamicCorsFilter", + new DynamicCorsFilter(corsAllowedOrigins, corsAllowCredentials)); + dynamicCorsFilter.addMappingForUrlPatterns(disps, true, "/rest/*"); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/WebMvcConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/WebMvcConfiguration.java new file mode 100644 index 000000000..7a1b75223 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/config/WebMvcConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.config; + +import io.github.microcks.security.UserInfoHandlerMethodArgumentResolver; +import io.github.microcks.security.UserInfoInContextInterceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * @author laurent + */ +@Configuration +public class WebMvcConfiguration implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new UserInfoInContextInterceptor()).addPathPatterns("/api/services/**"); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new UserInfoHandlerMethodArgumentResolver()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/MockInvocationEvent.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/MockInvocationEvent.java new file mode 100644 index 000000000..f0dd288e1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/MockInvocationEvent.java @@ -0,0 +1,77 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.event; + +import org.springframework.context.ApplicationEvent; + +import java.util.Date; + +/** + * Simple bean representing an invocation event on a Mock. + * @author laurent + */ +public class MockInvocationEvent extends ApplicationEvent { + + /** */ + private final String serviceName; + /** */ + private final String serviceVersion; + /** */ + private final String mockResponse; + /** */ + private final Date invocationTimestamp; + /** */ + private final long duration; + + /** + * Create a new mock invocation event. + * @param source Source object for event + * @param serviceName Name of invoked service + * @param serviceVersion Version of invoked service + * @param mockResponse Mock response returned during invocation + * @param invocationTimestamp Timestamp of invocation + * @param duration Duration of invocation + */ + public MockInvocationEvent(Object source, String serviceName, String serviceVersion, String mockResponse, + Date invocationTimestamp, long duration) { + super(source); + this.serviceName = serviceName; + this.serviceVersion = serviceVersion; + this.mockResponse = mockResponse; + this.invocationTimestamp = invocationTimestamp; + this.duration = duration; + } + + public String getServiceName() { + return serviceName; + } + + public String getServiceVersion() { + return serviceVersion; + } + + public String getMockResponse() { + return mockResponse; + } + + public Date getInvocationTimestamp() { + return invocationTimestamp; + } + + public long getDuration() { + return duration; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/ServiceChangeEvent.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/ServiceChangeEvent.java new file mode 100644 index 000000000..e78b47b02 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/ServiceChangeEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.event; + +import org.springframework.context.ApplicationEvent; + +/** + * Event raised when a Service has been changed (CREATED, UPDATED or DELETED). + * @author laurent + */ +public class ServiceChangeEvent extends ApplicationEvent { + + /** */ + private final String serviceId; + /** */ + private final ChangeType changeType; + + /** + * Creates a new {@code ServiceChangeEvent} with change type. + * @param source Source object for event + * @param serviceId Identifier of the updated Service + * @param changeType Type of changes this event if representing + */ + public ServiceChangeEvent(Object source, String serviceId, ChangeType changeType) { + super(source); + this.serviceId = serviceId; + this.changeType = changeType; + } + + public String getServiceId() { + return serviceId; + } + + public ChangeType getChangeType() { + return changeType; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/ServiceViewChangeEventSerializer.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/ServiceViewChangeEventSerializer.java new file mode 100644 index 000000000..4bcc67bf7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/ServiceViewChangeEventSerializer.java @@ -0,0 +1,40 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.event; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.kafka.common.errors.SerializationException; +import org.apache.kafka.common.serialization.Serializer; + +/** + * A Kafka serializer for ServiceView using Jackson ObjectMapper serialization. + * @author laurent + */ +public class ServiceViewChangeEventSerializer implements Serializer { + + private ObjectMapper mapper = new ObjectMapper(); + + @Override + public byte[] serialize(String topic, ServiceViewChangeEvent serviceViewChangeEvent) { + try { + return mapper.writeValueAsBytes(serviceViewChangeEvent); + } catch (JsonProcessingException e) { + throw new SerializationException("Error serializing serviceViewChangeEvent", e); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/TestCompletionEvent.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/TestCompletionEvent.java new file mode 100644 index 000000000..81aad79d8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/event/TestCompletionEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.event; + +import io.github.microcks.domain.TestResult; + +import org.springframework.context.ApplicationEvent; + +/** + * Event raised when a Service Test is completed. + * @author laurent + */ +public class TestCompletionEvent extends ApplicationEvent { + + /** The completed TestResult at the end of test. */ + private final TestResult result; + + /** + * Creates a new {@code TestCompletionEvent} with test result. + * @param source Source object for event + * @param result The TestResult after completion + */ + public TestCompletionEvent(Object source, TestResult result) { + super(source); + this.result = result; + } + + public TestResult getResult() { + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/DailyStatisticsFeeder.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/DailyStatisticsFeeder.java new file mode 100644 index 000000000..f0a4791bb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/DailyStatisticsFeeder.java @@ -0,0 +1,175 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import io.github.microcks.domain.DailyStatistic; +import io.github.microcks.event.MockInvocationEvent; +import io.github.microcks.repository.DailyStatisticRepository; + +import jakarta.annotation.PreDestroy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Application event listener that updates daily statistics on incoming event. + * @author laurent + */ +@Component +public class DailyStatisticsFeeder implements StatisticsFlusher, ApplicationListener { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(DailyStatisticsFeeder.class); + + private final DailyStatisticRepository statisticsRepository; + private final ScheduledExecutorService scheduler; + private final ConcurrentHashMap statisticsCache = new ConcurrentHashMap<>(); + + /** + * Build a DailyStatisticsFeeder with mandatory dependencies. + * @param statisticsRepository The repository to access statictics. + */ + public DailyStatisticsFeeder(DailyStatisticRepository statisticsRepository) { + this.statisticsRepository = statisticsRepository; + scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleAtFixedRate(this::flushToDatabase, 0, 10, TimeUnit.SECONDS); + } + + /** + * Flush the statistics cache to the database. This method is called periodically by the scheduler. It copies the + * current statistics cache to a local map, clears the cache, and then processes each entry to update or create daily + * statistics in the database. + */ + public void flushToDatabase() { + if (!statisticsCache.isEmpty()) { + // copy the cache to a local map to avoid concurrent modification issues + Map statisticsCache = new HashMap<>(this.statisticsCache); + // Clear the cache after copying + this.statisticsCache.clear(); + + for (Map.Entry entry : statisticsCache.entrySet()) { + String[] keys = entry.getKey().split(":"); + if (keys.length == 5) { + String serviceName = keys[0]; + String serviceVersion = keys[1]; + String day = keys[2]; + String hourKey = keys[3]; + String minuteKey = keys[4]; + int count = entry.getValue(); + // First check if there's a statistic document for invocation day. + DailyStatistic statistic = null; + List statistics = statisticsRepository.findByDayAndServiceNameAndServiceVersion(day, + serviceName, serviceVersion); + if (!statistics.isEmpty()) { + statistic = statistics.getFirst(); + } + + if (statistic == null) { + // No statistic's yet... + log.debug("There's no statistics for {} yet. Create one.", day); + // Initialize a new 0 filled structure. + statistic = new DailyStatistic(); + statistic.setDay(day); + statistic.setServiceName(serviceName); + statistic.setServiceVersion(serviceVersion); + statistic.setHourlyCount(initializeHourlyMap()); + statistic.setMinuteCount(initializeMinuteMap()); + // Now set first values before saving. + statistic.setDailyCount(count); + statistic.getHourlyCount().put(hourKey, count); + statistic.getMinuteCount().put(minuteKey, count); + statisticsRepository.save(statistic); + } else { + // Already a statistic document for this day, increment fields. + log.debug("Found an existing statistic document for {}", day); + statisticsRepository.incrementDailyStatistic(day, serviceName, serviceVersion, hourKey, minuteKey, + count); + } + } else { + log.warn("Invalid key format in statistics cache: {}", entry.getKey()); + } + } + } + } + + @Override + @Async + public void onApplicationEvent(MockInvocationEvent event) { + log.debug("Received a MockInvocationEvent on {} - v{}", event.getServiceName(), event.getServiceVersion()); + + // Compute day string representation. + Calendar calendar = Calendar.getInstance(); + calendar.setTime(event.getInvocationTimestamp()); + + // Computing keys based on invocation date. + int month = calendar.get(Calendar.MONTH) + 1; + String monthStr = (month < 10 ? "0" : "") + month; + int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + String dayOfMonthStr = (dayOfMonth < 10 ? "0" : "") + dayOfMonth; + + String day = calendar.get(Calendar.YEAR) + monthStr + dayOfMonthStr; + String hourKey = String.valueOf(calendar.get(Calendar.HOUR_OF_DAY)); + String minuteKey = String.valueOf((60 * calendar.get(Calendar.HOUR_OF_DAY)) + calendar.get(Calendar.MINUTE)); + + String key = event.getServiceName() + ":" + event.getServiceVersion() + ":" + day + ":" + hourKey + ":" + + minuteKey; + statisticsCache.merge(key, 1, Integer::sum); + if (log.isDebugEnabled()) { + log.debug("hourKey for statistic is {}", hourKey); + log.debug("minuteKey for statistic is {}", minuteKey); + } + } + + /** + * Shutdown the scheduler when the application context is closed. + */ + @PreDestroy + public void shutdown() { + log.debug("Shutting down DailyStatisticsFeeder scheduler..."); + // Flush remaining statistics to database. + this.flushToDatabase(); + scheduler.shutdown(); + log.debug("DailyStatisticsFeeder scheduler shutdown complete."); + } + + + private Map initializeHourlyMap() { + Map result = new HashMap<>(24); + for (int i = 0; i < 24; i++) { + result.put(String.valueOf(i), 0); + } + return result; + } + + private Map initializeMinuteMap() { + Map result = new HashMap<>(24 * 60); + for (int i = 0; i < 24 * 60; i++) { + result.put(String.valueOf(i), 0); + } + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/KafkaServiceChangeEventChannel.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/KafkaServiceChangeEventChannel.java new file mode 100644 index 000000000..a2a4703eb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/KafkaServiceChangeEventChannel.java @@ -0,0 +1,49 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import io.github.microcks.event.ServiceViewChangeEvent; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Profile; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +/** + * This is an implementation of {@code ServiceChangeEventChannel} that uses a Kafka topic as a destination recipient for + * {@code ServiceViewChangeEvent}. + * @author laurent + */ +@Component +@Profile({ "default", "prod" }) +@ConditionalOnProperty(value = "async-api.enabled", havingValue = "true", matchIfMissing = true) +public class KafkaServiceChangeEventChannel implements ServiceChangeEventChannel { + + private final KafkaTemplate kafkaTemplate; + + /** + * Build a new KafkaServiceChangeEventChannel from a KafkaTemplate. + * @param kafkaTemplate THe template used for sending Kafka messages. + */ + public KafkaServiceChangeEventChannel(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void sendServiceViewChangeEvent(ServiceViewChangeEvent event) throws Exception { + kafkaTemplate.send("microcks-services-updates", event.getServiceId(), event); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/ServiceChangeEventChannel.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/ServiceChangeEventChannel.java new file mode 100644 index 000000000..0dd6d4130 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/ServiceChangeEventChannel.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import io.github.microcks.event.ServiceViewChangeEvent; + +/** + * This represents a communication via the one me may publish events that relates services changes. + * @author laurent + */ +public interface ServiceChangeEventChannel { + + /** + * Send a change event on a Service complete view. + * @param event The event to send or propagate via this channel. + * @throws Exception if the case event cannot be sent + */ + void sendServiceViewChangeEvent(ServiceViewChangeEvent event) throws Exception; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/ServiceChangeEventPublisher.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/ServiceChangeEventPublisher.java new file mode 100644 index 000000000..ac6311768 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/ServiceChangeEventPublisher.java @@ -0,0 +1,114 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.event.ChangeType; +import io.github.microcks.event.ServiceChangeEvent; +import io.github.microcks.event.ServiceViewChangeEvent; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.service.MessageService; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.domain.ServiceView; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Application event listener that send a message on Kafka topic on incoming ServiceUpdateEvent. + * @author laurent + */ +@Component +@ConditionalOnProperty(value = "async-api.enabled", havingValue = "true", matchIfMissing = true) +public class ServiceChangeEventPublisher implements ApplicationListener { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ServiceChangeEventPublisher.class); + + + private final ServiceRepository serviceRepository; + private final MessageService messageService; + private final ServiceChangeEventChannel channel; + + + /** + * Create a new ServiceChangeEventPublisher with required dependencies. + * @param serviceRepository the repository for Service objects + * @param messageService the service for Message objects + * @param channel the channel for ServiceChangeEvent + */ + public ServiceChangeEventPublisher(ServiceRepository serviceRepository, MessageService messageService, + ServiceChangeEventChannel channel) { + this.serviceRepository = serviceRepository; + this.messageService = messageService; + this.channel = channel; + } + + @Override + @Async + public void onApplicationEvent(ServiceChangeEvent event) { + log.debug("Received a ServiceChangeEvent on {}", event.getServiceId()); + + ServiceView serviceView = null; + if (event.getChangeType() != ChangeType.DELETED) { + Service service = serviceRepository.findById(event.getServiceId()).orElse(null); + + if (service != null) { + // Put messages into a map where key is operation name. + Map> messagesMap = new HashMap<>(); + for (Operation operation : service.getOperations()) { + if (service.getType() == ServiceType.EVENT || service.getType() == ServiceType.GENERIC_EVENT) { + // If an event, we should explicitly retrieve event messages. + List events = messageService + .getEventByOperation(IdBuilder.buildOperationId(service, operation)); + messagesMap.put(operation.getName(), events); + } else { + // Otherwise we have traditional request / response pairs. + List pairs = messageService + .getRequestResponseByOperation(IdBuilder.buildOperationId(service, operation)); + messagesMap.put(operation.getName(), pairs); + } + } + + serviceView = new ServiceView(service, messagesMap); + } + } + + // Build and send a ServiceViewChangeEvent that wraps ServiceView. + ServiceViewChangeEvent serviceViewChangeEvent = new ServiceViewChangeEvent(event.getServiceId(), serviceView, + event.getChangeType(), System.currentTimeMillis()); + try { + channel.sendServiceViewChangeEvent(serviceViewChangeEvent); + log.debug("Processing of ServiceChangeEvent done !"); + } catch (Exception e) { + // This is best effort sending, just log the exception. + log.error("Failed sending ServiceChangeEvent to correct channel", e); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/StatisticsFlusher.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/StatisticsFlusher.java new file mode 100644 index 000000000..2dfe1f2b2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/StatisticsFlusher.java @@ -0,0 +1,27 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.listener; + +/** + * StatisticsFlusher is an interface for components that flush statistics to a database. + * @author laurent + */ +public interface StatisticsFlusher { + + /** Flush the statistics cache to the database. */ + void flushToDatabase(); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/TestConformanceMetricConfigurer.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/TestConformanceMetricConfigurer.java new file mode 100644 index 000000000..aa5a82504 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/TestConformanceMetricConfigurer.java @@ -0,0 +1,70 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import io.github.microcks.domain.Service; +import io.github.microcks.event.ChangeType; +import io.github.microcks.event.ServiceChangeEvent; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.service.MetricsService; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * Event Listener that deals with TestCoverageMetric operations depending on event type. + * @author laurent + */ +@Component +public class TestConformanceMetricConfigurer implements ApplicationListener { + + /** A commons logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(TestConformanceMetricConfigurer.class); + + private final ServiceRepository serviceRepository; + private final MetricsService metricsService; + + /** + * Create a new instance of TestConformanceMetricConfigurer with required dependencies. + * @param serviceRepository The repository for Service entities + * @param metricsService The service for metrics operations + */ + public TestConformanceMetricConfigurer(ServiceRepository serviceRepository, MetricsService metricsService) { + this.serviceRepository = serviceRepository; + this.metricsService = metricsService; + } + + @Override + @Async + public void onApplicationEvent(ServiceChangeEvent event) { + log.debug("Received a ServiceChangeEvent on {}", event.getServiceId()); + + if (event.getChangeType().equals(ChangeType.DELETED)) { + metricsService.removeTestConformanceMetric(event.getServiceId()); + } else { + Service service = serviceRepository.findById(event.getServiceId()).orElse(null); + if (service != null) { + metricsService.configureTestConformanceMetric(service); + } else { + log.warn("Service with id {} not found but not a DELETED event?!", event.getServiceId()); + } + } + log.debug("Processing of ServiceChangeEvent done !"); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/TestConformanceMetricUpdater.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/TestConformanceMetricUpdater.java new file mode 100644 index 000000000..77c2165fe --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/listener/TestConformanceMetricUpdater.java @@ -0,0 +1,54 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import io.github.microcks.event.TestCompletionEvent; +import io.github.microcks.service.MetricsService; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * Application event listener that updates TestCoverageMetric on test completion. + * @author laurent + */ +@Component +public class TestConformanceMetricUpdater implements ApplicationListener { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(TestConformanceMetricUpdater.class); + + private final MetricsService metricsService; + + /** + * Create a new instance of TestConformanceMetricUpdater with required dependencies. + * @param metricsService The service for metrics operations + */ + public TestConformanceMetricUpdater(MetricsService metricsService) { + this.metricsService = metricsService; + } + + @Override + @Async + public void onApplicationEvent(TestCompletionEvent event) { + log.debug("Received a TestCompletionEvent on {}", event.getResult().getId()); + metricsService.updateTestConformanceMetricOnTestResult(event.getResult()); + log.debug("Processing of TestCompletionEvent done !"); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomDailyStatisticRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomDailyStatisticRepository.java new file mode 100644 index 000000000..320e7a695 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomDailyStatisticRepository.java @@ -0,0 +1,57 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.DailyStatistic; + +import java.util.List; + +/** + * Custom repository interface for DailyStatistic domain objects. + * @author laurent + */ +public interface CustomDailyStatisticRepository { + + void incrementDailyStatistic(String day, String serviceName, String serviceVersion, String hourKey, String minuteKey, + int count); + + DailyStatistic aggregateDailyStatistics(String day); + + List aggregateDailyStatistics(String afterday, String beforeday); + + List findTopStatistics(String day, int limit); + + class InvocationCount { + String day; + Long number; + + public String getDay() { + return day; + } + + public void setDay(String day) { + this.day = day; + } + + public Long getNumber() { + return number; + } + + public void setNumber(Long number) { + this.number = number; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomGenericResourceRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomGenericResourceRepository.java new file mode 100644 index 000000000..beea3f2e7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomGenericResourceRepository.java @@ -0,0 +1,29 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.GenericResource; + +import java.util.List; + +/** + * Custom repository interface for GenericResource domain objects. + * @author laurent + */ +public interface CustomGenericResourceRepository { + + List findByServiceIdAndJSONQuery(String serviceId, String jsonQuery); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomImportJobRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomImportJobRepository.java new file mode 100644 index 000000000..23ab90af2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomImportJobRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.ImportJob; + +import java.util.List; +import java.util.Map; + +/** + * Custom repository interface for ImportJob domain objects. + * @author laurent + */ +public interface CustomImportJobRepository { + + List findByLabels(Map labels); + + List findByLabelsAndNameLike(Map labels, String name); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomServiceRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomServiceRepository.java new file mode 100644 index 000000000..56e3bac78 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomServiceRepository.java @@ -0,0 +1,80 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Service; + +import java.util.List; +import java.util.Map; + +/** + * Custom repository interface for Service domain objects. + * @author laurent + */ +public interface CustomServiceRepository { + + List findByIdIn(List ids); + + List findByLabels(Map labels); + + List findByLabelsAndNameLike(Map labels, String name); + + List countServicesByType(); + + List listLabels(); + + class ServiceCount { + String type; + int number; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + } + + class LabelValues { + String key; + String[] values; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String[] getValues() { + return values; + } + + public void setValues(String[] values) { + this.values = values; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomTestConformanceMetricRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomTestConformanceMetricRepository.java new file mode 100644 index 000000000..5998e2cfd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/CustomTestConformanceMetricRepository.java @@ -0,0 +1,29 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.WeightedMetricValue; + +import java.util.List; + +/** + * Custom repository interface for TestConformanceMetric domain objects. + * @author laurent + */ +public interface CustomTestConformanceMetricRepository { + + List aggregateTestConformanceMetric(); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/DailyStatisticRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/DailyStatisticRepository.java new file mode 100644 index 000000000..83dffe2eb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/DailyStatisticRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.DailyStatistic; + +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +/** + * Repository interface for DailyStatistic domain objects. + * @author laurent + */ +public interface DailyStatisticRepository + extends MongoRepository, CustomDailyStatisticRepository { + + List findByDayAndServiceNameAndServiceVersion(String day, String serviceName, String serviceVersion); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/DailyStatisticRepositoryImpl.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/DailyStatisticRepositoryImpl.java new file mode 100644 index 000000000..26224927b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/DailyStatisticRepositoryImpl.java @@ -0,0 +1,143 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.DailyStatistic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import java.util.List; + +import static org.springframework.data.domain.Sort.Direction.ASC; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; + +/** + * Implementation of CustomDailyStatisticRepository. + * @author laurent + */ +public class DailyStatisticRepositoryImpl implements CustomDailyStatisticRepository { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(DailyStatisticRepositoryImpl.class); + + @Autowired + private MongoTemplate template; + + @Override + public void incrementDailyStatistic(String day, String serviceName, String serviceVersion, String hourKey, + String minuteKey, int count) { + + // Build a query to select specific object within collection. + Query query = new Query( + Criteria.where("day").is(day).and("serviceName").is(serviceName).and("serviceVersion").is(serviceVersion)); + + // Other way to build a query using statically imported methods. + //Query queryShort = query(where("day").is(day).and("serviceName").is(serviceName).and("serviceVersion").is(serviceVersion)); + + // Build update to increment the 3 fields. + Update update = new Update().inc("dailyCount", count).inc("hourlyCount." + hourKey, count) + .inc("minuteCount." + minuteKey, count); + + // Do an upsert with find and modify. + template.findAndModify(query, update, DailyStatistic.class); + } + + @Override + public DailyStatistic aggregateDailyStatistics(String day) { + + // Build a query to pre-select the statistics that will be aggregated. + Query query = new Query(Criteria.where("day").is(day)); + + // Execute a MapReduce command. + MapReduceResults results = template.mapReduce(query, "dailyStatistic", + "classpath:mapDailyStatisticForADay.js", "classpath:reduceDailyStatisticForADay.js", + WrappedDailyStatistic.class); + + // Output some debug messages. + if (log.isDebugEnabled()) { + log.debug("aggregateDailyStatistics mapReduce for day " + day); + log.debug("aggregateDailyStatistics mapReduce result counts: " + results.getCounts()); + for (WrappedDailyStatistic wdt : results) { + log.debug("aggregateDailyStatistics mapReduce result value: " + wdt.getValue()); + } + } + + // We've got a result if we've got an output. + if (results.getCounts().getOutputCount() > 0) { + return results.iterator().next().getValue(); + } + + // Build and return an empty object otherwise? + DailyStatistic statistic = new DailyStatistic(); + statistic.setDay(day); + statistic.setDailyCount(0); + return statistic; + } + + @Override + public List aggregateDailyStatistics(String afterday, String beforeday) { + // Build a query to pre-select the statistics that will be aggregated. + Aggregation aggregation = newAggregation(match(Criteria.where("day").gte(afterday).lte(beforeday)), + group("day").sum("dailyCount").as("number"), project("number").and("day").previousOperation(), + sort(ASC, "day")); + AggregationResults results = template.aggregate(aggregation, DailyStatistic.class, + InvocationCount.class); + return results.getMappedResults(); + } + + @Override + public List findTopStatistics(String day, int limit) { + // Build a query selecting and sorting / limiting. + Query query = new Query(Criteria.where("day").is(day)).with(Sort.by(new Order(Direction.DESC, "dailyCount"))) + .limit(limit); + + return template.find(query, DailyStatistic.class); + } + + + /** Utility class used for wrapping a DailyStatistic object within MapReduce command results. */ + public class WrappedDailyStatistic { + private String id; + private DailyStatistic value; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public DailyStatistic getValue() { + return value; + } + + public void setValue(DailyStatistic value) { + this.value = value; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/EventMessageRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/EventMessageRepository.java new file mode 100644 index 000000000..6c3e15923 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/EventMessageRepository.java @@ -0,0 +1,39 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.EventMessage; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + +import java.util.List; + +/** + * Repository interface for EventMessage domain objects. + * @author laurent + */ +public interface EventMessageRepository extends MongoRepository { + + List findByOperationId(String operationId); + + List findByTestCaseId(String testCaseId); + + List findByOperationIdAndSourceArtifact(String operationId, String sourceArtifact); + + @Query("{ 'operationId' : {'$in' : ?0}}") + List findByOperationIdIn(List operationIds); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/GenericResourceRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/GenericResourceRepository.java new file mode 100644 index 000000000..2ed15e62d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/GenericResourceRepository.java @@ -0,0 +1,40 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.GenericResource; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +/** + * This is a Mongo repository interface for GenericResource domain objects. + * + * @author laurent + */ +public interface GenericResourceRepository + extends MongoRepository, CustomGenericResourceRepository { + + List findByServiceId(String serviceId, Pageable pageable); + + @Query(value = "{ 'reference' : true, 'serviceId' : ?0 }") + List findReferencesByServiceId(String serviceId); + + @Query(value = "{ 'serviceId' : ?0 }", count = true) + long countByServiceId(String serviceId); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/GenericResourceRepositoryImpl.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/GenericResourceRepositoryImpl.java new file mode 100644 index 000000000..7f52ecae5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/GenericResourceRepositoryImpl.java @@ -0,0 +1,62 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.GenericResource; +import org.bson.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.BasicQuery; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author laurent + */ +public class GenericResourceRepositoryImpl implements CustomGenericResourceRepository { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(GenericResourceRepositoryImpl.class); + + @Autowired + private MongoTemplate template; + + @Override + public List findByServiceIdAndJSONQuery(String serviceId, String jsonQuery) { + // First parse query document and prepare a list of key to rename then remove. + Document query = Document.parse(jsonQuery); + ArrayList keysToRemove = new ArrayList<>(); + + // Collect the keys of document that should be updated. + for (String key : query.keySet()) { + keysToRemove.add(key); + } + // Prefix all keys by payload. that is the nested document where we put resource in + // and remove all modified keys. + for (String keyToRemove : keysToRemove) { + query.append("payload." + keyToRemove, query.get(keyToRemove)); + query.remove(keyToRemove); + } + + // Finally, append serviceId criterion before launching selection. + query.append("serviceId", serviceId); + + return template.find(new BasicQuery(query.toJson()), GenericResource.class); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ImportJobRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ImportJobRepository.java new file mode 100644 index 000000000..b91581fc8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ImportJobRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.ImportJob; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + +import java.util.List; + +/** + * Repository interface for ImportJob domain objects. + * @author laurent + */ +public interface ImportJobRepository extends MongoRepository, CustomImportJobRepository { + + @Query("{ 'name' : {'$regex':?0, '$options':'i'}}") + List findByNameLike(String name); + + @Query("{ 'serviceRefs' : {'$elemMatch': {'serviceId':?0}}}") + List findByServiceRefId(String serviceRefId); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ImportJobRepositoryImpl.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ImportJobRepositoryImpl.java new file mode 100644 index 000000000..eb7deb20b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ImportJobRepositoryImpl.java @@ -0,0 +1,60 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.ImportJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +import java.util.List; +import java.util.Map; + +/** + * Implementation for CustomImportJobRepository. + * @author laurent + */ +public class ImportJobRepositoryImpl implements CustomImportJobRepository { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(ImportJobRepositoryImpl.class); + + @Autowired + private MongoTemplate template; + + @Override + public List findByLabels(Map labels) { + Query query = new Query(); + for (String labelKey : labels.keySet()) { + query.addCriteria(Criteria.where("metadata.labels." + labelKey).is(labels.get(labelKey))); + } + List results = template.find(query, ImportJob.class); + return results; + } + + @Override + public List findByLabelsAndNameLike(Map labels, String name) { + Query query = new Query(Criteria.where("name").regex(name, "i")); + for (String labelKey : labels.keySet()) { + query.addCriteria(Criteria.where("metadata.labels." + labelKey).is(labels.get(labelKey))); + } + List results = template.find(query, ImportJob.class); + return results; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/RequestRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/RequestRepository.java new file mode 100644 index 000000000..0aba37465 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/RequestRepository.java @@ -0,0 +1,39 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Request; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +/** + * Repository interface for Request domain objects. + * + * @author laurent + */ +public interface RequestRepository extends MongoRepository { + + List findByOperationId(String operationId); + + List findByTestCaseId(String testCaseId); + + List findByOperationIdAndSourceArtifact(String operationId, String sourceArtifact); + + @Query("{ 'operationId' : {'$in' : ?0}}") + List findByOperationIdIn(List operationIds); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ResourceRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ResourceRepository.java new file mode 100644 index 000000000..368d7988c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ResourceRepository.java @@ -0,0 +1,45 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +/** + * Repository interface for Resource domain objects. + * + * @author laurent + */ +public interface ResourceRepository extends MongoRepository { + + List findByName(String name); + + List findByServiceId(String serviceId); + + List findByServiceIdAndType(String serviceId, ResourceType type); + + List findByServiceIdAndSourceArtifact(String serviceId, String sourceArtifact); + + @Query("{ 'serviceId' : {'$in' : ?0}}") + List findByServiceIdIn(List serviceIds); + + @Query("{ 'mainArtifact': true, 'serviceId': ?0 }") + List findMainByServiceId(String serviceId); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ResponseRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ResponseRepository.java new file mode 100644 index 000000000..81bca3d4c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ResponseRepository.java @@ -0,0 +1,43 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Response; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +/** + * Repository interface for Response domain objects. + * + * @author laurent + */ +public interface ResponseRepository extends MongoRepository { + + List findByOperationId(String operationId); + + List findByTestCaseId(String testCaseId); + + List findByOperationIdAndName(String operationId, String name); + + List findByOperationIdAndDispatchCriteria(String operationId, String dispatchCriteria); + + List findByOperationIdAndSourceArtifact(String operationId, String sourceArtifact); + + @Query("{ 'operationId' : {'$in' : ?0}}") + List findByOperationIdIn(List operationIds); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/SecretRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/SecretRepository.java new file mode 100644 index 000000000..f54b95161 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/SecretRepository.java @@ -0,0 +1,35 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Secret; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +/** + * Repository interface for Service domain objects. + * + * @author laurent + */ +public interface SecretRepository extends MongoRepository { + + List findByName(String name); + + @Query("{'name' : {'$regex':?0, '$options':'i'}}") + List findByNameLike(String name); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceRepository.java new file mode 100644 index 000000000..0914fed0d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceRepository.java @@ -0,0 +1,39 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; + +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +/** + * Repository interface for Service domain objects. + * + * @author laurent + */ +public interface ServiceRepository extends MongoRepository, CustomServiceRepository { + + Service findByNameAndVersion(String name, String version); + + List findByType(ServiceType type); + + @Query("{'name' : {'$regex':?0, '$options':'i'}}") + List findByNameLike(String name); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceRepositoryImpl.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceRepositoryImpl.java new file mode 100644 index 000000000..c0e9ace94 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceRepositoryImpl.java @@ -0,0 +1,92 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Service; + +import org.bson.types.ObjectId; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.ObjectOperators; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.springframework.data.domain.Sort.Direction.DESC; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; + +/** + * Implementation for CustomServiceRepository. + * @author laurent + */ +public class ServiceRepositoryImpl implements CustomServiceRepository { + + @Autowired + private MongoTemplate template; + + @Override + public List findByIdIn(List ids) { + // Convert ids into BSON ObjectId. + List objIds = new ArrayList(); + for (String id : ids) { + objIds.add(new ObjectId(id)); + } + + List results = template.find(new Query(Criteria.where("_id").in(objIds)), Service.class); + + return results; + } + + @Override + public List findByLabels(Map labels) { + Query query = new Query(); + for (String labelKey : labels.keySet()) { + query.addCriteria(Criteria.where("metadata.labels." + labelKey).is(labels.get(labelKey))); + } + List results = template.find(query, Service.class); + return results; + } + + @Override + public List findByLabelsAndNameLike(Map labels, String name) { + Query query = new Query(Criteria.where("name").regex(name, "i")); + for (String labelKey : labels.keySet()) { + query.addCriteria(Criteria.where("metadata.labels." + labelKey).is(labels.get(labelKey))); + } + List results = template.find(query, Service.class); + return results; + } + + public List countServicesByType() { + Aggregation aggregation = newAggregation(project("type"), group("type").count().as("number"), + project("number").and("type").previousOperation(), sort(DESC, "number")); + AggregationResults results = template.aggregate(aggregation, Service.class, ServiceCount.class); + return results.getMappedResults(); + } + + public List listLabels() { + ObjectOperators.ObjectToArray labels = ObjectOperators.ObjectToArray.valueOfToArray("metadata.labels"); + Aggregation aggregation = newAggregation(project().and(labels).as("labels"), unwind("labels"), + sort(DESC, "labels.v"), group("labels.k").addToSet("labels.v").as("values").first("labels.k").as("key")); + AggregationResults results = template.aggregate(aggregation, Service.class, LabelValues.class); + return results.getMappedResults(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceStateRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceStateRepository.java new file mode 100644 index 000000000..ff3529d22 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/ServiceStateRepository.java @@ -0,0 +1,30 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.repository; + +import io.github.microcks.domain.ServiceState; + +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * Repository interface for ServiceState domain objects. + * @author laurent + */ +public interface ServiceStateRepository extends MongoRepository { + + ServiceState findByServiceIdAndKey(String serviceId, String key); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestConformanceMetricRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestConformanceMetricRepository.java new file mode 100644 index 000000000..68f288402 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestConformanceMetricRepository.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.TestConformanceMetric; + +import org.springframework.data.mongodb.repository.MongoRepository; + + +/** + * Repository interface for TestConformanceMetric domain objects. + * @author laurent + */ +public interface TestConformanceMetricRepository + extends MongoRepository, CustomTestConformanceMetricRepository { + + TestConformanceMetric findByServiceId(String serviceId); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestConformanceMetricRepositoryImpl.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestConformanceMetricRepositoryImpl.java new file mode 100644 index 000000000..4ab3895ba --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestConformanceMetricRepositoryImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.TestConformanceMetric; +import io.github.microcks.domain.WeightedMetricValue; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; + +import java.util.List; + +import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; + +/** + * Implementation for CustomTestConformanceMetricRepository. + * @author laurent + */ +public class TestConformanceMetricRepositoryImpl implements CustomTestConformanceMetricRepository { + + @Autowired + private MongoTemplate template; + + @Override + public List aggregateTestConformanceMetric() { + // Match all but group by label (domain) and compute average and weight. + Aggregation aggregation = newAggregation( + group("aggregationLabelValue").avg("currentScore").as("value").count().as("weight"), + project("value", "weight").and("_id").as("name"), sort(Sort.Direction.DESC, "value")); + AggregationResults results = template.aggregate(aggregation, TestConformanceMetric.class, + WeightedMetricValue.class); + return results.getMappedResults(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestResultRepository.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestResultRepository.java new file mode 100644 index 000000000..963e211da --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/repository/TestResultRepository.java @@ -0,0 +1,41 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.TestResult; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + +import java.util.Date; +import java.util.List; + +/** + * Repository interface for TestResult domain objects. + * @author laurent + */ +public interface TestResultRepository extends MongoRepository { + + List findByServiceId(String serviceId); + + List findByServiceId(String serviceId, Pageable page); + + @Query("{ 'testDate' : { $gt: ?0 } }") + List findAllWithTestDateAfter(Date date); + + @Query(value = "{ 'serviceId' : ?0}}", count = true) + long countByServiceId(String serviceId); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/AuthorizationChecker.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/AuthorizationChecker.java new file mode 100644 index 000000000..99385e76f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/AuthorizationChecker.java @@ -0,0 +1,105 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import io.github.microcks.domain.ImportJob; +import io.github.microcks.domain.Service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.annotation.PropertySources; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +/** + * A Spring component that holds security checking related utility methods. + * @author laurent + */ +@Component +@PropertySources({ @PropertySource("features.properties"), + @PropertySource(value = "file:/deployments/config/features.properties", ignoreResourceNotFound = true), + @PropertySource("application.properties"), }) +public class AuthorizationChecker { + + /** The Microcks user role name. */ + public static final String ROLE_USER = "user"; + /** The Microcks manager role name. */ + public static final String ROLE_MANAGER = "manager"; + /** The Microcks admin role name. */ + public static final String ROLE_ADMIN = "admin"; + + /** The prefix used for Microcks groups name. */ + private static final String MICROCKS_GROUPS_PREFIX = "/microcks/"; + + @Value("${keycloak.enabled}") + private final Boolean authenticationEnabled = true; + + @Value("${features.feature.repository-tenancy.enabled}") + private final Boolean authorizationEnabled = false; + + @Value("${features.feature.repository-filter.label-key}") + private final String filterLabelKey = null; + + /** + * Check if provided user is having a specific role at the global level. + * @param userInfo The information representing user to check access for. + * @param role The role the user should endorse. + * @return True if authorized, false otherwise. + */ + public boolean hasRole(UserInfo userInfo, String role) { + if (authenticationEnabled && userInfo.getRoles() != null) { + return Arrays.stream(userInfo.getRoles()).anyMatch(role::equals); + } + return true; + } + + /** + * Check if provided user is having a specific role for given service. + * @param userInfo The information representing user to check access for. + * @param role The role the user should endorse. + * @param service The service the user should be authorized with the role. + * @return True if authorized, false otherwise. + */ + public boolean hasRoleForService(UserInfo userInfo, String role, Service service) { + if (authorizationEnabled && userInfo.getRoles() != null && service.getMetadata().getLabels() != null) { + // Build the full rolePath that is checked for group membership. + String rolePath = MICROCKS_GROUPS_PREFIX + role + "/" + service.getMetadata().getLabels().get(filterLabelKey); + boolean serviceRole = Arrays.stream(userInfo.getGroups()).anyMatch(rolePath::equals); + return serviceRole || hasRole(userInfo, role); + } + // Default to global role endorsing. + return hasRole(userInfo, role); + } + + /** + * Check if provided user is having a specific role for given import job. + * @param userInfo The information representing user to check access for. + * @param role The role the user should endorse. + * @param job The import job the user should be authorized with the role. + * @return True if authorized, false otherwise. + */ + public boolean hasRoleForImportJob(UserInfo userInfo, String role, ImportJob job) { + if (authorizationEnabled && job.getMetadata().getLabels() != null) { + // Build the full rolePath that is checked for group membership. + String rolePath = MICROCKS_GROUPS_PREFIX + role + "/" + job.getMetadata().getLabels().get(filterLabelKey); + boolean jobRole = Arrays.stream(userInfo.getGroups()).anyMatch(rolePath::equals); + return jobRole || hasRole(userInfo, role); + } + // Default to global role endorsing. + return hasRole(userInfo, role); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/AuthorizationException.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/AuthorizationException.java new file mode 100644 index 000000000..5754149ce --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/AuthorizationException.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +/** + * Checked exception for authorization issues. + * @author laurent + */ +public class AuthorizationException extends Exception { + + public AuthorizationException(String message) { + super(message); + } + + public AuthorizationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/EnableUserInfoInContext.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/EnableUserInfoInContext.java new file mode 100644 index 000000000..01d8cb0b8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/EnableUserInfoInContext.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Method level annotation marking methods that need injection of UserInfo bean into current execution context. + * Depending on the component declaring the annotated method, context can be: HTTP request context, service execution + * context, transaction context, etc... + * @author laurent + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnableUserInfoInContext { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/KeycloakJwtToken.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/KeycloakJwtToken.java new file mode 100644 index 000000000..c9caf6809 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/KeycloakJwtToken.java @@ -0,0 +1,33 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +/** + * Some specific claims we're expecting in a Keycloak provided token. + * @author laurent + */ +public class KeycloakJwtToken { + + /** The name of the token claim that holds resource access. */ + public static final String RESOURCE_ACCESS_TOKEN_CLAIM = "resource_access"; + + /** The name of the token resource that holds roles information. */ + public static final String MICROCKS_APP_RESOURCE = "microcks-app"; + + /** The name of token claim that contains the groups information. */ + public static final String MICROCKS_GROUPS_TOKEN_CLAIM = "microcks-groups"; + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/KeycloakTokenToUserInfoMapper.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/KeycloakTokenToUserInfoMapper.java new file mode 100644 index 000000000..de049d07a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/KeycloakTokenToUserInfoMapper.java @@ -0,0 +1,69 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import static io.github.microcks.security.KeycloakJwtToken.MICROCKS_GROUPS_TOKEN_CLAIM; +import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.*; + +/** + * Simpler mapper for transforming KeycloakSecurityContext token into UserInfo bean. + * @author laurent + */ +public class KeycloakTokenToUserInfoMapper { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(KeycloakTokenToUserInfoMapper.class); + + private KeycloakTokenToUserInfoMapper() { + // Hide implicit public constructor. + } + + /** + * Maps the information from Spring SecurityContext tokens into a UserInfo instance. + * @param context The current security context provided by Spring Security server adapter. + * @return A new UserInfo with info coming from Keycloak tokens. + */ + public static UserInfo map(SecurityContext context) { + Authentication authentication = context.getAuthentication(); + + if (authentication instanceof JwtAuthenticationToken jwtToken) { + Jwt jwt = jwtToken.getToken(); + + String[] microcksGroups = new String[] {}; + if (jwt.hasClaim(MICROCKS_GROUPS_TOKEN_CLAIM)) { + microcksGroups = jwt.getClaimAsStringList(MICROCKS_GROUPS_TOKEN_CLAIM).toArray(String[]::new); + } + + // Create and return UserInfo. + UserInfo userInfo = new UserInfo(jwt.getClaimAsString(NAME), jwt.getClaimAsString(PREFERRED_USERNAME), + jwt.getClaimAsString(GIVEN_NAME), jwt.getClaimAsString(FAMILY_NAME), jwt.getClaimAsString(EMAIL), + authentication.getAuthorities().stream() + .map(grantedAuthority -> grantedAuthority.getAuthority().replace("ROLE_", "")) + .toArray(String[]::new), + microcksGroups); + log.debug("Current user is: {}", userInfo); + return userInfo; + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/MicrocksJwtConverter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/MicrocksJwtConverter.java new file mode 100644 index 000000000..c3e2eab64 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/MicrocksJwtConverter.java @@ -0,0 +1,57 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * A JWT converter responsible for retrieving microcks-app specific granted authorities. + * @author laurent + */ +public class MicrocksJwtConverter implements Converter { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(MicrocksJwtConverter.class); + + @Override + public JwtAuthenticationToken convert(Jwt jwt) { + Map>> resourceAccess = jwt + .getClaim(KeycloakJwtToken.RESOURCE_ACCESS_TOKEN_CLAIM); + + if (resourceAccess != null) { + Map> microcksResource = resourceAccess.get(KeycloakJwtToken.MICROCKS_APP_RESOURCE); + + if (microcksResource != null) { + Collection roles = microcksResource.get("roles"); + log.trace("JWT extracted roles for microcks-app: {}", roles); + + var grantedAuthorities = roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList(); + return new JwtAuthenticationToken(jwt, grantedAuthorities); + } + } + return new JwtAuthenticationToken(jwt, Collections.emptyList()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/OAuth2AuthorizedClientProvider.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/OAuth2AuthorizedClientProvider.java new file mode 100644 index 000000000..a4c067e55 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/OAuth2AuthorizedClientProvider.java @@ -0,0 +1,133 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import io.github.microcks.domain.OAuth2AuthorizedClient; +import io.github.microcks.domain.OAuth2ClientContext; +import io.github.microcks.domain.OAuth2GrantType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; + +import java.util.Arrays; + +/** + * Process OAuth2 authorization flow and tries to authorize from a client context. + * @author laurent + */ +public class OAuth2AuthorizedClientProvider { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(OAuth2AuthorizedClientProvider.class); + + /** + * Realize OAuth2 authorization trying to get an access token from the given OAuth2 client context. Token as well as + * traceability information are then transferred as a result in the OAuth2AuthorizedClient + * @param oAuth2ClientContext The client context for OAuth2 authorization flow + * @return An authorized client with traceability elements being principal and access token. + * @throws AuthorizationException + */ + public OAuth2AuthorizedClient authorize(OAuth2ClientContext oAuth2ClientContext) throws AuthorizationException { + + OAuth2AccessToken accessToken = null; + try { + switch (oAuth2ClientContext.getGrantType()) { + case PASSWORD -> accessToken = getResourceOwnerPasswordAccessToken(oAuth2ClientContext); + case CLIENT_CREDENTIALS -> accessToken = getClientCredentialsAccessToken(oAuth2ClientContext); + case REFRESH_TOKEN -> accessToken = getRefreshTokenAccessToken(oAuth2ClientContext); + } + } catch (OAuth2AuthorizationException oAuth2AuthorizationException) { + log.error("Error during {} grant type fetching", oAuth2ClientContext.getGrantType(), + oAuth2AuthorizationException); + throw new AuthorizationException("Error during " + oAuth2ClientContext.getGrantType() + " grant type fetching", + oAuth2AuthorizationException); + } + + String principalName = oAuth2ClientContext.getClientId(); + if (oAuth2ClientContext.getGrantType() == OAuth2GrantType.PASSWORD) { + principalName = oAuth2ClientContext.getUsername(); + } + + return new OAuth2AuthorizedClient(oAuth2ClientContext.getGrantType(), principalName, + oAuth2ClientContext.getTokenUri(), String.join(" ", accessToken.getScopes()), accessToken.getTokenValue()); + } + + private OAuth2AccessToken getResourceOwnerPasswordAccessToken(OAuth2ClientContext oAuth2ClientContext) { + // Build a ClientRegistration with PASSWORD grant type. + ClientRegistration registration = initializeClientRegistration(oAuth2ClientContext) + .authorizationGrantType(AuthorizationGrantType.PASSWORD).build(); + + DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient(); + OAuth2PasswordGrantRequest request = new OAuth2PasswordGrantRequest(registration, + oAuth2ClientContext.getUsername(), oAuth2ClientContext.getPassword()); + OAuth2AccessTokenResponse response = client.getTokenResponse(request); + + return response.getAccessToken(); + } + + private OAuth2AccessToken getClientCredentialsAccessToken(OAuth2ClientContext oAuth2ClientContext) { + // Build a ClientRegistration with CLIENT_CREDENTIALS grant type. + ClientRegistration registration = initializeClientRegistration(oAuth2ClientContext) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); + + DefaultClientCredentialsTokenResponseClient client = new DefaultClientCredentialsTokenResponseClient(); + OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration); + OAuth2AccessTokenResponse response = client.getTokenResponse(request); + + return response.getAccessToken(); + } + + private OAuth2AccessToken getRefreshTokenAccessToken(OAuth2ClientContext oAuth2ClientContext) { + // Build a ClientRegistration with REFRESH_TOKEN grant type. + ClientRegistration registration = initializeClientRegistration(oAuth2ClientContext) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).build(); + + DefaultRefreshTokenTokenResponseClient clientRT = new DefaultRefreshTokenTokenResponseClient(); + OAuth2RefreshTokenGrantRequest requestRT = new OAuth2RefreshTokenGrantRequest(registration, + new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "fake-one", null, null), + new OAuth2RefreshToken(oAuth2ClientContext.getRefreshToken(), null)); + OAuth2AccessTokenResponse response = clientRT.getTokenResponse(requestRT); + + return response.getAccessToken(); + } + + private ClientRegistration.Builder initializeClientRegistration(OAuth2ClientContext oAuth2ClientContext) { + ClientRegistration.Builder builder = ClientRegistration.withRegistrationId("microcks-test-idp") + .clientId(oAuth2ClientContext.getClientId()).clientSecret(oAuth2ClientContext.getClientSecret()) + .tokenUri(oAuth2ClientContext.getTokenUri()); + + if (oAuth2ClientContext.getScopes() != null) { + String[] scopes = (oAuth2ClientContext.getScopes() + " openid").split(" "); + builder.scope(Arrays.asList(scopes)); + } else { + builder.scope("openid"); + } + + return builder; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfo.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfo.java new file mode 100644 index 000000000..5dba383bc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfo.java @@ -0,0 +1,90 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import java.util.Arrays; + +/** + * This is a wrapper for holding user informations. + * @author laurent + */ +public class UserInfo { + + private final String name; + private final String username; + private final String givenName; + private final String familyName; + private final String email; + private final String[] roles; + private final String[] groups; + + /** + * Create a new UserInfo with all required properties. + * @param name The full displayable name for this user (eg. Laurent Broudoux) + * @param username The system username for this user (eg. lbroudoux) + * @param givenName The user given name (eg. Laurent) + * @param familyName The user family name (eg. Broudoux) + * @param email The user email address (eg. laurent@microcks.io) + * @param roles The array of endorsed roles for this user (eg. [user, manager, admin]) + * @param groups The array of groups this user is member of (eg. [/microcks/manager/authentication, + * /microcks/manager/greetings]) + */ + public UserInfo(String name, String username, String givenName, String familyName, String email, String[] roles, + String[] groups) { + this.name = name; + this.username = username; + this.givenName = givenName; + this.familyName = familyName; + this.email = email; + this.roles = roles; + this.groups = groups; + } + + public String getName() { + return name; + } + + public String getUsername() { + return username; + } + + public String getGivenName() { + return givenName; + } + + public String getFamilyName() { + return familyName; + } + + public String getEmail() { + return email; + } + + public String[] getRoles() { + return roles; + } + + public String[] getGroups() { + return groups; + } + + @Override + public String toString() { + return "UserInfo{" + "name='" + name + '\'' + ", username='" + username + '\'' + ", givenName='" + givenName + + '\'' + ", familyName='" + familyName + '\'' + ", email='" + email + '\'' + ", roles=" + + Arrays.toString(roles) + ", groups=" + Arrays.toString(groups) + '}'; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfoHandlerMethodArgumentResolver.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfoHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..5a517d0a1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfoHandlerMethodArgumentResolver.java @@ -0,0 +1,67 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * A Spring Web HandlerMethodArgumentResolver that knows how to retrieve a UserInfo controller method argument. + * @author laurent + */ +public class UserInfoHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(UserInfoHandlerMethodArgumentResolver.class); + + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter.getParameterType().equals(UserInfo.class); + } + + @Override + public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, + NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { + + // Check if already present. + Object userInfoObj = nativeWebRequest.getAttribute(UserInfo.class.getName(), RequestAttributes.SCOPE_REQUEST); + + if (userInfoObj != null) { + log.debug("UserInfo is already present into request attribute"); + return UserInfo.class.cast(userInfoObj); + } + + log.debug("Creating a new UserInfo to resolve {} argument", methodParameter.getMethod()); + UserInfo userInfo = null; + + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext.getAuthentication() != null) { + log.debug("Found a Spring Security Authentication to map to UserInfo"); + userInfo = KeycloakTokenToUserInfoMapper.map(securityContext); + nativeWebRequest.setAttribute(UserInfo.class.getName(), userInfo, RequestAttributes.SCOPE_REQUEST); + } + + return userInfo; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfoInContextInterceptor.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfoInContextInterceptor.java new file mode 100644 index 000000000..92b211678 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/security/UserInfoInContextInterceptor.java @@ -0,0 +1,63 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * A Spring Web HandlerInterceptor that checks if @EnabledUserInfoInContext annotation is present on controller method + * in order to inject UserInfo as a HTTP request attribute. + * @author laurent + */ +public class UserInfoInContextInterceptor implements HandlerInterceptor { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(UserInfoInContextInterceptor.class); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + log.trace("Intercepting and pre-handling request to check @EnableUserInfoInContext"); + if (handler instanceof HandlerMethod handlerMethod) { + EnableUserInfoInContext needUserInfo = handlerMethod.getMethodAnnotation(EnableUserInfoInContext.class); + if (needUserInfo == null) { + needUserInfo = handlerMethod.getMethod().getDeclaringClass().getAnnotation(EnableUserInfoInContext.class); + } + + // We're sure we do not need to inject UserInfo in context, so xew can proceed. + if (needUserInfo == null) { + return true; + } + + log.debug("@EnableUserInfoInContext is present on {}", handler); + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext.getAuthentication() != null) { + log.debug("Found a Spring Security Authentication to map to UserInfo"); + // Create and store UserInfo in request attribute. + UserInfo userInfo = KeycloakTokenToUserInfoMapper.map(securityContext); + request.setAttribute(UserInfo.class.getName(), userInfo); + } + } + return HandlerInterceptor.super.preHandle(request, response, handler); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/AICopilotRunnerService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/AICopilotRunnerService.java new file mode 100644 index 000000000..f7272073d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/AICopilotRunnerService.java @@ -0,0 +1,79 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.service; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Service; +import io.github.microcks.security.UserInfo; +import io.github.microcks.util.ai.AICopilot; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * A service that runs the AI Copilot on asynchronous/long-running tasks. + * @author laurent + */ +@org.springframework.stereotype.Service +public class AICopilotRunnerService { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(AICopilotRunnerService.class); + + private AICopilot copilot; + private final ServiceService serviceService; + + + /** + * Build a new AICopilotRunnerService with the required dependencies. + * @param serviceService The service service. + * @param copilot The optional AI Copilot + */ + public AICopilotRunnerService(ServiceService serviceService, Optional copilot) { + this.serviceService = serviceService; + copilot.ifPresent(aiCopilot -> this.copilot = aiCopilot); + } + + /** + * Generate samples for all operations of a service using asynchronous/completable future pattern. + * @param service The service to generate samples for. + * @param contract The contract to generate samples for. + * @param userInfo The user information for user having launched the generation. + * @return A Future wrapping generation success flag. + */ + @Async + public CompletableFuture generateSamplesForService(Service service, Resource contract, UserInfo userInfo) { + for (Operation operation : service.getOperations()) { + try { + List exchanges = copilot.suggestSampleExchanges(service, operation, contract, 3); + serviceService.addAICopilotExchangesToServiceOperation(service.getId(), operation.getName(), + (List) exchanges, userInfo); + } catch (Exception e) { + log.error("Caught an exception while generating samples", e); + return CompletableFuture.completedFuture(false); + } + } + return CompletableFuture.completedFuture(true); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ArtifactInfo.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ArtifactInfo.java new file mode 100644 index 000000000..d6c60e806 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ArtifactInfo.java @@ -0,0 +1,52 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +/** + * Simple wrapper for essential information about a API | Service artifact. + * @author laurent + */ +public class ArtifactInfo { + + private String artifactName; + private boolean mainArtifact; + + /** + * Create a new ArtifactInfo with mandatory attributes. + * @param artifactName The human readable name of artifact + * @param mainArtifact Is it a main artifact for Service definition or a secondary for examples only? + */ + public ArtifactInfo(String artifactName, boolean mainArtifact) { + this.artifactName = artifactName; + this.mainArtifact = mainArtifact; + } + + public String getArtifactName() { + return artifactName; + } + + public void setArtifactName(String artifactName) { + this.artifactName = artifactName; + } + + public boolean isMainArtifact() { + return mainArtifact; + } + + public void setMainArtifact(boolean mainArtifact) { + this.mainArtifact = mainArtifact; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ExchangeSelection.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ExchangeSelection.java new file mode 100644 index 000000000..25b4bb195 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ExchangeSelection.java @@ -0,0 +1,46 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.service; + +import java.util.List; +import java.util.Map; + +/** + * Simple wrapper for essential information about an Exchange selection. + * @author laurent + */ +public class ExchangeSelection { + + private String serviceId; + private Map> exchanges; + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceeId) { + this.serviceId = serviceeId; + } + + public Map> getExchanges() { + return exchanges; + } + + public void setExchanges(Map> exchanges) { + this.exchanges = exchanges; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ImportExportService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ImportExportService.java new file mode 100644 index 000000000..5a8dff0db --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ImportExportService.java @@ -0,0 +1,291 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.event.ChangeType; +import io.github.microcks.event.ServiceChangeEvent; +import io.github.microcks.repository.EventMessageRepository; +import io.github.microcks.repository.RequestRepository; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.security.AuthorizationChecker; +import io.github.microcks.security.UserInfo; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.MockRepositoryExportException; +import io.github.microcks.util.MockRepositoryExporter; +import io.github.microcks.util.MockRepositoryExporterFactory; +import io.github.microcks.util.metadata.ExamplesExporter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; + +import java.util.ArrayList; +import java.util.List; + +/** + * A Service for managing imports and exports of Microcks repository part. + * @author laurent + */ +@org.springframework.stereotype.Service +public class ImportExportService { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ImportExportService.class); + + private final ServiceService serviceService; + private final MessageService messageService; + private final RequestRepository requestRepository; + private final ResourceRepository resourceRepository; + private final ResponseRepository responseRepository; + private final EventMessageRepository eventMessageRepository; + private final ServiceRepository serviceRepository; + private final ApplicationContext applicationContext; + private final AuthorizationChecker authorizationChecker; + + /** + * Create a new ImportExportService with required dependencies. + * @param serviceService The service for services + * @param messageService The service for messages + * @param requestRepository The repository for requests + * @param resourceRepository The repository for resources + * @param responseRepository The repository for responses + * @param eventMessageRepository The repository for event messages + * @param serviceRepository The repository for services + * @param applicationContext The Spring application context + * @param authorizationChecker The authorization checker service + */ + public ImportExportService(ServiceService serviceService, MessageService messageService, + RequestRepository requestRepository, ResourceRepository resourceRepository, + ResponseRepository responseRepository, EventMessageRepository eventMessageRepository, + ServiceRepository serviceRepository, ApplicationContext applicationContext, + AuthorizationChecker authorizationChecker) { + this.serviceService = serviceService; + this.messageService = messageService; + this.requestRepository = requestRepository; + this.resourceRepository = resourceRepository; + this.responseRepository = responseRepository; + this.eventMessageRepository = eventMessageRepository; + this.serviceRepository = serviceRepository; + this.applicationContext = applicationContext; + this.authorizationChecker = authorizationChecker; + } + + /** + * Import a repository from JSON definitions. + * @param json A String encoded into json and representing repository object definitions. + * @return A boolean indicating operation success. + */ + public boolean importRepository(String json) { + ObjectMapper mapper = new ObjectMapper(); + ImportExportModel model = null; + try { + model = mapper.readValue(json, ImportExportModel.class); + } catch (Exception e) { + log.error("Exception while reading json import", e); + } + + if (model != null) { + log.info("Retrieve {} services to import into repository", model.getServices().size()); + log.info("Retrieve {} resources to import into repository", model.getResources().size()); + log.info("Retrieve {} responses to import into repository", model.getResponses().size()); + log.info("Retrieve {} requests to import into repository", model.getRequests().size()); + log.info("Retrieve {} event messages to import into repository", + model.getEventMessages() != null ? model.getEventMessages().size() : 0); + + serviceRepository.saveAll(model.getServices()); + resourceRepository.saveAll(model.getResources()); + responseRepository.saveAll(model.getResponses()); + requestRepository.saveAll(model.getRequests()); + // Make it optional to allow importing an old snapshot. + if (model.getEventMessages() != null) { + eventMessageRepository.saveAll(model.getEventMessages()); + } + + // Once everything is saved, be sure to fire a change event to allow + // propagation. + publishServiceChangeEvent(model); + return true; + } + log.info("No services, resources or messages to import into repository"); + return false; + } + + /** Publish a ServiceChangeEvent towards minions or some other consumers. */ + private void publishServiceChangeEvent(ImportExportModel model) { + for (Service service : model.getServices()) { + ServiceChangeEvent event = new ServiceChangeEvent(this, service.getId(), ChangeType.UPDATED); + applicationContext.publishEvent(event); + log.debug("Service change event has been published"); + } + } + + /** + * Get a partial export of repository using the specified services identifiers. + * @param ids The list of service ids to export + * @param format The format for this export (reserved for future usage) + * @return A string representation of this repository export + */ + public String exportRepository(List ids, String format) { + StringBuilder result = new StringBuilder("{"); + ObjectMapper mapper = new ObjectMapper(); + + // First, retrieve service list. + List services = serviceRepository.findByIdIn(ids); + try { + String jsonArray = mapper.writeValueAsString(services); + result.append("\"services\":").append(jsonArray).append(", "); + } catch (Exception e) { + log.error("Exception while serializing services for export", e); + } + + // Then, get resources associated to services. + List resources = resourceRepository.findByServiceIdIn(ids); + try { + String jsonArray = mapper.writeValueAsString(resources); + result.append("\"resources\":").append(jsonArray).append(", "); + } catch (Exception e) { + log.error("Exception while serializing resources for export", e); + } + + // Finally, get requests and responses associated to the services. + List operationIds = new ArrayList<>(); + for (Service service : services) { + for (Operation operation : service.getOperations()) { + operationIds.add(IdBuilder.buildOperationId(service, operation)); + } + } + + List requests = requestRepository.findByOperationIdIn(operationIds); + List responses = responseRepository.findByOperationIdIn(operationIds); + List eventMessages = eventMessageRepository.findByOperationIdIn(operationIds); + try { + String jsonArray = mapper.writeValueAsString(requests); + result.append("\"requests\":").append(jsonArray).append(", "); + jsonArray = mapper.writeValueAsString(responses); + result.append("\"responses\":").append(jsonArray).append(", "); + jsonArray = mapper.writeValueAsString(eventMessages); + result.append("\"eventMessages\":").append(jsonArray); + } catch (Exception e) { + log.error("Exception while serializing messages for export", e); + } + + return result.append("}").toString(); + } + + public static class ImportExportModel { + private List services; + private List resources; + private List requests; + private List responses; + private List eventMessages; + + public List getServices() { + return services; + } + + public void setServices(List services) { + this.services = services; + } + + public List getResources() { + return resources; + } + + public void setResources(List resources) { + this.resources = resources; + } + + public List getRequests() { + return requests; + } + + public void setRequests(List requests) { + this.requests = requests; + } + + public List getResponses() { + return responses; + } + + public void setResponses(List responses) { + this.responses = responses; + } + + public List getEventMessages() { + return eventMessages; + } + + public void setEventMessages(List eventMessages) { + this.eventMessages = eventMessages; + } + } + + /** + * Export a selection of exchanges for a service. + * @param serviceId The unique identifier of the service + * @param exchangeSelection The selection of exchanges to export + * @param exportFormat The format for this export + * @param userInfo The user information + * @return A string representation of this exchange selection export + */ + public String exportExchangeSelection(String serviceId, ExchangeSelection exchangeSelection, String exportFormat, + UserInfo userInfo) throws MockRepositoryExportException { + Service service = serviceService.getServiceById(serviceId); + if (service != null + && authorizationChecker.hasRoleForService(userInfo, AuthorizationChecker.ROLE_MANAGER, service)) { + + // Create an exporter and add the service definition. + MockRepositoryExporter exporter = MockRepositoryExporterFactory.getMockRepositoryExporter(exportFormat); + exporter.addServiceDefinition(service); + + for (Operation operation : service.getOperations()) { + if (exchangeSelection.getExchanges().containsKey(operation.getName())) { + log.debug("Exporting selected exchanges for operation '{}'", operation.getName()); + String operationId = IdBuilder.buildOperationId(service, operation); + + List selectedExchanges = null; + if (ServiceType.EVENT.equals(service.getType())) { + selectedExchanges = messageService.getEventByOperation(operationId).stream() + .filter(unidirectionalEvent -> exchangeSelection.getExchanges().get(operation.getName()) + .contains(unidirectionalEvent.getEventMessage().getName())) + .toList(); + } else { + selectedExchanges = messageService.getRequestResponseByOperation(operationId).stream() + .filter(pair -> exchangeSelection.getExchanges().get(operation.getName()) + .contains(pair.getRequest().getName())) + .toList(); + } + exporter.addMessageDefinitions(service, operation, selectedExchanges); + } + } + return exporter.exportAsString(); + } + log.warn("Didn't find any service with id {} to export or unauthorized, returning empty content", serviceId); + return ""; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/JobService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/JobService.java new file mode 100644 index 000000000..071feab7f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/JobService.java @@ -0,0 +1,95 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import io.github.microcks.domain.ImportJob; +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceRef; +import io.github.microcks.repository.ImportJobRepository; +import io.github.microcks.repository.SecretRepository; +import io.github.microcks.util.MockRepositoryImportException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.List; + +/** + * Bean defining service operations around ImportJob domain objects. + * @author laurent + */ +@org.springframework.stereotype.Service +public class JobService { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(JobService.class); + + private final ImportJobRepository jobRepository; + private final SecretRepository secretRepository; + private final ServiceService serviceService; + + /** + * Create a new JobService with required dependencies. + * @param jobRepository The job repository to use. + * @param secretRepository The secret repository to use. + * @param serviceService The service service to use. + */ + public JobService(ImportJobRepository jobRepository, SecretRepository secretRepository, + ServiceService serviceService) { + this.jobRepository = jobRepository; + this.secretRepository = secretRepository; + this.serviceService = serviceService; + } + + /** + * Realize the import of a repository defined into an import job. + * @param job The job containing information onto the repository. + */ + public void doImportJob(ImportJob job) { + log.info("Starting import for job '{}'", job.getName()); + + // Retrieve associated secret if any. + Secret jobSecret = null; + if (job.getSecretRef() != null) { + log.debug("Retrieving secret {} for job {}", job.getSecretRef().getName(), job.getName()); + jobSecret = secretRepository.findById(job.getSecretRef().getSecretId()).orElse(null); + } + + // Reinitialize service references and import errors before new import. + job.setServiceRefs(null); + job.setLastImportError(null); + List services = null; + try { + services = serviceService.importServiceDefinition(job.getRepositoryUrl(), jobSecret, + job.isRepositoryDisableSSLValidation(), job.isMainArtifact()); + } catch (MockRepositoryImportException mrie) { + log.warn("MockRepositoryImportException while importing job '{}' : {}", job.getName(), mrie.getMessage()); + job.setLastImportError(mrie.getMessage()); + } + + // Add service references if any. + if (services != null) { + for (Service service : services) { + job.addServiceRef(new ServiceRef(service.getId(), service.getName(), service.getVersion())); + } + } + job.setLastImportDate(new Date()); + jobRepository.save(job); + log.info("Import of job '{}' done", job.getName()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/MessageService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/MessageService.java new file mode 100644 index 000000000..4869107c4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/MessageService.java @@ -0,0 +1,157 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import io.github.microcks.domain.*; +import io.github.microcks.repository.EventMessageRepository; +import io.github.microcks.repository.RequestRepository; +import io.github.microcks.repository.ResponseRepository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Service bean for common processing around messages (request and responses). + * @author laurent + */ +@org.springframework.stereotype.Service +public class MessageService { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(MessageService.class); + + private final RequestRepository requestRepository; + private final ResponseRepository responseRepository; + private final EventMessageRepository eventMessageRepository; + + /** + * Create a new MessageService with required dependencies. + * @param requestRepository The repository for requests + * @param responseRepository The repository for responses + * @param eventMessageRepository The repository for event messages + */ + public MessageService(RequestRepository requestRepository, ResponseRepository responseRepository, + EventMessageRepository eventMessageRepository) { + this.requestRepository = requestRepository; + this.responseRepository = responseRepository; + this.eventMessageRepository = eventMessageRepository; + } + + + /** + * Retrieve unidirectional events corresponding to an Operation. + * @param operationId The identifier of operation to get messages for. + * @return A list of unidirectional event messages + */ + public List getEventByOperation(String operationId) { + // Retrieve event messages using operation identifier. + List eventMessages = eventMessageRepository.findByOperationId(operationId); + if (log.isDebugEnabled()) { + log.debug("Found {} event(s) for operation {}", eventMessages.size(), operationId); + } + + // Just wrap then into an UnidirectionalEvent exchange. + List results = new ArrayList<>(eventMessages.size()); + for (EventMessage eventMessage : eventMessages) { + results.add(new UnidirectionalEvent(eventMessage)); + } + return results; + } + + /** + * Retrieve pairs of requests and responses corresponding to an Operation. + * @param operationId The identifier of operation to get messages for. + * @return A list of paired requests and responses + */ + public List getRequestResponseByOperation(String operationId) { + // Retrieve requests and responses using operation identifier. + List requests = requestRepository.findByOperationId(operationId); + List responses = responseRepository.findByOperationId(operationId); + if (log.isDebugEnabled()) { + log.debug("Found {} request(s) for operation {}", requests.size(), operationId); + log.debug("Found {} response(s) for operation {}", responses.size(), operationId); + } + + // Browse them to reassociate them. + List results = associatePairs(requests, responses); + if (log.isDebugEnabled()) { + log.debug("Emitting {} request/response pair(s) as result", results.size()); + } + return results; + } + + /** + * Retrieve unidirectional events corresponding to a TestCase. + * @param testCaseId The identifier of test case to get messages for. + * @return A list of unidirectional event messages + */ + public List getEventByTestCase(String testCaseId) { + // Retrieve events using testCase identifier. + List eventMessages = eventMessageRepository.findByTestCaseId(testCaseId); + if (log.isDebugEnabled()) { + log.debug("Found {} event(s) for testCase {}", eventMessages.size(), testCaseId); + } + + // Just wrap then into an UnidirectionalEvent exchange. + List results = new ArrayList<>(eventMessages.size()); + for (EventMessage eventMessage : eventMessages) { + results.add(new UnidirectionalEvent(eventMessage)); + } + return results; + } + + /** + * Retrieved pairs of requests and responses corresponding to a TestCase. + * @param testCaseId The identifier of test case to get messages for. + * @return A list of paired requests and responses + */ + public List getRequestResponseByTestCase(String testCaseId) { + // Retrieve requests and responses using testCase identifier. + List requests = requestRepository.findByTestCaseId(testCaseId); + List responses = responseRepository.findByTestCaseId(testCaseId); + if (log.isDebugEnabled()) { + log.debug("Found {} request(s) for testCase {}", requests.size(), testCaseId); + log.debug("Found {} response(s) for testCase {}", responses.size(), testCaseId); + } + + // Browse them to reassociate them. + List results = associatePairs(requests, responses); + if (log.isDebugEnabled()) { + log.debug("Emitting {} request/response pair(s) as result", results.size()); + } + return results; + } + + /** */ + private List associatePairs(List requests, List responses) { + List results = new ArrayList<>(); + + // Browse them to reassociate them. + for (Request request : requests) { + for (Response response : responses) { + if (request.getResponseId() != null && request.getResponseId().equals(response.getId())) { + // If found a matching response, build a pair. + results.add(new RequestResponsePair(request, response)); + break; + } + } + } + return results; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/MetricsService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/MetricsService.java new file mode 100644 index 000000000..af9a1c494 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/MetricsService.java @@ -0,0 +1,214 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.TestCaseResult; +import io.github.microcks.domain.TestConformanceMetric; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestStepResult; +import io.github.microcks.domain.Trend; +import io.github.microcks.repository.TestConformanceMetricRepository; +import io.github.microcks.util.IdBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.annotation.PropertySources; + +import java.util.Calendar; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Service bean for common processing around metrics. + * + * @author laurent + */ +@org.springframework.stereotype.Service +@PropertySources({ @PropertySource("features.properties"), + @PropertySource(value = "file:/deployments/config/features.properties", ignoreResourceNotFound = true), + @PropertySource("application.properties") }) +public class MetricsService { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(MetricsService.class); + + @Autowired + private MessageService messageService; + + @Autowired + private TestConformanceMetricRepository metricRepository; + + @Value("${features.feature.repository-filter.label-key}") + private String filterLabelKey; + + @Value("#{new Integer('${test-conformance.trend-size:3}')}") + private Integer testConformanceTrendSize; + + @Value("#{new Integer('${test-conformance.trend-history-size:10}')}") + private Integer testConformanceTrendHistorySize; + + /** + * Configure a TestConformanceMetric object for a given Service. + * + * @param service The Service to configure coverage metrics for. + */ + public void configureTestConformanceMetric(Service service) { + TestConformanceMetric metric = metricRepository.findByServiceId(service.getId()); + if (metric == null) { + log.debug("Creating a new TestCoverageMetric for Service '{}'", service.getId()); + metric = new TestConformanceMetric(); + metric.setServiceId(service.getId()); + metric.setLatestTrend(Trend.STABLE); + } + + // Update max possible score and aggregation label on crate or change. + metric.setMaxPossibleScore(computeMaxPossibleConformanceScore(service)); + if (filterLabelKey != null && service.getMetadata().getLabels() != null) { + metric.setAggregationLabelValue(service.getMetadata().getLabels().get(filterLabelKey)); + } + + metricRepository.save(metric); + } + + /** + * Remove TestConformanceMetric associated to a given Service. + * + * @param serviceId The identifier of Service to remove coverage metrics for. + */ + public void removeTestConformanceMetric(String serviceId) { + TestConformanceMetric metric = metricRepository.findByServiceId(serviceId); + if (metric != null) { + metricRepository.delete(metric); + } + } + + /** + * Update the test conformance score when a test is completed. + * + * @param testResult The newly completed Test for a Service. + */ + public void updateTestConformanceMetricOnTestResult(TestResult testResult) { + TestConformanceMetric metric = metricRepository.findByServiceId(testResult.getServiceId()); + if (metric != null) { + // Compute current score. + double currentScore = 0; + if (testResult.isSuccess()) { + currentScore = metric.getMaxPossibleScore(); + } else { + int totalSteps = 0; + int totalSuccess = 0; + for (TestCaseResult caseResult : testResult.getTestCaseResults()) { + for (TestStepResult stepResult : caseResult.getTestStepResults()) { + if (stepResult.isSuccess()) { + totalSuccess++; + } + totalSteps++; + } + } + currentScore = metric.getMaxPossibleScore() * (((double) totalSuccess) / ((double) totalSteps)); + } + + // Update entity with score and last update day. + metric.setCurrentScore(currentScore); + metric.setLastUpdateDay(getTodayAsString()); + + // If we have enough history, compute a trend. + if (metric.getLatestScores().size() >= testConformanceTrendSize - 1) { + // Start sorting the history map in reverse order (last days measure first) + List> latestReversedScores = metric.getLatestScores().entrySet().stream() + .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder())).collect(Collectors.toList()); + + // Get history average and last n days average. + double globalAvg = latestReversedScores.stream().collect(Collectors.averagingDouble(Map.Entry::getValue)); + + double latestAvg = currentScore; + for (int i = 0; i < testConformanceTrendSize - 1; i++) { + latestAvg += latestReversedScores.get(i).getValue(); + } + latestAvg = latestAvg / testConformanceTrendSize; + + // Compute a trend based on diff. + double avgDiff = latestAvg - globalAvg; + if (avgDiff >= 5) { + metric.setLatestTrend(Trend.UP); + } else if (avgDiff < 5 && avgDiff >= 0.2) { + metric.setLatestTrend(Trend.LOW_UP); + } else if (avgDiff < 0.2 && avgDiff > -0.2) { + metric.setLatestTrend(Trend.STABLE); + } else if (avgDiff <= -0.2 && avgDiff > -5) { + metric.setLatestTrend(Trend.LOW_DOWN); + } else if (avgDiff <= -5) { + metric.setLatestTrend(Trend.DOWN); + } + + // Remove all entries >9 to keep 10 entries. + while (latestReversedScores.size() >= testConformanceTrendHistorySize) { + Map.Entry lastEntry = latestReversedScores.get(latestReversedScores.size() - 1); + metric.getLatestScores().remove(lastEntry.getKey()); + latestReversedScores.remove(latestReversedScores.size() - 1); + } + metric.getLatestScores().put(metric.getLastUpdateDay(), currentScore); + } + // Save updated metric. + metricRepository.save(metric); + } + } + + /** Compute the max possible coverage for a Service. */ + private double computeMaxPossibleConformanceScore(Service service) { + double maxScore = 0.00; + double operationContrib = 100 / ((double) service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + List exchanges; + if (ServiceType.EVENT.equals(service.getType()) || ServiceType.GENERIC_EVENT.equals(service.getType())) { + // If an event, we should explicitly retrieve event messages. + exchanges = messageService.getEventByOperation(IdBuilder.buildOperationId(service, operation)); + } else { + // Otherwise we have traditional request / response pairs. + exchanges = messageService.getRequestResponseByOperation(IdBuilder.buildOperationId(service, operation)); + } + if (exchanges != null) { + if (exchanges.size() >= 2) { + maxScore += operationContrib; + } else if (exchanges.size() == 1) { + maxScore += (operationContrib / 2); + } + } + } + return maxScore; + } + + /** Return today's date as yyyyMMdd pattern. */ + private String getTodayAsString() { + // Compute day string representation. + Calendar calendar = Calendar.getInstance(); + int month = calendar.get(Calendar.MONTH) + 1; + String monthStr = (month < 10 ? "0" : "") + String.valueOf(month); + int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + String dayOfMonthStr = (dayOfMonth < 10 ? "0" : "") + String.valueOf(dayOfMonth); + + return calendar.get(Calendar.YEAR) + monthStr + dayOfMonthStr; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ProxyService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ProxyService.java new file mode 100644 index 000000000..8300f2160 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ProxyService.java @@ -0,0 +1,76 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClientResponseException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * A service that acts as a simple Http proxy to external URLs or services. + */ +@org.springframework.stereotype.Service +public class ProxyService { + + private static final Logger log = LoggerFactory.getLogger(ProxyService.class); + + private static final RestTemplate restTemplate = new RestTemplate(); + + /** + * Call an external Http service url, propagating current method, headers and body. + * @param externalUrl The target backend service url. + * @param method The Http method to propagate + * @param headers The Http headers to propagate + * @param body The Http body to propagate + * @return The response entity returned by external service as is + */ + public ResponseEntity callExternal(URI externalUrl, HttpMethod method, HttpHeaders headers, String body) { + headers.put("Host", List.of(externalUrl.getHost())); + + if (log.isDebugEnabled()) { + log.debug("Proxy request url: {}", externalUrl); + log.debug("Proxy request headers: {}", headers); + log.debug("Proxy request body: {}", body); + } + + try { + ResponseEntity response = restTemplate.exchange(externalUrl, method, new HttpEntity<>(body, headers), + byte[].class); + + if (log.isDebugEnabled()) { + log.debug("Proxy returned: {}", response.getStatusCode()); + log.debug("Proxy response headers: {}", response.getHeaders()); + log.debug("Proxy response body: {}", new String(response.getBody(), StandardCharsets.UTF_8)); + } + return response; + } catch (RestClientResponseException ex) { + if (log.isDebugEnabled()) { + log.debug("Proxy raised: {}", ex.getStatusCode()); + log.debug("Proxy exception body: {}", ex.getResponseBodyAsString()); + } + return new ResponseEntity<>(ex.getResponseBodyAsByteArray(), ex.getResponseHeaders(), ex.getStatusCode()); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ServiceService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ServiceService.java new file mode 100644 index 000000000..e36ffe068 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ServiceService.java @@ -0,0 +1,773 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import io.github.microcks.domain.Binding; +import io.github.microcks.domain.BindingType; +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.GenericResource; +import io.github.microcks.domain.Metadata; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.event.ChangeType; +import io.github.microcks.event.ServiceChangeEvent; +import io.github.microcks.repository.EventMessageRepository; +import io.github.microcks.repository.GenericResourceRepository; +import io.github.microcks.repository.RequestRepository; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.repository.TestResultRepository; +import io.github.microcks.security.AuthorizationChecker; +import io.github.microcks.security.UserInfo; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.EntityAlreadyExistsException; +import io.github.microcks.util.HTTPDownloader; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; +import io.github.microcks.util.MockRepositoryImporterFactory; +import io.github.microcks.util.ReferenceResolver; +import io.github.microcks.util.RelativeReferenceURLBuilder; +import io.github.microcks.util.RelativeReferenceURLBuilderFactory; +import io.github.microcks.util.ResourceUtil; +import io.github.microcks.util.openapi.OpenAPISchemaBuilder; + +import com.fasterxml.jackson.databind.JsonNode; +import org.bson.Document; +import org.bson.json.JsonParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Bean defining service operations around Service domain objects. + * @author laurent + */ +@org.springframework.stereotype.Service +public class ServiceService { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ServiceService.class); + + private static final String AI_COPILOT_SOURCE = "AI Copilot"; + + private final ServiceRepository serviceRepository; + private final ResourceRepository resourceRepository; + private final GenericResourceRepository genericResourceRepository; + private final RequestRepository requestRepository; + private final ResponseRepository responseRepository; + private final EventMessageRepository eventMessageRepository; + private final TestResultRepository testResultRepository; + + private final ApplicationContext applicationContext; + private final AuthorizationChecker authorizationChecker; + + @Value("${async-api.default-binding}") + private final String defaultAsyncBinding = null; + + @Value("${async-api.default-frequency}") + private final Long defaultAsyncFrequency = 30L; + + + /** + * Build a ServiceService with required dependencies. + * @param serviceRepository The repository to access service definitions + * @param resourceRepository The repository to access resource definitions + * @param genericResourceRepository The repository to access generic resource definitions + * @param requestRepository The repository to access request definitions + * @param responseRepository The repository to access response definitions + * @param eventMessageRepository The repository to access event message definitions + * @param testResultRepository The repository to access test result definitions + * @param applicationContext The Spring application context + * @param authorizationChecker The authorization checker service + */ + public ServiceService(ServiceRepository serviceRepository, ResourceRepository resourceRepository, + GenericResourceRepository genericResourceRepository, RequestRepository requestRepository, + ResponseRepository responseRepository, EventMessageRepository eventMessageRepository, + TestResultRepository testResultRepository, ApplicationContext applicationContext, + AuthorizationChecker authorizationChecker) { + this.serviceRepository = serviceRepository; + this.resourceRepository = resourceRepository; + this.genericResourceRepository = genericResourceRepository; + this.requestRepository = requestRepository; + this.responseRepository = responseRepository; + this.eventMessageRepository = eventMessageRepository; + this.testResultRepository = testResultRepository; + this.applicationContext = applicationContext; + this.authorizationChecker = authorizationChecker; + } + + /** + * Retrieve the corresponding Service from its identifier. + * @param serviceId The technical or functional (service_name:service_version) identifier of service to retrieve + * @return The corresponding Service or null if not found + */ + public Service getServiceById(String serviceId) { + // serviceId may have the form of : + if (serviceId.contains(":")) { + String name = serviceId.substring(0, serviceId.indexOf(':')); + String version = serviceId.substring(serviceId.indexOf(':') + 1); + + // If service name was encoded with '+' instead of '%20', replace them. + if (name.contains("+")) { + name = name.replace('+', ' '); + } + return serviceRepository.findByNameAndVersion(name, version); + } + return serviceRepository.findById(serviceId).orElse(null); + } + + /** + * Import definitions of services and bounded resources and messages into Microcks repository. This uses a + * MockRepositoryImporter under the hood. + * @param repositoryUrl The String representing mock repository url. + * @param repositorySecret The authentication secret associated with the repository url. Can be set to null if + * none. + * @param disableSSLValidation Whether SSL certificates validation should be turned off. + * @param mainArtifact Whether this repository should be considered as main artifact for Services to import. + * @return The list of imported Services + * @throws MockRepositoryImportException if something goes wrong (URL not reachable nor readable, etc...) + */ + public List importServiceDefinition(String repositoryUrl, Secret repositorySecret, + boolean disableSSLValidation, boolean mainArtifact) throws MockRepositoryImportException { + log.info("Importing service definitions from {}", repositoryUrl); + File localFile = null; + Map> fileProperties = null; + + if (repositoryUrl.startsWith("http")) { + try { + HTTPDownloader.FileAndHeaders fileAndHeaders = HTTPDownloader + .handleHTTPDownloadToFileAndHeaders(repositoryUrl, repositorySecret, disableSSLValidation); + localFile = fileAndHeaders.getLocalFile(); + fileProperties = fileAndHeaders.getResponseHeaders(); + } catch (IOException ioe) { + throw new MockRepositoryImportException(repositoryUrl + " cannot be downloaded", ioe); + } + } else { + // Simply build localFile from repository url. + localFile = new File(repositoryUrl); + } + + RelativeReferenceURLBuilder referenceURLBuilder = RelativeReferenceURLBuilderFactory + .getRelativeReferenceURLBuilder(fileProperties); + String artifactName = referenceURLBuilder.getFileName(repositoryUrl, fileProperties); + + // Initialize a reference resolver to the folder of this repositoryUrl. + ReferenceResolver referenceResolver = new ReferenceResolver(repositoryUrl, repositorySecret, disableSSLValidation, + referenceURLBuilder); + return importServiceDefinition(localFile, referenceResolver, new ArtifactInfo(artifactName, mainArtifact)); + } + + + /** + * Import definitions of services and bounded resources and messages into Microcks repository. This uses a + * MockRepositoryImporter under hood. + * @param repositoryFile The File for mock repository. + * @param referenceResolver The Resolver to be used during import (may be null). + * @param artifactInfo The essential information on Artifact to import. + * @return The list of imported Services + * @throws MockRepositoryImportException if something goes wrong (URL not reachable nor readable, etc...) + */ + public List importServiceDefinition(File repositoryFile, ReferenceResolver referenceResolver, + ArtifactInfo artifactInfo) throws MockRepositoryImportException { + // Retrieve the correct importer based on file path. + MockRepositoryImporter importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(repositoryFile, referenceResolver); + } catch (IOException ioe) { + throw new MockRepositoryImportException(ioe.getMessage(), ioe); + } + + Service reference = null; + boolean serviceUpdate = false; + + List services = importer.getServiceDefinitions(); + for (Service service : services) { + Service existingService = serviceRepository.findByNameAndVersion(service.getName(), service.getVersion()); + log.debug("Service [{}, {}] exists ? {}", service.getName(), service.getVersion(), existingService != null); + + // If it's the main artifact: retrieve previous id and props if update, save anyway. + if (artifactInfo.isMainArtifact()) { + if (existingService != null) { + // Retrieve its previous identifier and metadata + // (backup metadata that may have been imported with extensions). + Metadata backup = service.getMetadata(); + service.setId(existingService.getId()); + service.setMetadata(existingService.getMetadata()); + // If there was metadata found through extensions, overwrite historical ones. + if (backup != null) { + existingService.getMetadata().setLabels(backup.getLabels()); + existingService.getMetadata().setAnnotations(backup.getAnnotations()); + } + + // Keep its overriden operation properties. + copyOverridenOperations(existingService, service); + serviceUpdate = true; + } + if (service.getMetadata() == null) { + service.setMetadata(new Metadata()); + } + + // For services of type EVENT, we should put default values on frequency and bindings. + if (ServiceType.EVENT.equals(service.getType())) { + manageEventServiceDefaults(service); + } + + service.getMetadata().objectUpdated(); + service.setSourceArtifact(artifactInfo.getArtifactName()); + service = serviceRepository.save(service); + + // We're dealing with main artifact so reference is saved or updated one. + reference = service; + } else { + // It's a secondary artifact just for messages or metadata. We'll have problems if not having an existing service... + if (existingService == null) { + log.warn( + "Trying to import {} as a secondary artifact but there's no existing [{}, {}] Service. Just skipping.", + artifactInfo.getArtifactName(), service.getName(), service.getVersion()); + break; + } + + // If metadata and operation properties found through metadata import, + // update the existing service with them. + if (service.getMetadata() != null) { + existingService.getMetadata().setLabels(service.getMetadata().getLabels()); + existingService.getMetadata().setAnnotations(service.getMetadata().getAnnotations()); + } + for (Operation operation : service.getOperations()) { + Operation existingOp = existingService.getOperations().stream() + .filter(op -> op.getName().equals(operation.getName())).findFirst().orElse(null); + if (existingOp != null) { + if (operation.getDefaultDelay() != null) { + existingOp.setDefaultDelay(operation.getDefaultDelay()); + } + if (operation.getDispatcher() != null) { + existingOp.setDispatcher(operation.getDispatcher()); + } + if (operation.getDispatcherRules() != null) { + existingOp.setDispatcherRules(operation.getDispatcherRules()); + } + if (operation.getParameterConstraints() != null) { + if (existingOp.getParameterConstraints() == null) { + existingOp.setParameterConstraints(operation.getParameterConstraints()); + } else { + existingOp.getParameterConstraints().addAll(operation.getParameterConstraints()); + } + } + } + } + + // We're dealing with secondary artifact so reference is the pre-existing one. + // Moreover, we should replace current imported service (unbound/unsaved) + // by reference in the results list. + reference = existingService; + services.remove(service); + services.add(reference); + } + + // Remove resources and messages previously attached to service. + updateArtifactResources(reference, importer, service, artifactInfo); + updateArtifactMessages(reference, importer, service, artifactInfo); + + // When extracting message information, we may have modified Operation because discovered new resource paths + // depending on variable URI parts. As a consequence, we got to update Service in repository. + serviceRepository.save(reference); + + // Publish a Service update event before returning. + publishServiceChangeEvent(reference, serviceUpdate ? ChangeType.UPDATED : ChangeType.CREATED); + } + log.info("Having imported {} services definitions into repository", services.size()); + return services; + } + + /** + * Create a new Service concerning a GenericResource for dynamic mocking. + * @param name The name of the new Service to create + * @param version The version of the new Service to create + * @param resource The resource that will be exposed as CRUD operations for this service + * @param referencePayload An optional reference payload if provided + * @return The newly created Service object + * @throws EntityAlreadyExistsException if a Service with same name and version is already present in store + */ + public Service createGenericResourceService(String name, String version, String resource, String referencePayload) + throws EntityAlreadyExistsException { + log.info("Creating a new Service '{}-{}' for generic resource {}", name, version, resource); + + // Check if corresponding Service already exists. + Service existingService = serviceRepository.findByNameAndVersion(name, version); + if (existingService != null) { + log.warn("A Service '{}-{}' is already existing. Throwing an Exception", name, version); + throw new EntityAlreadyExistsException( + String.format("Service '%s-%s' is already present in store", name, version)); + } + // Create new service with GENERIC_REST type. + Service service = new Service(); + service.setName(name); + service.setVersion(version); + service.setType(ServiceType.GENERIC_REST); + service.setMetadata(new Metadata()); + + // Now create basic crud operations for the resource. + Operation createOp = new Operation(); + createOp.setName("POST /" + resource); + createOp.setMethod("POST"); + service.addOperation(createOp); + + Operation getOp = new Operation(); + getOp.setName("GET /" + resource + "/:id"); + getOp.setMethod("GET"); + getOp.setDispatcher(DispatchStyles.URI_PARTS); + getOp.setDispatcherRules("id"); + service.addOperation(getOp); + + Operation updateOp = new Operation(); + updateOp.setName("PUT /" + resource + "/:id"); + updateOp.setMethod("PUT"); + updateOp.setDispatcher(DispatchStyles.URI_PARTS); + updateOp.setDispatcherRules("id"); + service.addOperation(updateOp); + + Operation listOp = new Operation(); + listOp.setName("GET /" + resource); + listOp.setMethod("GET"); + service.addOperation(listOp); + + Operation delOp = new Operation(); + delOp.setName("DELETE /" + resource + "/:id"); + delOp.setMethod("DELETE"); + delOp.setDispatcher(DispatchStyles.URI_PARTS); + delOp.setDispatcherRules("id"); + service.addOperation(delOp); + + serviceRepository.save(service); + log.info("Having created Service '{}' for generic resource {}", service.getId(), resource); + + // If reference payload is provided, record a first resource. + if (referencePayload != null) { + GenericResource genericResource = new GenericResource(); + genericResource.setServiceId(service.getId()); + genericResource.setReference(true); + + try { + Document document = Document.parse(referencePayload); + genericResource.setPayload(document); + genericResourceRepository.save(genericResource); + } catch (JsonParseException jpe) { + log.error("Cannot parse the provided reference payload as JSON: {}", referencePayload); + log.error("Reference is ignored, please provide JSON the next time"); + } + } + + // Publish a Service create event before returning. + publishServiceChangeEvent(service, ChangeType.CREATED); + + return service; + } + + /** + * Create a new Service concerning a Generic Event for dynamic mocking. + * @param name The name of the new Service to create + * @param version The version of the new Service to create + * @param event The event that will be exposed through a SUBSCRIBE operation + * @param referencePayload An optional reference payload if provided + * @return The newly created Service object + * @throws EntityAlreadyExistsException if a Service with same name and version is already present in store + */ + public Service createGenericEventService(String name, String version, String event, String referencePayload) + throws EntityAlreadyExistsException { + log.info("Creating a new Service '{}-{}' for generic event {}", name, version, event); + + // Check if corresponding Service already exists. + Service existingService = serviceRepository.findByNameAndVersion(name, version); + if (existingService != null) { + log.warn("A Service '{}-{}' is already existing. Throwing an Exception", name, version); + throw new EntityAlreadyExistsException( + String.format("Service '%s-%s' is already present in store", name, version)); + } + // Create new service with GENERIC_EVENT type. + Service service = new Service(); + service.setName(name); + service.setVersion(version); + service.setType(ServiceType.GENERIC_EVENT); + service.setMetadata(new Metadata()); + + // Now create basic crud operations for the resource. + Operation subscribeOp = new Operation(); + subscribeOp.setName("SUBSCRIBE " + event); + subscribeOp.setMethod("SUBSCRIBE"); + + subscribeOp.setDefaultDelay(defaultAsyncFrequency); + // Create bindings for Kafka and Websockets. + Binding kafkaBinding = new Binding(BindingType.KAFKA); + kafkaBinding.setKeyType("string"); + Binding wsBinding = new Binding(BindingType.WS); + wsBinding.setMethod("POST"); + subscribeOp.addBinding(BindingType.KAFKA.name(), kafkaBinding); + subscribeOp.addBinding(BindingType.WS.name(), wsBinding); + service.addOperation(subscribeOp); + + serviceRepository.save(service); + log.info("Having created Service '{}' for generic event {}", service.getId(), event); + + // If reference payload is provided, record a first resource. + if (referencePayload != null) { + Resource artifact = new Resource(); + artifact.setName(event + "-asyncapi.yaml"); + artifact.setType(ResourceType.ASYNC_API_SPEC); + artifact.setServiceId(service.getId()); + artifact.setSourceArtifact(event + "-asyncapi.yaml"); + artifact.setContent(buildAsyncAPISpecContent(service, event, referencePayload)); + resourceRepository.save(artifact); + + EventMessage eventMessage = new EventMessage(); + eventMessage.setName("Reference"); + eventMessage.setContent(referencePayload); + eventMessage.setOperationId(IdBuilder.buildOperationId(service, subscribeOp)); + eventMessage.setMediaType("application/json"); + eventMessageRepository.save(eventMessage); + log.info("Having created resource '{}' for Service '{}'", artifact.getId(), service.getId()); + } + + // Publish a Service create event before returning. + publishServiceChangeEvent(service, ChangeType.CREATED); + + return service; + } + + /** + * Remove a Service and its bound documents using the service id. + * @param id The identifier of service to remove. + * @param userInfo The current user information to check if authorized to delete + * @return True if service is not found or found and deleted, false otherwise. + */ + public Boolean deleteService(String id, UserInfo userInfo) { + // Get service to remove. + Service service = getServiceById(id); + + if (service == null) { + log.warn("Service [{}] not found for deletion", id); + return true; + } + + if (authorizationChecker.hasRole(userInfo, AuthorizationChecker.ROLE_ADMIN) + || authorizationChecker.hasRoleForService(userInfo, AuthorizationChecker.ROLE_MANAGER, service)) { + // Delete all resources first. + resourceRepository.deleteAll(resourceRepository.findByServiceId(id)); + + // Delete all tests related to service. + testResultRepository.deleteAll(testResultRepository.findByServiceId(id)); + + // Delete all requests and responses bound to service operation. + for (Operation operation : service.getOperations()) { + String operationId = IdBuilder.buildOperationId(service, operation); + requestRepository.deleteAll(requestRepository.findByOperationId(operationId)); + responseRepository.deleteAll(responseRepository.findByOperationId(operationId)); + eventMessageRepository.deleteAll(eventMessageRepository.findByOperationId(operationId)); + } + + // Finally delete service and publish event. + serviceRepository.delete(service); + publishServiceChangeEvent(service, ChangeType.DELETED); + log.info("Service [{}] has been fully deleted", id); + return true; + } + return false; + } + + /** + * Update the metadata for a Service. Only deals with annotations and labels and takes care of updated the + * lastUpdate marker. + * @param id The identifier of service to update metadata for + * @param metadata The new metadata for this Service + * @param userInfo The current user information to check if authorized to do the update + * @return True if service has been found and updated, false otherwise. + */ + public Boolean updateMetadata(String id, Metadata metadata, UserInfo userInfo) { + // Get service to update. + Service service = getServiceById(id); + if (service != null + && authorizationChecker.hasRoleForService(userInfo, AuthorizationChecker.ROLE_MANAGER, service)) { + service.getMetadata().setLabels(metadata.getLabels()); + service.getMetadata().setAnnotations(metadata.getAnnotations()); + service.getMetadata().objectUpdated(); + serviceRepository.save(service); + + // Publish a Service update event before returning. + publishServiceChangeEvent(service, ChangeType.UPDATED); + return true; + } + return false; + } + + /** + * Update the default delay of a Service operation + * @param id The identifier of service to update operation for + * @param operationName The name of operation to update delay for + * @param dispatcher The dispatcher to use for this operation + * @param dispatcherRules The dispatcher rules to use for this operation + * @param delay The new delay value for operation + * @param constraints Constraints for this operation parameters + * @param userInfo The current user information to check if authorized to do the update + * @return True if operation has been found and updated, false otherwise. + */ + public Boolean updateOperation(String id, String operationName, String dispatcher, String dispatcherRules, + Long delay, Set constraints, UserInfo userInfo) { + // Get service to update. + Service service = getServiceById(id); + if (service != null + && authorizationChecker.hasRoleForService(userInfo, AuthorizationChecker.ROLE_MANAGER, service)) { + for (Operation operation : service.getOperations()) { + if (operation.getName().equals(operationName)) { + operation.setDispatcher(dispatcher); + operation.setDispatcherRules(dispatcherRules); + operation.setParameterConstraints(constraints); + operation.setDefaultDelay(delay); + operation.setOverride(true); + serviceRepository.save(service); + + // Publish a Service update event before returning. + publishServiceChangeEvent(service, ChangeType.UPDATED); + return true; + } + } + } + return false; + } + + /** + * Add new sample exchanges to an existing service. + * @param id The identifier of service to add exchanges for + * @param operationName The name of operation to add exchanges for + * @param exchanges A list of exchanges to add to the corresponding service operation + * @param userInfo The current user information to check if authorized to do the update + * @return True if service operation has been found and updated, false otherwise. + */ + public Boolean addAICopilotExchangesToServiceOperation(String id, String operationName, List exchanges, + UserInfo userInfo) { + // Get service to update. + Service service = getServiceById(id); + if (service != null + && authorizationChecker.hasRoleForService(userInfo, AuthorizationChecker.ROLE_MANAGER, service)) { + for (Operation operation : service.getOperations()) { + if (operation.getName().equals(operationName)) { + String operationId = IdBuilder.buildOperationId(service, operation); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair pair) { + // Associate request and response with operation and artifact. + pair.getRequest().setOperationId(operationId); + pair.getResponse().setOperationId(operationId); + pair.getRequest().setSourceArtifact(AI_COPILOT_SOURCE); + pair.getResponse().setSourceArtifact(AI_COPILOT_SOURCE); + + // Save response and associate request with response before saving it. + responseRepository.save(pair.getResponse()); + pair.getRequest().setResponseId(pair.getResponse().getId()); + requestRepository.save(pair.getRequest()); + + } else if (exchange instanceof UnidirectionalEvent event) { + // Associate event message with operation and artifact before saving it. + event.getEventMessage().setOperationId(operationId); + event.getEventMessage().setSourceArtifact(AI_COPILOT_SOURCE); + eventMessageRepository.save(event.getEventMessage()); + } + } + // Publish a Service update event before returning. + publishServiceChangeEvent(service, ChangeType.UPDATED); + return true; + } + } + } + return false; + } + + /** + * Remove sample exchanges from an existing service. + * @param id The identifier of service to remove exchanges for + * @param exchangeSelection The selection of exchanges to remove + * @param userInfo The current user information to check if authorized to do the update + * @return True if service exchanges have been found and removed, false otherwise. + */ + public Boolean removeAICopilotExchangesFromService(String id, ExchangeSelection exchangeSelection, + UserInfo userInfo) { + // Get service to update. + Service service = getServiceById(id); + if (service != null + && authorizationChecker.hasRoleForService(userInfo, AuthorizationChecker.ROLE_MANAGER, service)) { + for (Operation operation : service.getOperations()) { + if (exchangeSelection.getExchanges().containsKey(operation.getName())) { + log.debug("Removing AI Copilot exchanges for operation {}", operation.getName()); + String operationId = IdBuilder.buildOperationId(service, operation); + List exchangeNames = exchangeSelection.getExchanges().get(operation.getName()); + + if (!ServiceType.EVENT.equals(service.getType())) { + // Remove all the requests and responses associated with the operation and AI Copilot. + requestRepository + .deleteAll(requestRepository.findByOperationIdAndSourceArtifact(operationId, AI_COPILOT_SOURCE) + .stream().filter(request -> exchangeNames.contains(request.getName())).toList()); + responseRepository + .deleteAll(responseRepository.findByOperationIdAndSourceArtifact(operationId, AI_COPILOT_SOURCE) + .stream().filter(response -> exchangeNames.contains(response.getName())).toList()); + } else { + // Remove all the event messages associated with the operation and AI Copilot. + eventMessageRepository.deleteAll(eventMessageRepository + .findByOperationIdAndSourceArtifact(operationId, AI_COPILOT_SOURCE).stream() + .filter(eventMessage -> exchangeNames.contains(eventMessage.getName())).toList()); + } + } + } + // Publish a Service update event before returning. + publishServiceChangeEvent(service, ChangeType.UPDATED); + return true; + } + return false; + } + + + /** Recopy overriden operation mutable properties into newService. */ + private void copyOverridenOperations(Service existingService, Service newService) { + for (Operation existingOperation : existingService.getOperations()) { + if (existingOperation.hasOverride()) { + for (Operation op : newService.getOperations()) { + if (existingOperation.getName().equals(op.getName())) { + op.setDefaultDelay(existingOperation.getDefaultDelay()); + op.setDispatcher(existingOperation.getDispatcher()); + op.setDispatcherRules(existingOperation.getDispatcherRules()); + op.setParameterConstraints(existingOperation.getParameterConstraints()); + op.setOverride(true); + } + } + } + } + } + + /** Manage the default values for Service of type EVENT */ + private void manageEventServiceDefaults(Service service) { + // For services of type EVENT, we should put default values on frequency and bindings. + if (service.getType().equals(ServiceType.EVENT)) { + for (Operation operation : service.getOperations()) { + if (operation.getDefaultDelay() == null) { + operation.setDefaultDelay(defaultAsyncFrequency); + } + if (operation.getBindings() == null || operation.getBindings().isEmpty()) { + operation.addBinding(defaultAsyncBinding, new Binding(BindingType.valueOf(defaultAsyncBinding))); + } + } + } + } + + /** Manage the update of previous artifact resources with new imported ones. */ + private void updateArtifactResources(Service reference, MockRepositoryImporter importer, Service service, + ArtifactInfo artifactInfo) throws MockRepositoryImportException { + + // Remove resources previously attached to service. + List existingResources = resourceRepository.findByServiceIdAndSourceArtifact(reference.getId(), + artifactInfo.getArtifactName()); + if (existingResources != null && !existingResources.isEmpty()) { + resourceRepository.deleteAll(existingResources); + } + + // Save new resources. + List resources = importer.getResourceDefinitions(service); + for (Resource resource : resources) { + resource.setServiceId(reference.getId()); + resource.setSourceArtifact(artifactInfo.getArtifactName()); + resource.setMainArtifact(artifactInfo.isMainArtifact()); + } + resourceRepository.saveAll(resources); + } + + /** Manage the update of previous artifact messages with new imported ones. */ + private void updateArtifactMessages(Service reference, MockRepositoryImporter importer, Service service, + ArtifactInfo artifactInfo) throws MockRepositoryImportException { + + for (Operation operation : reference.getOperations()) { + String operationId = IdBuilder.buildOperationId(reference, operation); + + // Remove messages previously attached to service. + requestRepository.deleteAll( + requestRepository.findByOperationIdAndSourceArtifact(operationId, artifactInfo.getArtifactName())); + responseRepository.deleteAll( + responseRepository.findByOperationIdAndSourceArtifact(operationId, artifactInfo.getArtifactName())); + eventMessageRepository.deleteAll( + eventMessageRepository.findByOperationIdAndSourceArtifact(operationId, artifactInfo.getArtifactName())); + + // Save new messages. We should use 'reference' here instead of 'service' as it may contain + // additional information to proper import messages (such as dispatch criteria inferring). + List exchanges = importer.getMessageDefinitions(reference, operation); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair pair) { + // Associate request and response with operation and artifact. + pair.getRequest().setOperationId(operationId); + pair.getResponse().setOperationId(operationId); + pair.getRequest().setSourceArtifact(artifactInfo.getArtifactName()); + pair.getResponse().setSourceArtifact(artifactInfo.getArtifactName()); + + // Save response and associate request with response before saving it. + responseRepository.save(pair.getResponse()); + pair.getRequest().setResponseId(pair.getResponse().getId()); + requestRepository.save(pair.getRequest()); + + } else if (exchange instanceof UnidirectionalEvent event) { + // Associate event message with operation and artifact before saving it.. + event.getEventMessage().setOperationId(operationId); + event.getEventMessage().setSourceArtifact(artifactInfo.getArtifactName()); + eventMessageRepository.save(event.getEventMessage()); + } + } + } + } + + /** Build generic event service associated AsyncAPI spec content. */ + private String buildAsyncAPISpecContent(Service service, String event, String referencePayload) { + InputStream stream = null; + JsonNode referenceSchema = null; + + try { + stream = ResourceUtil.getClasspathResource("templates/asyncapi-2.4.yaml"); + referenceSchema = OpenAPISchemaBuilder.buildTypeSchemaFromJson(referencePayload); + return ResourceUtil.replaceTemplatesInSpecStream(stream, service, event, referenceSchema, referencePayload); + } catch (IOException ioe) { + log.error("Exception while building ASyncAPISpec for Service '{}': {}", service.getId(), ioe.getMessage()); + return ""; + } + } + + /** Publish a ServiceChangeEvent towards minions or some other consumers. */ + private void publishServiceChangeEvent(Service service, ChangeType changeType) { + ServiceChangeEvent event = new ServiceChangeEvent(this, service.getId(), changeType); + applicationContext.publishEvent(event); + log.debug("Service change event has been published"); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ServiceStateStore.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ServiceStateStore.java new file mode 100644 index 000000000..f7a23c73d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/ServiceStateStore.java @@ -0,0 +1,76 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.service; + +import io.github.microcks.domain.ServiceState; +import io.github.microcks.repository.ServiceStateRepository; + +import javax.annotation.Nullable; +import java.util.Date; + +/** + * An implementation of {@code StateStore} scoped to Service, that uses a MongoDB repository with a collection having + * some Time To Live indices. + * @author laurent + */ +public class ServiceStateStore implements StateStore { + + private static final int DEFAULT_SECONDS_TTL = 10; + + private final ServiceStateRepository repository; + private final String serviceId; + + /** + * Build a ServiceStateStore for required elements. + * @param repository The MongoDB repository to use for persistence + * @param serviceId The ID of Service this state store will be scoped to + */ + public ServiceStateStore(ServiceStateRepository repository, String serviceId) { + this.repository = repository; + this.serviceId = serviceId; + } + + public void put(String key, String value) { + put(key, value, DEFAULT_SECONDS_TTL); + } + + public void put(String key, String value, int secondsTTL) { + ServiceState state = repository.findByServiceIdAndKey(serviceId, key); + if (state == null) { + state = new ServiceState(serviceId, key); + } + state.setValue(value); + state.setExpireAt(new Date(System.currentTimeMillis() + (secondsTTL * 1000L))); + repository.save(state); + } + + @Nullable + public String get(String key) { + ServiceState state = repository.findByServiceIdAndKey(serviceId, key); + if (state != null) { + return state.getValue(); + } + return null; + } + + public void delete(String key) { + ServiceState state = repository.findByServiceIdAndKey(serviceId, key); + if (state != null) { + repository.delete(state); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/StateStore.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/StateStore.java new file mode 100644 index 000000000..4bb95fd60 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/StateStore.java @@ -0,0 +1,55 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.service; + +import javax.annotation.Nullable; + +/** + * Simple service that allows storing some state as Key/Value pair. + * @author laurent + */ +public interface StateStore { + + /** + * Put a KV into the store. + * @param key The unique key + * @param value The value corresponding to key + */ + void put(String key, String value); + + /** + * Put a KV into the store with a specific Time To Live. + * @param key The unique key + * @param value The value corresponding to key + * @param secondsTTL The TTL in seconds + */ + void put(String key, String value, int secondsTTL); + + /** + * Retrieve a state value using its unique key. + * @param key The unique key to get value for + * @return The state value (it may be null) + */ + @Nullable + String get(String key); + + /** + * Delete a key corresponding to this unique key. + * @param key The unique key to remove value for + */ + void delete(String key); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/TestRunnerService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/TestRunnerService.java new file mode 100644 index 000000000..377597023 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/TestRunnerService.java @@ -0,0 +1,449 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import io.github.microcks.domain.OAuth2AuthorizedClient; +import io.github.microcks.domain.OAuth2ClientContext; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestCaseResult; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.domain.TestRunnerType; +import io.github.microcks.domain.TestStepResult; +import io.github.microcks.event.TestCompletionEvent; +import io.github.microcks.repository.RequestRepository; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.SecretRepository; +import io.github.microcks.repository.TestResultRepository; +import io.github.microcks.security.AuthorizationException; +import io.github.microcks.security.OAuth2AuthorizedClientProvider; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.asyncapi.AsyncAPITestRunner; +import io.github.microcks.util.graphql.GraphQLTestRunner; +import io.github.microcks.util.grpc.GrpcTestRunner; +import io.github.microcks.util.openapi.OpenAPITestRunner; +import io.github.microcks.util.postman.PostmanTestStepsRunner; +import io.github.microcks.util.soapui.SoapUIAssertionsTestRunner; +import io.github.microcks.util.test.AbstractTestRunner; +import io.github.microcks.util.test.HttpTestRunner; +import io.github.microcks.util.test.SoapHttpTestRunner; + +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.scheduling.annotation.Async; + +import javax.net.ssl.SSLContext; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Bean managing the launch of new Tests. + * @author laurent + */ +@org.springframework.stereotype.Service +public class TestRunnerService { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(TestRunnerService.class); + + /** Constant representing the header line in a custom CA Cert in PEM format. */ + private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; + /** Constant representing the footer line in a custom CA Cert in PEM format. */ + private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; + + private final ResourceRepository resourceRepository; + private final RequestRepository requestRepository; + private final ResponseRepository responseRepository; + private final TestResultRepository testResultRepository; + private final SecretRepository secretRepository; + private final ApplicationContext applicationContext; + + @Value("${tests-callback.url}") + private String testsCallbackUrl = null; + + @Value("${postman-runner.url}") + private String postmanRunnerUrl = null; + + @Value("${async-minion.url}") + private String asyncMinionUrl = null; + + @Value("${validation.resourceUrl}") + private String validationResourceUrl = null; + + /** + * Build a new TestRunnerService with the required dependencies. + * @param resourceRepository The repository to manage persistent resources + * @param requestRepository The repository to manage persistent requests + * @param responseRepository The repository to manage persistent responses + * @param testResultRepository The repository to manage persistent testResults + * @param secretRepository The repository to manage persistent secrets + * @param applicationContext The Spring application context + */ + public TestRunnerService(ResourceRepository resourceRepository, RequestRepository requestRepository, + ResponseRepository responseRepository, TestResultRepository testResultRepository, + SecretRepository secretRepository, ApplicationContext applicationContext) { + this.resourceRepository = resourceRepository; + this.requestRepository = requestRepository; + this.responseRepository = responseRepository; + this.testResultRepository = testResultRepository; + this.secretRepository = secretRepository; + this.applicationContext = applicationContext; + } + + /** + * Launch tests using asynchronous/completable future pattern. + * @param testResult TestResults to aggregate results within + * @param service Service to test + * @param runnerType Type of runner for launching the tests + * @param oAuth2Context An optional OAuth2ClientContext that may complement Secret information + * @return A Future wrapping test results + */ + @Async + public CompletableFuture launchTestsInternal(TestResult testResult, Service service, + TestRunnerType runnerType, OAuth2ClientContext oAuth2Context) { + // Found next build number for this test. + List older = testResultRepository.findByServiceId(service.getId(), + PageRequest.of(0, 2, Sort.Direction.DESC, "testNumber")); + if (older != null && !older.isEmpty() && older.get(0).getTestNumber() != null) { + testResult.setTestNumber(older.get(0).getTestNumber() + 1L); + } else { + testResult.setTestNumber(1L); + } + + Secret secret = null; + if (testResult.getSecretRef() != null) { + secret = secretRepository.findById(testResult.getSecretRef().getSecretId()).orElse(null); + log.debug("Using a secret to test endpoint? '{}'", secret != null ? secret.getName() : "none"); + } + + if (oAuth2Context != null) { + log.debug("Applying OAuth2 grant before actually running the test '{}'", oAuth2Context.getGrantType()); + OAuth2AuthorizedClientProvider tokenProvider = new OAuth2AuthorizedClientProvider(); + try { + OAuth2AuthorizedClient authorizedClient = tokenProvider.authorize(oAuth2Context); + testResult.setAuthorizedClient(authorizedClient); + if (secret != null) { + log.debug("Updating the Secret with token from OAuth2 for '{}'", authorizedClient.getPrincipalName()); + secret.setToken(authorizedClient.getEncodedAccessToken()); + secret.setTokenHeader(null); + } else { + secret = new Secret(); + secret.setToken(authorizedClient.getEncodedAccessToken()); + } + } catch (AuthorizationException authorizationException) { + log.error("OAuth2 token flow '{}' failed with: {}", oAuth2Context.getGrantType(), + authorizationException.getMessage()); + log.error("Marking the test as a failure before cancelling it"); + // Set flags and add to results before exiting loop. + testResult.setSuccess(false); + testResult.setInProgress(false); + testResult.setElapsedTime(0); + testResultRepository.save(testResult); + return CompletableFuture.completedFuture(testResult); + } + } + + // Initialize runner once as it is shared for each test. + AbstractTestRunner testRunner = retrieveRunner(runnerType, secret, testResult.getTimeout(), + service.getId()); + if (testRunner == null) { + // Set failure and stopped flags and save before exiting. + testResult.setSuccess(false); + testResult.setInProgress(false); + testResult.setElapsedTime(0); + testResultRepository.save(testResult); + return CompletableFuture.completedFuture(testResult); + } + + for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) { + // Retrieve operation corresponding to testCase. + Operation operation = service.getOperations().stream() + .filter(o -> o.getName().equals(testCaseResult.getOperationName())).findFirst().get(); + String testCaseId = IdBuilder.buildTestCaseId(testResult, operation); + + // Prepare collection of requests to launch. + List requests = requestRepository.findByOperationId(IdBuilder.buildOperationId(service, operation)); + requests = cloneRequestsForTestCase(requests, testCaseId); + + List results = new ArrayList<>(); + try { + HttpMethod method = testRunner.buildMethod(operation.getMethod()); + results = testRunner.runTest(service, operation, testResult, requests, testResult.getTestedEndpoint(), + method); + } catch (URISyntaxException use) { + log.error("URISyntaxException on endpoint {}, aborting current tests", testResult.getTestedEndpoint(), use); + // Set flags and add to results before exiting loop. + testCaseResult.setSuccess(false); + testCaseResult.setElapsedTime(0); + testResultRepository.save(testResult); + break; + } catch (Throwable t) { + log.error("Throwable while testing operation {}", operation.getName(), t); + } + + // Update result if we got returns. If no returns, it means that there's no + // sample request for that operation -> mark it as failed. + if (results == null) { + testCaseResult.setSuccess(false); + testCaseResult.setElapsedTime(0); + testResultRepository.save(testResult); + } else if (!results.isEmpty()) { + updateTestCaseResultWithReturns(testCaseResult, results, testCaseId); + testResultRepository.save(testResult); + } + } + + // Update success, progress indicators and total time before saving and returning. + updateTestResult(testResult); + + return CompletableFuture.completedFuture(testResult); + } + + + /** + * + */ + private void updateTestCaseResultWithReturns(TestCaseResult testCaseResult, List testReturns, + String testCaseId) { + // Prepare a bunch of flag we're going to complete. + boolean successFlag = true; + long caseElapsedTime = 0; + List responses = new ArrayList<>(); + List actualRequests = new ArrayList<>(); + + for (TestReturn testReturn : testReturns) { + // Deal with elapsed time and success flag. + caseElapsedTime += testReturn.getElapsedTime(); + TestStepResult testStepResult = testReturn.buildTestStepResult(); + if (!testStepResult.isSuccess()) { + successFlag = false; + } + + // Extract, complete and store response and request. + testReturn.getResponse().setTestCaseId(testCaseId); + testReturn.getRequest().setTestCaseId(testCaseId); + responses.add(testReturn.getResponse()); + actualRequests.add(testReturn.getRequest()); + + testCaseResult.getTestStepResults().add(testStepResult); + } + + // Save the responses into repository to get their ids. + log.debug("Saving {} responses with testCaseId {}", responses.size(), testCaseId); + responseRepository.saveAll(responses); + + // Associate responses to requests before saving requests. + for (int i = 0; i < actualRequests.size(); i++) { + actualRequests.get(i).setResponseId(responses.get(i).getId()); + } + log.debug("Saving {} requests with testCaseId {}", responses.size(), testCaseId); + requestRepository.saveAll(actualRequests); + + // Update and save the completed TestCaseResult. + // We cannot consider as success if we have no TestStepResults associated... + if (!testCaseResult.getTestStepResults().isEmpty()) { + testCaseResult.setSuccess(successFlag); + } + testCaseResult.setElapsedTime(caseElapsedTime); + } + + /** + * + */ + private void updateTestResult(TestResult testResult) { + log.debug("Updating total testResult"); + // Update success, progress indicators and total time before saving and returning. + boolean globalSuccessFlag = true; + boolean globalProgressFlag = false; + long totalElapsedTime = 0; + for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) { + totalElapsedTime += testCaseResult.getElapsedTime(); + if (!testCaseResult.isSuccess()) { + globalSuccessFlag = false; + } + // -1 is default elapsed time for testcase so its mean that still in + // progress because not updated yet. + if (testCaseResult.getElapsedTime() == -1) { + log.debug("testCaseResult.elapsedTime is -1, set globalProgressFlag to true"); + globalProgressFlag = true; + } + } + + // Update aggregated flags before saving whole testResult. + testResult.setSuccess(globalSuccessFlag); + testResult.setInProgress(globalProgressFlag); + testResult.setElapsedTime(totalElapsedTime); + + testResultRepository.save(testResult); + + // If test is completed, publish a completion event. + if (!testResult.isInProgress()) { + publishTestCompletionEvent(testResult); + } + } + + /** Clone and prepare request for a test case usage. */ + private List cloneRequestsForTestCase(List requests, String testCaseId) { + List result = new ArrayList<>(); + for (Request request : requests) { + Request clone = new Request(); + clone.setName(request.getName()); + clone.setContent(request.getContent()); + clone.setHeaders(request.getHeaders()); + clone.setQueryParameters(request.getQueryParameters()); + clone.setResponseId(request.getResponseId()); + // Assign testCaseId. + clone.setTestCaseId(testCaseId); + result.add(clone); + } + return result; + } + + /** Retrieve correct test runner according given type. */ + private AbstractTestRunner retrieveRunner(TestRunnerType runnerType, Secret secret, Long runnerTimeout, + String serviceId) { + // TODO: remove this ugly initialization later. + // Initialize new HttpComponentsClientHttpRequestFactory that supports https connections. + SSLContext sslContext = null; + try { + // Initialize trusting material depending on Secret content. + if (secret != null && secret.getCaCertPem() != null) { + log.debug("Test Secret contains a CA Cert, installing certificate into SSLContext"); + sslContext = SSLContexts.custom() + .loadTrustMaterial(buildCustomCaCertTruststore(secret.getCaCertPem()), null).build(); + } else { + log.debug("No Test Secret or no CA Cert found, installing accept everything strategy"); + sslContext = SSLContexts.custom().loadTrustMaterial(null, (cert, authType) -> true).build(); + } + } catch (Exception e) { + log.error("Exception while building SSLContext with acceptingTrustStrategy", e); + return null; + } + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, + new String[] { "TLSv1.2", "TLSv1.3" }, null, NoopHostnameVerifier.INSTANCE); + + // BasicHttpClientConnectionManager was facing issues detecting close connections and re-creating new ones. + // Switching to PoolingHttpClientConnectionManager for HC 5.2 solves this issue. + final PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslsf).build(); + + CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager) + .setDefaultRequestConfig( + RequestConfig.custom().setResponseTimeout(Timeout.ofMilliseconds(runnerTimeout)).build()) + .build(); + + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); + factory.setConnectTimeout(200); + + switch (runnerType) { + case SOAP_HTTP: + SoapHttpTestRunner soapRunner = new SoapHttpTestRunner(resourceRepository); + soapRunner.setClientHttpRequestFactory(factory); + soapRunner.setResourceUrl(validationResourceUrl); + soapRunner.setSecret(secret); + return soapRunner; + case OPEN_API_SCHEMA: + OpenAPITestRunner openApiRunner = new OpenAPITestRunner(resourceRepository, responseRepository, true); + openApiRunner.setClientHttpRequestFactory(factory); + openApiRunner.setResourceUrl(validationResourceUrl); + openApiRunner.setSecret(secret); + return openApiRunner; + case ASYNC_API_SCHEMA: + AsyncAPITestRunner asyncApiRunner = new AsyncAPITestRunner(resourceRepository, secretRepository); + asyncApiRunner.setClientHttpRequestFactory(factory); + asyncApiRunner.setAsyncMinionUrl(asyncMinionUrl); + return asyncApiRunner; + case GRPC_PROTOBUF: + GrpcTestRunner grpcRunner = new GrpcTestRunner(resourceRepository); + grpcRunner.setSecret(secret); + grpcRunner.setTimeout(runnerTimeout); + return grpcRunner; + case GRAPHQL_SCHEMA: + GraphQLTestRunner graphqlRunner = new GraphQLTestRunner(resourceRepository); + graphqlRunner.setClientHttpRequestFactory(factory); + graphqlRunner.setSecret(secret); + return graphqlRunner; + case POSTMAN: + PostmanTestStepsRunner postmanRunner = new PostmanTestStepsRunner(resourceRepository); + postmanRunner.setClientHttpRequestFactory(factory); + postmanRunner.setTestsCallbackUrl(testsCallbackUrl); + postmanRunner.setPostmanRunnerUrl(postmanRunnerUrl); + return postmanRunner; + case SOAP_UI: + SoapUIAssertionsTestRunner soapUIRunner = new SoapUIAssertionsTestRunner(resourceRepository); + soapUIRunner.setClientHttpRequestFactory(factory); + soapUIRunner.setResourceUrl(validationResourceUrl); + soapUIRunner.setSecret(secret); + return soapUIRunner; + default: + HttpTestRunner httpRunner = new HttpTestRunner(); + httpRunner.setClientHttpRequestFactory(factory); + httpRunner.setSecret(secret); + return httpRunner; + } + } + + /** Build a customer truststore with provided certificate in PEM format. */ + private KeyStore buildCustomCaCertTruststore(String caCertPem) throws Exception { + // First compute a stripped PEM certificate and decode it from base64. + String strippedPem = caCertPem.replace(BEGIN_CERTIFICATE, "").replace(END_CERTIFICATE, ""); + InputStream is = new ByteArrayInputStream(org.apache.commons.codec.binary.Base64.decodeBase64(strippedPem)); + + // Generate a new x509 certificate from the stripped decoded pem. + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate caCert = (X509Certificate) cf.generateCertificate(is); + + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null); // You don't need the KeyStore instance to come from a file. + ks.setCertificateEntry("caCert", caCert); + + return ks; + } + + /** Publish a TestCompletionEvent towards asynchronous consumers. */ + private void publishTestCompletionEvent(TestResult testResult) { + TestCompletionEvent event = new TestCompletionEvent(this, testResult); + applicationContext.publishEvent(event); + log.debug("Test completion event has been published"); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/TestService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/TestService.java new file mode 100644 index 000000000..77494aa9c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/service/TestService.java @@ -0,0 +1,319 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import io.github.microcks.domain.*; +import io.github.microcks.event.TestCompletionEvent; +import io.github.microcks.repository.EventMessageRepository; +import io.github.microcks.repository.RequestRepository; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.TestResultRepository; +import io.github.microcks.util.IdBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Bean defining service operations around Test domain objects. + * @author laurent + */ +@org.springframework.stereotype.Service +public class TestService { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(TestService.class); + + private final RequestRepository requestRepository; + private final ResponseRepository responseRepository; + private final EventMessageRepository eventMessageRepository; + private final TestResultRepository testResultRepository; + private final TestRunnerService testRunnerService; + private final ApplicationContext applicationContext; + + /** + * Build a new TestService with the required dependencies. + * @param requestRepository The repository to manage persistent requests + * @param responseRepository The repository to manage persistent responses + * @param eventMessageRepository The repository to manage persistent eventMessages + * @param testResultRepository The repository to manage persistent testResults + * @param testRunnerService The service for running tests + * @param applicationContext The Spring application context + */ + public TestService(RequestRepository requestRepository, ResponseRepository responseRepository, + EventMessageRepository eventMessageRepository, TestResultRepository testResultRepository, + TestRunnerService testRunnerService, ApplicationContext applicationContext) { + this.requestRepository = requestRepository; + this.responseRepository = responseRepository; + this.eventMessageRepository = eventMessageRepository; + this.testResultRepository = testResultRepository; + this.testRunnerService = testRunnerService; + this.applicationContext = applicationContext; + } + + /** + * Launch tests for a Service on dedicated endpoint URI. + * @param service Service to launch tests for + * @param testEndpoint Endpoint URI for running the tests + * @param runnerType The type of runner fo tests + * @param testOptionals Additional / optionals elements for test + * @return An initialized TestResults (mostly empty for now since tests run asynchronously) + */ + public TestResult launchTests(Service service, String testEndpoint, TestRunnerType runnerType, + TestOptionals testOptionals) { + TestResult testResult = new TestResult(); + testResult.setTestDate(new Date()); + testResult.setTestedEndpoint(testEndpoint); + testResult.setServiceId(service.getId()); + testResult.setRunnerType(runnerType); + testResult.setTimeout(testOptionals.getTimeout()); + testResult.setSecretRef(testOptionals.getSecretRef()); + testResult.setOperationsHeaders(testOptionals.getOperationsHeaders()); + + // Initialize the TestCaseResults containers before saving it. + initializeTestCaseResults(testResult, service, testOptionals); + testResultRepository.save(testResult); + + // Launch test asynchronously before returning result. + log.debug("Calling launchTestsInternal() marked as Async"); + testRunnerService.launchTestsInternal(testResult, service, runnerType, testOptionals.getOAuth2Context()); + log.debug("Async launchTestsInternal() as now finished"); + return testResult; + } + + /** + * Endpoint for reporting test case results + * @param testResultId Unique identifier of test results we report results for + * @param operationName Name of operation to report a result for + * @param testReturns List of test returns to add to this test case. + * @return A completed TestCaseResult object + */ + public TestCaseResult reportTestCaseResult(String testResultId, String operationName, List testReturns) { + log.info("Reporting a TestCaseResult for testResult {} on operation '{}'", testResultId, operationName); + TestResult testResult = testResultRepository.findById(testResultId).orElse(null); + TestCaseResult updatedTestCaseResult = null; + + // This part can be done safely with no race condition because we only + // record new requests/responses corresponding to testReturns. + // So just find the correct testCase to build a suitable id and then createTestReturns. + + for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) { + // Ensure we have a testCaseResult matching operation name. + if (testCaseResult.getOperationName().equals(operationName)) { + // If results, we need to create requests/responses pairs and associate them to testCase. + if (testReturns != null && !testReturns.isEmpty()) { + String testCaseId = IdBuilder.buildTestCaseId(testResult, operationName); + createTestReturns(testReturns, testCaseId); + } + break; + } + } + + // There may be a race condition while updating testResult at each testReturn report. + // So be prepared to catch a org.springframework.dao.OptimisticLockingFailureException and retry + // saving a bunch of time. Hopefully, we'll succeed. It does not matter if it takes time because + // everything runs asynchronously. + int times = 0; + boolean saved = false; + + while (!saved && times < 5) { + + for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) { + // Ensure we have a testCaseResult matching operation name. + if (testCaseResult.getOperationName().equals(operationName)) { + updatedTestCaseResult = testCaseResult; + // If results we now update the success flag and elapsed time of testCase? + if (testReturns == null || testReturns.isEmpty()) { + log.info("testReturns are null or empty, setting elapsedTime to -1 and success to false for {}", + operationName); + testCaseResult.setElapsedTime(-1); + testCaseResult.setSuccess(false); + } else { + updateTestCaseResultWithReturns(testCaseResult, testReturns, + TestRunnerType.ASYNC_API_SCHEMA != testResult.getRunnerType(), + TestRunnerType.ASYNC_API_SCHEMA == testResult.getRunnerType()); + } + break; + } + } + + // Finally, update success, progress indicators and total time before saving and returning. + try { + updateTestResult(testResult); + saved = true; + log.debug("testResult {} has been updated !", testResult.getId()); + // If test is completed, publish a completion event. + if (!testResult.isInProgress()) { + publishTestCompletionEvent(testResult); + } + } catch (org.springframework.dao.OptimisticLockingFailureException olfe) { + // Update counter and refresh domain object. + log.warn("Caught an OptimisticLockingFailureException, trying refreshing for {} times", times); + saved = false; + waitSomeRandomMS(5, 50); + testResult = testResultRepository.findById(testResult.getId()).orElse(null); + times++; + } + } + return updatedTestCaseResult; + } + + /** */ + private void initializeTestCaseResults(TestResult testResult, Service service, TestOptionals testOptionals) { + for (Operation operation : service.getOperations()) { + // Pick operation if no filter or present in filtered operations. + if (testOptionals.getFilteredOperations() == null || testOptionals.getFilteredOperations().isEmpty() + || testOptionals.getFilteredOperations().contains(operation.getName())) { + TestCaseResult testCaseResult = new TestCaseResult(); + testCaseResult.setOperationName(operation.getName()); + testResult.getTestCaseResults().add(testCaseResult); + } + } + } + + /** */ + private void createTestReturns(List testReturns, String testCaseId) { + List responses = new ArrayList<>(); + List actualRequests = new ArrayList<>(); + List eventMessages = new ArrayList<>(); + + for (TestReturn testReturn : testReturns) { + if (testReturn.isRequestResponseTest()) { + // Extract, complete and store response and request. + testReturn.getResponse().setTestCaseId(testCaseId); + testReturn.getRequest().setTestCaseId(testCaseId); + responses.add(testReturn.getResponse()); + actualRequests.add(testReturn.getRequest()); + } else if (testReturn.isEventTest()) { + // Complete and store event messages for tracking testCaseId. + testReturn.getEventMessage().setTestCaseId(testCaseId); + eventMessages.add(testReturn.getEventMessage()); + } + } + + if (!responses.isEmpty() && !actualRequests.isEmpty()) { + // Save the responses into repository to get their ids. + log.debug("Saving {} responses with testCaseId {}", responses.size(), testCaseId); + responseRepository.saveAll(responses); + + // Associate responses to requests before saving requests. + for (int i = 0; i < actualRequests.size(); i++) { + actualRequests.get(i).setResponseId(responses.get(i).getId()); + } + log.debug("Saving {} requests with testCaseId {}", responses.size(), testCaseId); + requestRepository.saveAll(actualRequests); + } + + if (!eventMessages.isEmpty()) { + // Save the eventMessages into repository. + log.debug("Saving {} eventMessages with testCaseId {}", eventMessages.size(), testCaseId); + eventMessageRepository.saveAll(eventMessages); + } + } + + /** + * + */ + private void updateTestCaseResultWithReturns(TestCaseResult testCaseResult, List testReturns, + boolean sumElapsedTimes, boolean findMaxElapsedTime) { + + // Prepare a bunch of flag we're going to complete. + boolean successFlag = true; + long caseElapsedTime = 0; + + for (TestReturn testReturn : testReturns) { + // Deal with elapsed time and success flag. + if (sumElapsedTimes) { + caseElapsedTime += testReturn.getElapsedTime(); + } else if (findMaxElapsedTime) { + if (testReturn.getElapsedTime() > caseElapsedTime) { + caseElapsedTime = testReturn.getElapsedTime(); + } + } + TestStepResult testStepResult = testReturn.buildTestStepResult(); + if (!testStepResult.isSuccess()) { + successFlag = false; + } + + // Add testStepResult to testCase. + testCaseResult.getTestStepResults().add(testStepResult); + } + + // Update and save the completed TestCaseResult. + // We cannot consider as success if we have no TestStepResults associated... + if (!testCaseResult.getTestStepResults().isEmpty()) { + testCaseResult.setSuccess(successFlag); + } + testCaseResult.setElapsedTime(caseElapsedTime); + log.debug("testCaseResult for {} have been updated with {} elapsedTime and success flag to {}", + testCaseResult.getOperationName(), testCaseResult.getElapsedTime(), testCaseResult.isSuccess()); + } + + /** + * + */ + private void updateTestResult(TestResult testResult) { + // Update success, progress indicators and total time before saving and returning. + boolean globalSuccessFlag = true; + boolean globalProgressFlag = false; + long totalElapsedTime = 0; + for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) { + totalElapsedTime += testCaseResult.getElapsedTime(); + if (!testCaseResult.isSuccess()) { + globalSuccessFlag = false; + } + // -1 is default elapsed time for testcase so it means that still in + // progress because not updated yet. + if (testCaseResult.getElapsedTime() == -1) { + log.debug("testCaseResult.elapsedTime is -1, set globalProgressFlag to true"); + globalProgressFlag = true; + } + } + + // Update aggregated flags before saving whole testResult. + testResult.setSuccess(globalSuccessFlag); + testResult.setInProgress(globalProgressFlag); + testResult.setElapsedTime(totalElapsedTime); + + log.debug("Trying to update testResult {} with {} elapsedTime and success flag to {}", testResult.getId(), + testResult.getElapsedTime(), testResult.isSuccess()); + testResultRepository.save(testResult); + } + + private void waitSomeRandomMS(int min, int max) { + Object semaphore = new Object(); + long timeout = ThreadLocalRandom.current().nextInt(min, max + 1); + synchronized (semaphore) { + try { + semaphore.wait(timeout); + } catch (Exception e) { + log.debug("waitSomeRandomMS semaphore was interrupted"); + } + } + } + + /** Publish a TestCompletionEvent towards asynchronous consumers. */ + private void publishTestCompletionEvent(TestResult testResult) { + TestCompletionEvent event = new TestCompletionEvent(this, testResult); + applicationContext.publishEvent(event); + log.debug("Test completion event has been published"); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/task/ImportServiceDefinitionTask.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/task/ImportServiceDefinitionTask.java new file mode 100644 index 000000000..80f8329d8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/task/ImportServiceDefinitionTask.java @@ -0,0 +1,115 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.task; + +import io.github.microcks.domain.ImportJob; +import io.github.microcks.domain.Secret; +import io.github.microcks.repository.ImportJobRepository; +import io.github.microcks.repository.SecretRepository; +import io.github.microcks.service.JobService; +import io.github.microcks.util.HTTPDownloader; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.List; + +/** + * Scheduled task responsible for periodically update Service definitions if mock repository have changed since previous + * scan. + * @author laurent + */ +@Component +public class ImportServiceDefinitionTask { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ImportServiceDefinitionTask.class); + + private static final int CHUNK_SIZE = 20; + + private final ImportJobRepository jobRepository; + private final SecretRepository secretRepository; + private final JobService jobService; + + /** + * Build a new ImportServiceDefinitionTask with required dependencies. + * @param jobRepository The job repository to use. + * @param secretRepository The secret repository to use. + * @param jobService The job service to use. + */ + public ImportServiceDefinitionTask(ImportJobRepository jobRepository, SecretRepository secretRepository, + JobService jobService) { + this.jobRepository = jobRepository; + this.secretRepository = secretRepository; + this.jobService = jobService; + } + + @Scheduled(cron = "${services.update.interval}") + public void importServiceDefinition() { + // Prepare some flags. + int updated = 0; + long startTime = System.currentTimeMillis(); + + log.info("Starting scan of Service definitions update scheduled task..."); + + long numJobs = jobRepository.count(); + log.debug("Found {} jobs to check. Splitting in {} chunks.", numJobs, (numJobs / CHUNK_SIZE + 1)); + + for (int i = 0; i < numJobs / CHUNK_SIZE + 1; i++) { + List jobs = jobRepository.findAll(PageRequest.of(i, CHUNK_SIZE)).getContent(); + log.debug("Found {} jobs into chunk {}", jobs.size(), i); + + for (ImportJob job : jobs) { + log.debug("Dealing with job {}", job.getName()); + if (job.isActive()) { + + // Retrieve associated secret if any. + Secret jobSecret = null; + if (job.getSecretRef() != null) { + log.debug("Retrieving secret {} for job {}", job.getSecretRef().getName(), job.getName()); + jobSecret = secretRepository.findById(job.getSecretRef().getSecretId()).orElse(null); + } + + // Get older and fresh Etag if any. + String etag = job.getEtag(); + String freshEtag = null; + try { + freshEtag = HTTPDownloader.getURLEtag(job.getRepositoryUrl(), jobSecret, + job.isRepositoryDisableSSLValidation()); + } catch (IOException ioe) { + log.error("Got an IOException while checking ETag for {}, pursuing...", job.getRepositoryUrl()); + } + + // Test if we must update this service definition. + if (freshEtag == null || (freshEtag != null && !freshEtag.equals(etag))) { + log.debug("No Etag or fresher one found, updating service definition for {}", job.getName()); + + job.setEtag(freshEtag); + jobService.doImportJob(job); + + updated++; + } + } + } + } + long duration = System.currentTimeMillis() - startTime; + log.info("Task end in {} ms, updating {} job services definitions", duration, updated); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/AbsoluteUrlMatcher.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/AbsoluteUrlMatcher.java new file mode 100644 index 000000000..5a2bd9788 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/AbsoluteUrlMatcher.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import java.util.regex.Pattern; + +/** + * Util class to Match a Absolute URL + */ +public class AbsoluteUrlMatcher { + static final String regex = ".+://.*"; + + static final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE); + + public static boolean matches(String url) { + return pattern.matcher(url).matches(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/AbstractJsonRepositoryImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/AbstractJsonRepositoryImporter.java new file mode 100644 index 000000000..b6e7fd775 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/AbstractJsonRepositoryImporter.java @@ -0,0 +1,346 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * An abstract class that may be used as a base for implementing MockRepositoryImporter that are using JSON/YAML file as + * a repository. It provides utility methods for handling references and loading of external resources, resolution of + * JSON pointers and so on... + * @author laurent + */ +public abstract class AbstractJsonRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(AbstractJsonRepositoryImporter.class); + + protected Boolean isYaml; + protected JsonNode rootSpecification; + protected String rootSpecificationContent; + protected ReferenceResolver referenceResolver; + protected List externalResources = new ArrayList<>(); + protected Map externalResourcesContent = new HashMap<>(); + + /** + * Build a new importer using the path of specification file and an optional reference resolver. + * @param specificationFilePath The path to local JSON spec file + * @param referenceResolver An optional resolver for references present into the OpenAPI file + * @throws IOException if JSON spec file cannot be found or read. + */ + protected AbstractJsonRepositoryImporter(String specificationFilePath, ReferenceResolver referenceResolver) + throws IOException { + this.referenceResolver = referenceResolver; + BufferedReader reader = null; + try { + // Analyse first lines of file content to guess repository type. + String line = null; + int lineNumber = 0; + reader = Files.newBufferedReader(new File(specificationFilePath).toPath(), StandardCharsets.UTF_8); + while ((line = reader.readLine()) != null && isYaml == null) { + line = line.trim(); + // Only treat as JSON if the very first line starts with { or [ + if (lineNumber == 0 && (line.startsWith("{") || line.startsWith("["))) { + isYaml = false; + } else if (line.startsWith("---") || line.startsWith("-") || line.startsWith("openapi: ") + || line.startsWith("asyncapi: ")) { + isYaml = true; + } + lineNumber++; + } + + // Read spec bytes. + byte[] bytes = Files.readAllBytes(Paths.get(specificationFilePath)); + rootSpecificationContent = new String(bytes, StandardCharsets.UTF_8); + + // Convert them to Node using Jackson object mapper. + ObjectMapper mapper = getObjectMapper(isYaml); + rootSpecification = mapper.readTree(rootSpecificationContent.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + log.error("Exception while parsing JSON specification file " + specificationFilePath, e); + throw new IOException("JSON spec file parsing error"); + } finally { + if (reader != null) { + reader.close(); + } + } + } + + /** + * Given a service, initialize all external references that may be found via `$ref` nodes pointing to absolute or + * relative pointers. This has the side effect to initialize the `externalResources` member. + * @param service The main service for naming discovered resources. + */ + protected void initializeReferencedResources(Service service) throws MockRepositoryImportException { + if (referenceResolver != null) { + String rootBaseUrl = referenceResolver.getBaseRepositoryUrl(); + + // Keep track and collect reference resources, using absolute URL as key. + Map referenceResources = new HashMap<>(); + + // We need to create a temporary root resource to initiate the recursive resolution. + Resource rootResource = new Resource(); + rootResource.setName(service.getName() + "-" + service.getVersion()); + rootResource.setContent(rootSpecificationContent); + referenceResources.put(rootBaseUrl, rootResource); + + // Seek, resolve and build reference resources. + resolveExternalReferences(service, referenceResources, rootBaseUrl, "", rootSpecification); + + // Secondly: update root specification content with new reference names and + // Refresh the root specification node with re-normalized references. + rootSpecificationContent = rootResource.getContent(); + try { + ObjectMapper mapper = getObjectMapper(isYaml); + rootSpecification = mapper.readTree(rootSpecificationContent.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + log.error("Exception while parsing re-normalized JSON specification file", e); + throw new MockRepositoryImportException("Exception while parsing re-normalized JSON specification file"); + } + + // Finally try to clean up resolved references and associated resources (files) + referenceResolver.cleanResolvedReferences(); + } + } + + /** + * Recursive method for browsing resource spec, finding $ref, downloading content and accumulating result in + * referenceResources. + */ + private void resolveExternalReferences(Service service, Map referenceResources, + String baseRepositoryUrl, String baseContext, JsonNode resourceSpecification) { + Set references = findAllExternalRefs(resourceSpecification); + Resource currentResource = referenceResources.get(baseRepositoryUrl); + + for (String ref : references) { + referenceResolver.setBaseRepositoryUrl(baseRepositoryUrl); + String refUrl = referenceResolver.getReferenceURL(ref); + + Resource referenceResource = referenceResources.get(refUrl); + if (referenceResource == null) { + try { + // Extract content using resolver. + String content = referenceResolver.getReferenceContent(ref, StandardCharsets.UTF_8); + + // Build resource name from short name. + String resourceName = ref.substring(ref.lastIndexOf('/') + 1); + String referencePath = ref.contains("/") ? ref.substring(0, ref.lastIndexOf('/')) : "."; + // Introduce a context tracker if it's a relative dependency. + String referenceContext = buildContext(baseContext, referencePath); + if (!ref.startsWith("http")) { + resourceName = IdBuilder.buildResourceFullName(service, resourceName, referenceContext); + } else { + resourceName = IdBuilder.buildResourceFullName(service, resourceName); + } + + // Build a new resource from content. Use the escaped operation path. + referenceResource = new Resource(); + referenceResource.setName(resourceName); + referenceResource.setPath(ref); + referenceResource.setContent(content); + referenceResource.setType(guessResourceType(ref, content)); + + // Keep track of this newly created resource. + referenceResources.put(refUrl, referenceResource); + externalResources.add(referenceResource); + + // Now go down the resource content and resolve its embedded references. + // Also update the references bases to track the root url for this ref in order + ObjectMapper mapper = getObjectMapper(!ref.endsWith(".json")); + JsonNode refResourceSpecification = mapper.readTree(content); + resolveExternalReferences(service, referenceResources, refUrl, referenceContext, + refResourceSpecification); + } catch (IOException ioe) { + log.error("IOException while trying to resolve reference {}", ref, ioe); + log.info("Ignoring the reference {} cause it could not be resolved", ref); + } + } + + if (!ref.startsWith("http") && referenceResource != null) { + // If a relative resource, replace with new name in resource content. + String refNewName = referenceResources.get(refUrl).getName(); + String normalizedContent = currentResource.getContent().replace(ref, + URLEncoder.encode(refNewName, StandardCharsets.UTF_8)); + currentResource.setContent(normalizedContent); + } + } + } + + /** + * Build a context for resource name in order to avoid name collisions (resource having same short name in different + * folders. + */ + private String buildContext(String baseContext, String referencePath) { + // Treat the obvious "." case. + if (".".equals(referencePath)) { + return baseContext; + } + // Else recompose a path to append. + String pathToAppend = referencePath; + while (pathToAppend.startsWith("../")) { + if (baseContext.contains("/")) { + baseContext = baseContext.substring(0, baseContext.lastIndexOf("/")); + } + pathToAppend = pathToAppend.substring(3); + } + if (pathToAppend.startsWith("./")) { + pathToAppend = pathToAppend.substring(2); + } + if (pathToAppend.startsWith("/")) { + pathToAppend = pathToAppend.substring(1); + } + String result = baseContext + "/" + pathToAppend; + return result.startsWith("/") ? result.substring(1) : result; + } + + /** Follow the $ref if we have one. Otherwise, return given node. */ + protected JsonNode followRefIfAny(JsonNode referencableNode) { + if (referencableNode.has("$ref")) { + String ref = referencableNode.path("$ref").asText(); + return getNodeForRef(ref); + } + return referencableNode; + } + + /** Get the string representation of a node in spec. */ + protected String getValueString(JsonNode valueNode) { + // Get string representation if array or object. + if (valueNode.getNodeType() == JsonNodeType.ARRAY || valueNode.getNodeType() == JsonNodeType.OBJECT) { + return valueNode.toString(); + } + // Else get raw representation. + return valueNode.asText(); + } + + /** Get appropriate Yaml or Json object mapper. */ + protected ObjectMapper getObjectMapper(boolean isYaml) { + return isYaml ? ObjectMapperFactory.getYamlObjectMapper() : ObjectMapperFactory.getJsonObjectMapper(); + } + + /** Browse Json node to extract references and store them into externalRefs. */ + private Set findAllExternalRefs(JsonNode node) { + Set externalRefs = new HashSet<>(); + // If node has a $ref child, it's a stop condition. + if (node.has("$ref")) { + String ref = node.path("$ref").asText(); + if (!ref.startsWith("#")) { + // Our ref could be used for examples and be something like './weather-examples.json#/0'. + String filePath = ref; + if (ref.contains("#/")) { + filePath = ref.substring(0, ref.indexOf("#/")); + } + externalRefs.add(filePath); + } + } else { + // Iterate on all other children. + Iterator children = node.elements(); + while (children.hasNext()) { + externalRefs.addAll(findAllExternalRefs(children.next())); + } + } + return externalRefs; + } + + /** Get the JsonNode for reference within the specification. */ + private JsonNode getNodeForRef(String reference) { + if (reference.startsWith("#/")) { + return rootSpecification.at(reference.substring(1)); + } + return getNodeForExternalRef(reference); + } + + /** Get the JsonNode for reference that is localed in external resource. */ + private JsonNode getNodeForExternalRef(String externalReference) { + String path = externalReference; + + // We may have a Json pointer to a specific place in external reference. + String pointerInFile = null; + if (externalReference.indexOf("#/") != -1) { + path = externalReference.substring(0, externalReference.indexOf("#/")); + pointerInFile = externalReference.substring(externalReference.indexOf("#/")); + } + + for (Resource resource : externalResources) { + // Path direct equality is for absolute ref ("http://raw.githubusercontent.com/...") + // Path equality with resource name if for relative refs that have been re-normalized ("Service name+Service version+...)) + if (path.equals(resource.getPath()) + || path.equals(URLEncoder.encode(resource.getName(), StandardCharsets.UTF_8))) { + JsonNode resourceNode = externalResourcesContent.computeIfAbsent(resource, k -> { + try { + return ObjectMapperFactory.getYamlObjectMapper().readTree(resource.getContent()); + } catch (JsonProcessingException e) { + throw new JsonRepositoryParsingException("Get a JSON processing exception on " + externalReference, + e); + } + }); + if (pointerInFile != null) { + return resourceNode.at(pointerInFile.substring(1)); + } + return resourceNode; + } + } + log.warn("Found no resource for reference {}", externalReference); + return null; + } + + /** Try to guess resource type from a reference name and its content. */ + private ResourceType guessResourceType(String ref, String content) { + if (ref.endsWith(".avsc")) { + return ResourceType.AVRO_SCHEMA; + } else if (ref.endsWith(".proto")) { + return ResourceType.PROTOBUF_SCHEMA; + } else if (content.contains("$schema") || content.contains("properties:") + || content.contains("\"properties\":")) { + return ResourceType.JSON_SCHEMA; + } + return ResourceType.JSON_FRAGMENT; + } + + /** Custom runtime exception for Json repository parsing errors. */ + public class JsonRepositoryParsingException extends RuntimeException { + public JsonRepositoryParsingException(String message) { + super(message); + } + + public JsonRepositoryParsingException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/DispatchCriteriaHelper.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/DispatchCriteriaHelper.java new file mode 100644 index 000000000..37af98e7e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/DispatchCriteriaHelper.java @@ -0,0 +1,566 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.TreeMultimap; + +import io.github.microcks.domain.Operation; +import io.github.microcks.util.dispatcher.FallbackSpecification; +import io.github.microcks.util.dispatcher.JsonMappingException; +import io.github.microcks.util.dispatcher.ProxyFallbackSpecification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * This is a helper for extracting and building dispatch criteria from many sources. + * @author laurent + */ +public class DispatchCriteriaHelper { + + private static final String CURLY_PART_PATTERN = "(\\{[^\\}]+\\})"; + private static final String CURLY_PART_EXTRACTION_PATTERN = "\\\\{(.+)\\\\}"; + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(DispatchCriteriaHelper.class); + + private DispatchCriteriaHelper() { + // Hide default no argument constructor as it's a utility class. + } + + /** + * Extract a dispatch rule string from URI parameters (specified using example values) + * @param uri The URI containing parameters + * @return A string representing dispatch rules for the corresponding incoming request. + */ + public static String extractParamsFromURI(String uri) { + if (uri.contains("?") && uri.contains("=")) { + String parameters = uri.substring(uri.indexOf("?") + 1); + StringBuilder params = new StringBuilder(); + + for (String parameter : parameters.split("&")) { + String[] pair = parameter.split("="); + String key = URLDecoder.decode(pair[0], StandardCharsets.UTF_8); + if (!params.isEmpty()) { + params.append(" && "); + } + params.append(key); + } + return params.toString(); + } + return ""; + } + + /** + * Extract the common prefix between a set of URIs + * @param uris A set of URIs that are expected to share a common prefix + * @return A string representing the common prefix of given URIs + */ + public static String extractCommonPrefix(List uris) { + String commonURIPath = uris.getFirst(); + + // 1st pass on collection: find a common prefix. + for (int prefixLen = 0; prefixLen < uris.getFirst().length(); prefixLen++) { + char c = uris.getFirst().charAt(prefixLen); + for (int i = 1; i < uris.size(); i++) { + if (prefixLen >= uris.get(i).length() || uris.get(i).charAt(prefixLen) != c) { + // Mismatch found. + String commonString = uris.get(i).substring(0, prefixLen); + return commonString.substring(0, commonString.lastIndexOf('/')); + } + } + } + return commonURIPath; + } + + /** + * Extract the common suffix between a set of URIs + * @param uris A set of URIs that are expected to share a common suffix + * @return A string representing the common suffix of given URIs + */ + public static String extractCommonSuffix(List uris) { + // 1st pass on collection: find a common suffix. + for (int suffixLen = 0; suffixLen < uris.getFirst().length(); suffixLen++) { + char c = uris.getFirst().charAt(uris.getFirst().length() - suffixLen - 1); + for (int i = 1; i < uris.size(); i++) { + if (suffixLen >= uris.get(i).length() || uris.get(i).charAt(uris.get(i).length() - suffixLen - 1) != c) { + // Mismatch found. Have we found at least one common char ? + if (suffixLen > 0) { + String commonString = uris.get(i).substring(uris.get(i).length() - suffixLen - 1); + if (commonString.indexOf('/') != -1) { + return commonString.substring(commonString.indexOf('/')); + } + return null; + } else { + return null; + } + } + } + } + return null; + } + + /** + * Extract from given URIs a dispatching rule representing the number of variable parts in this different URIs. For + * example, given 'http://s/r/f//d/m/s' and 'http://s/r/f/d', method will detect 2 variable parts ('m' and 's'). + * Because it does know anything about the semantics of this parts, it produces a generic dispatch rule + * 'part1 && part2' telling that URIs can be templatized like 'http://s/r/f/d/{part1}/{part2} + * and that this 2 parts should be taken into account when dispatching request to response. + * @param uris A set of URIs that are expected to share a common prefix + * @return A string representing dispatch rules for the corresponding incoming request. + */ + public static String extractPartsFromURIs(List uris) { + // 1st pass on collection: find a common prefix. + String commonURIPath = extractCommonPrefix(uris); + + // 2nd pass on collection: find a common suffix. + String commonURIEnd = extractCommonSuffix(uris); + + // 3rd pass on collection: guess the max number of part. + int partsLen = 0; + for (String uri : uris) { + String parts = uri.substring(commonURIPath.length() + 1); + if (commonURIEnd != null) { + parts = parts.substring(0, parts.lastIndexOf(commonURIEnd)); + } + int numOfParts = parts.split("/").length; + if (numOfParts > partsLen) { + partsLen = numOfParts; + } + } + + if (partsLen > 0) { + StringBuilder parts = new StringBuilder(); + for (int i = 0; i < partsLen; i++) { + parts.append("part").append(i + 1); + if (i < partsLen - 1) { + parts.append(" && "); + } + } + return parts.toString(); + } + return ""; + } + + /** + * Build a template URL like 'http://s/r/f/d/{part1}/{part2}' with parts extracted from given URIs. For example, + * given 'http://s/r/f/d/m/s' and 'http://s/r/f/d', method will detect 2 variable parts ('m' and 's'). Because it + * does know anything about the semantics of this parts, it produces a generic dispatch rule + * 'part1 && part2' telling that URIs can be templatized like 'http://s/r/f/d/{part1}/{part2}' + * @param uris A set of URIs that are expected to share a common prefix + * @return A templatized URL containing parts. + */ + public static String buildTemplateURLWithPartsFromURIs(List uris) { + // 1st pass on collection: find a common prefix. + String commonURIPath = extractCommonPrefix(uris); + + // 2nd pass on collection: find a common suffix. + String commonURIEnd = extractCommonSuffix(uris); + + StringBuilder partsURI = new StringBuilder(); + partsURI.append(commonURIPath); + + int partsLen = 0; + for (String uri : uris) { + String parts = uri.substring(commonURIPath.length() + 1); + if (commonURIEnd != null) { + parts = parts.substring(0, parts.lastIndexOf(commonURIEnd)); + } + int numOfParts = parts.split("/").length; + if (numOfParts > partsLen) { + partsLen = numOfParts; + } + } + + if (partsLen > 0) { + for (int i = 0; i < partsLen; i++) { + if (i == 0) { + partsURI.append("/"); + } + partsURI.append("{part").append(i + 1).append("}"); + if (i < partsLen - 1) { + partsURI.append("/"); + } + } + } + + if (commonURIEnd != null) { + partsURI.append("/").append(commonURIEnd); + } + + return partsURI.toString(); + } + + /** + * Extract a dispatch rule string from URI pattern (containing variable parts within {}) in order to explain which + * parts are variables. + * @param pattern The URI pattern containing variables parts ({} or :part patterns) + * @return A string representing dispatch rules for the corresponding incoming request. + */ + public static String extractPartsFromURIPattern(String pattern) { + // Sanitize pattern as it may contains query params expressed using '{{}}'. + if (pattern.contains("?")) { + pattern = pattern.substring(0, pattern.indexOf('?')); + } + // and as it may contains variables using '{{}}'. + if (pattern.contains("{{") && pattern.contains("}}")) { + pattern = pattern.replace("{{", ""); + pattern = pattern.replace("}}", ""); + } + // and as it may contains $ signs with forms like '/items/$count'. + if (pattern.contains("$")) { + pattern = pattern.replace("$", ""); + } + + // Build a pattern for extracting parts from pattern. + String partsPattern = null; + if (pattern.contains("/{")) { + partsPattern = pattern.replaceAll(CURLY_PART_PATTERN, CURLY_PART_EXTRACTION_PATTERN); + } else if (pattern.contains("/:")) { + // We should add a leading / to avoid getting port number ;-) + partsPattern = pattern.replaceAll("(/:[^:^/]+)", "\\/:(.+)"); + } + return buildPartsDispatchRule(pattern, partsPattern); + } + + /** + * Extract a dispatch rule string from String pattern (containing variable parts within {}) in order to explain which + * parts are variables. + * @param pattern The String pattern containing variables parts ({} patterns) + * @return A string representing dispatch rules for the corresponding incoming request. + */ + public static String extractPartsFromStringPattern(String pattern) { + // Build a pattern for extracting parts from pattern. + String partsPattern = null; + if (pattern.contains("{")) { + partsPattern = pattern.replaceAll(CURLY_PART_PATTERN, CURLY_PART_EXTRACTION_PATTERN); + } + return buildPartsDispatchRule(pattern, partsPattern); + } + + /** Apply a regexp on pattern to extract parts and create a dispatch rule. */ + private static String buildPartsDispatchRule(String pattern, String partsExtractPattern) { + if (partsExtractPattern != null) { + Pattern partsP = Pattern.compile(partsExtractPattern); + Matcher partsM = partsP.matcher(pattern); + + if (partsM.matches()) { + StringBuilder parts = new StringBuilder(); + for (int i = 1; i < partsM.groupCount() + 1; i++) { + parts.append(partsM.group(i)); + if (i < partsM.groupCount()) { + parts.append(" && "); + } + } + return parts.toString(); + } + } + return ""; + } + + /** + * Extract and build a dispatch criteria string from URI pattern (containing variable parts within {} or prefixed + * with :), projected onto a real instanciated URI. + * @param paramsRuleString The dispatch rules referencing parameters to consider + * @param pattern The URI pattern containing variables parts ({}) + * @param realURI The real URI that should match pattern. + * @return A string representing dispatch criteria for the corresponding incoming request. + */ + public static String extractFromURIPattern(String paramsRuleString, String pattern, String realURI) { + Map criteriaMap = extractMapFromURIPattern(paramsRuleString, pattern, realURI); + + // Just appends sorted entries, separating them with /. + StringBuilder result = new StringBuilder(); + for (Map.Entry criteria : criteriaMap.entrySet()) { + result.append("/").append(criteria.getKey()).append("=").append(criteria.getValue()); + } + return result.toString(); + } + + /** + * Extract a map of parameters from URI pattern (containing variable parts within '{}' or prefixed with ':'), + * projected onto a real instantiated URI. The extracted map only contains values for parameters referenced in + * {@code paramsRuleString}. + * @param paramsRuleString The dispatch rules referencing parameters to consider + * @param pattern The URI pattern containing variables parts ({}) + * @param realURI The real URI that should match pattern. + * @return A map of parameters extracted from the URI for the corresponding incoming request. + * @see #extractMapFromURIPattern(String, String) + */ + public static Map extractMapFromURIPattern(String paramsRuleString, String pattern, String realURI) { + // Rule string can be a URI_ELEMENT rule and containers ?? elements. + // We must remove them before parsing the URI parts. + if (paramsRuleString.contains("??")) { + paramsRuleString = paramsRuleString.split("\\?\\?")[0]; + } + final var paramsRule = Arrays.stream(paramsRuleString.split("&&")).map(String::trim).distinct() + .collect(Collectors.toUnmodifiableSet()); + + // Ensure realURI does not contain query string. + if (realURI.contains("?")) { + realURI = realURI.substring(0, realURI.indexOf('?')); + } + + // Filter the extracted parameter map by the referenced parameters in paramsRule + return extractMapFromURIPattern(pattern, realURI).entrySet().stream() + .filter(entry -> paramsRule.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * Extract a map of parameters from URI pattern (containing variable parts within '{}' or prefixed with ':'), + * projected onto a real instantiated URI. + * @param pattern The URI pattern containing variables parts ({}) + * @param realURI The real URI that should match pattern. + * @return A map of parameters extracted from the URI for the corresponding incoming request. + * @see #extractMapFromURIPattern(String, String, String) + */ + public static Map extractMapFromURIPattern(String pattern, String realURI) { + Map criteriaMap = new TreeMap<>(); + pattern = sanitizeURLForRegExp(pattern); + realURI = sanitizeURLForRegExp(realURI); + + // Build a pattern for extracting parts from pattern and a pattern for extracting values + // from realURI. Supporting both {id} and :id. + String partsPattern = null; + String valuesPattern = null; + if (pattern.contains("/{")) { + partsPattern = pattern.replaceAll(CURLY_PART_PATTERN, CURLY_PART_EXTRACTION_PATTERN); + valuesPattern = pattern.replaceAll(CURLY_PART_PATTERN, "(.+)"); + } else { + partsPattern = pattern.replaceAll("(:[^:^/]+)", "\\:(.+)"); + valuesPattern = pattern.replaceAll("(:[^:^/]+)", "(.+)"); + } + if (pattern.contains("$")) { + partsPattern = partsPattern.replace("$", "\\$"); + valuesPattern = valuesPattern.replace("$", "\\$"); + } + Pattern partsP = Pattern.compile(partsPattern); + Matcher partsM = partsP.matcher(pattern); + + Pattern valuesP = Pattern.compile(valuesPattern); + Matcher valuesM = valuesP.matcher(realURI); + + // Both should match and have the same group count. + if (valuesM.matches() && partsM.matches() && valuesM.groupCount() == partsM.groupCount()) { + for (int i = 1; i < partsM.groupCount() + 1; i++) { + final String paramName = partsM.group(i); + final String paramValue = valuesM.group(i); + criteriaMap.put(paramName, paramValue); + } + } + return criteriaMap; + } + + /** + * Build a dispatch criteria string from map of parts (key is part name, value is part real value) + * @param partsRule The dispatch rules referencing parts to consider + * @param partsMap The Map containing parts (not necessarily sorted) + * @return A string representing dispatch criteria for the corresponding incoming request. + */ + public static String buildFromPartsMap(String partsRule, Map partsMap) { + if (partsMap != null && !partsMap.isEmpty()) { + Multimap multimap = partsMap.entrySet().stream().collect(ArrayListMultimap::create, + (m, e) -> m.put(e.getKey(), e.getValue()), Multimap::putAll); + return buildFromPartsMap(partsRule, multimap); + } + return ""; + } + + /** + * Build a dispatch criteria string from map of parts (key is part name, value is part real value) + * @param partsRule The dispatch rules referencing parts to consider + * @param partsMap The Multimap containing parts (not necessarily sorted) + * @return A string representing dispatch criteria for the corresponding incoming request. + */ + public static String buildFromPartsMap(String partsRule, Multimap partsMap) { + // We may have a partsRule for URI_ELEMENT with params parts, ignore this part. + if (partsRule.contains("??")) { + partsRule = partsRule.split("\\?\\?")[0]; + } + + if (partsMap != null && !partsMap.isEmpty()) { + Multimap criteriaMap = TreeMultimap.create(partsMap); + + // Just appends sorted entries, separating them with /. + StringBuilder result = new StringBuilder(); + for (Map.Entry criteria : criteriaMap.entries()) { + /* + * Check that criteria is embedded into the rule. Simply check word boundary with \b is not enough as - are + * valid in params (according RFC 3986) but not included into word boundary - so "word-ext" string is + * matching ".*\\bword\\b.*" We need to tweak it a bit to prevent matching when there's a - before or after + * the criteria we're looking for (see + * https://stackoverflow.com/questions/32380375/hyphen-dash-to-be-included-in-regex-word-boundary-b) + */ + if (partsRule.matches(".*(^|[^-])\\b" + criteria.getKey() + "\\b([^-]|$).*")) { + result.append("/").append(criteria.getKey()).append("=").append(criteria.getValue()); + } + } + return result.toString(); + } + return ""; + } + + /** + * Build a dispatch criteria string from URI parameters contained into a map + * @param paramsRule The dispatch rules referencing parameters to consider + * @param paramsMap The Map containing URI params (not necessarily sorted) + * @return A string representing a dispatch criteria for the corresponding incoming request. + */ + public static String buildFromParamsMap(String paramsRule, Multimap paramsMap) { + // We may have a paramsRule for URI_ELEMENT with path parts, ignore this part. + if (paramsRule.contains("??")) { + paramsRule = paramsRule.split("\\?\\?")[1]; + } + + if (paramsMap != null && !paramsMap.isEmpty()) { + Multimap criteriaMap = TreeMultimap.create(paramsMap); + + // Just appends sorted entries, separating them with ?. + StringBuilder result = new StringBuilder(); + for (Map.Entry criteria : criteriaMap.entries()) { + /* + * Check that criteria is embedded into the rule. Simply check word boundary with \b is not enough as - are + * valid in params (according RFC 3986) but not included into word boundary - so "word-ext" string is + * matching ".*\\bword\\b.*" We need to tweak it a bit to prevent matching when there's a - before or after + * the criteria we're looking for (see + * https://stackoverflow.com/questions/32380375/hyphen-dash-to-be-included-in-regex-word-boundary-b) + */ + if (paramsRule.matches(".*(^|[^-])\\b" + criteria.getKey() + "\\b([^-]|$).*")) { + result.append("?").append(criteria.getKey()).append("=").append(criteria.getValue()); + } + } + return result.toString(); + } + return ""; + } + + /** + * Extract and build a dispatch criteria string from URI parameters + * @param paramsRule The dispatch rules referencing parameters to consider + * @param uri The URI from which we should build a specific dispatch criteria + * @return A string representing a dispatch criteria for the corresponding incoming request. + */ + public static String extractFromURIParams(String paramsRule, String uri) { + Multimap criteriaMap = extractMapFromURIParams(paramsRule, uri); + + // Just appends sorted entries, separating them with ?. + StringBuilder result = new StringBuilder(); + for (Map.Entry criteria : criteriaMap.entries()) { + if (paramsRule.contains(criteria.getKey())) { + result.append("?").append(criteria.getKey()).append("=").append(criteria.getValue()); + } + } + return result.toString(); + } + + /** + * Extract a map of parameters from URI parameters + * @param paramsRule The dispatch rules referencing parameters to consider + * @param uri The URI from which we should build a specific dispatch criteria + * @return A map of parameters extracted from the URI for the corresponding incoming request. + */ + public static Multimap extractMapFromURIParams(String paramsRule, String uri) { + Multimap criteriaMap = TreeMultimap.create(); + + if (uri.contains("?") && uri.contains("=")) { + String parameters = uri.substring(uri.indexOf("?") + 1); + + for (String parameter : parameters.split("&")) { + String[] pair = parameter.split("="); + if (pair.length > 1) { + String key = URLDecoder.decode(pair[0], StandardCharsets.UTF_8); + String value = URLDecoder.decode(pair[1], StandardCharsets.UTF_8); + if (paramsRule.contains(key)) { + criteriaMap.put(key, value); + } + } + } + } + return criteriaMap; + } + + /** + * Extract and build a dispatch criteria string from URI parameters already stored into a Map. + * @param paramsRule The dispatch rules referencing parameters to consider + * @param paramMap The URI fetched params from which we should build a specific dispatch criteria + * @return A string representing a dispatch criteria for the corresponding incoming request. + */ + public static String extractFromParamMap(String paramsRule, Map paramMap) { + Set sortedKeys = paramMap.keySet().stream().sorted().collect(Collectors.toSet()); + + StringBuilder result = new StringBuilder(); + for (String param : sortedKeys) { + if (paramsRule.contains(param)) { + result.append("?").append(param).append("=").append(paramMap.get(param)); + } + } + return result.toString(); + } + + /** + * Get the root dispatcher and dispatcher rules for an operation, taking into account fallback and proxy-fallback + * @param operation The operation for which we want to extract dispatcher details + * @return A DispatcherDetails object containing root dispatcher and dispatcher rules + */ + public static DispatcherDetails extractDispatcherWithRules(Operation operation) { + String rootDispatcher = operation.getDispatcher(); + String rootDispatcherRules = operation.getDispatcherRules(); + + if (DispatchStyles.FALLBACK.equals(operation.getDispatcher())) { + try { + FallbackSpecification fallbackSpec = FallbackSpecification + .buildFromJsonString(operation.getDispatcherRules()); + rootDispatcher = fallbackSpec.getDispatcher(); + rootDispatcherRules = fallbackSpec.getDispatcherRules(); + } catch (JsonMappingException e) { + log.warn("Operation '{}' has a malformed Fallback dispatcher rules", operation.getName()); + } + } + if (DispatchStyles.PROXY_FALLBACK.equals(operation.getDispatcher())) { + try { + ProxyFallbackSpecification proxyFallbackSpec = ProxyFallbackSpecification + .buildFromJsonString(operation.getDispatcherRules()); + rootDispatcher = proxyFallbackSpec.getDispatcher(); + rootDispatcherRules = proxyFallbackSpec.getDispatcherRules(); + } catch (JsonMappingException e) { + log.warn("Operation '{}' has a malformed Proxy-Fallback dispatcher rules", operation.getName()); + } + } + + return new DispatcherDetails(rootDispatcher, rootDispatcherRules); + } + + public record DispatcherDetails(String rootDispatcher, String rootDispatcherRules) { + } + + private static String sanitizeURLForRegExp(String url) { + return url.replace("+", " "); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/DispatchStyles.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/DispatchStyles.java new file mode 100644 index 000000000..a41f9977f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/DispatchStyles.java @@ -0,0 +1,66 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +/** + * Handler for dispatch styles constants. + * @author laurent + */ +public class DispatchStyles { + + /** Constant for QUERY_MATCH dispatch style. */ + public static final String QUERY_MATCH = "QUERY_MATCH"; + + /** Constant for QUERY_ARGS dispatch style. */ + public static final String QUERY_ARGS = "QUERY_ARGS"; + + /** Constant for QUERY_HEADER dispatch style. */ + public static final String QUERY_HEADER = "QUERY_HEADER"; + + /** Constant for SCRIPT dispatch style. */ + public static final String SCRIPT = "SCRIPT"; + + /** Constant for SEQUENCE dispatch style. */ + public static final String SEQUENCE = "SEQUENCE"; + + /** Constant for URI_PARAMS dispatch style. */ + public static final String URI_PARAMS = "URI_PARAMS"; + + /** Constant for URI_PARTS dispatch style. */ + public static final String URI_PARTS = "URI_PARTS"; + + /** Constant for URI_ELEMENTS dispatch style (PARTS and PARAMS). */ + public static final String URI_ELEMENTS = "URI_ELEMENTS"; + + /** Constant for JSON_BODY dispatch style. */ + public static final String JSON_BODY = "JSON_BODY"; + + /** Constant for PROXY dispatch style. */ + public static final String PROXY = "PROXY"; + + /** Constant for FALLBACK dispatch style. */ + public static final String FALLBACK = "FALLBACK"; + + /** Constant for PROXY_FALLBACK dispatch style. */ + public static final String PROXY_FALLBACK = "PROXY_FALLBACK"; + + /** Constant for RANDOM dispatch style. */ + public static final String RANDOM = "RANDOM"; + + private DispatchStyles() { + // Hide default no argument constructor as it's a utility class. + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/EntityAlreadyExistsException.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/EntityAlreadyExistsException.java new file mode 100644 index 000000000..0af3e5099 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/EntityAlreadyExistsException.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +/** + * A typed exception telling that an entity with similar properties already exists into datastore. + * @author laurent + */ +public class EntityAlreadyExistsException extends Exception { + + public EntityAlreadyExistsException(String message) { + super(message); + } + + public EntityAlreadyExistsException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/GitLabReferenceURLBuilder.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/GitLabReferenceURLBuilder.java new file mode 100644 index 000000000..01f816002 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/GitLabReferenceURLBuilder.java @@ -0,0 +1,86 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * An implementation of RelativeReferenceURLBuilder that follows GitLab conventions for encoding file path + * into remote URL. + * @author laurent + */ +public class GitLabReferenceURLBuilder extends SimpleReferenceURLBuilder { + + /** Header used by GitLab APIs to provide the downloaded file name. */ + public static final String GITLAB_FILE_NAME_HEADER = "X-Gitlab-File-Name"; + + private static final String REPOSITORY_FILES_MARKER = "/repository/files/"; + private static final String ENCODED_FILE_SEPARATOR = "%2F"; + + @Override + public String getFileName(String baseRepositoryURL, Map> headers) { + if (headers != null) { + for (String key : headers.keySet()) { + if (key != null && key.equalsIgnoreCase(GITLAB_FILE_NAME_HEADER)) { + return headers.get(key).get(0); + } + } + } + + // If not present, extract from raw URL. + String lastPath = baseRepositoryURL.substring(baseRepositoryURL.lastIndexOf("/") + 1); + if (lastPath.startsWith("raw?ref=") || lastPath.indexOf('.') == -1) { + String[] pathElements = baseRepositoryURL.split("/"); + int i = pathElements.length; + while (i > 0) { + String path = pathElements[i - 1]; + if (path.contains(".") && path.contains(ENCODED_FILE_SEPARATOR)) { + return path.substring(path.indexOf(ENCODED_FILE_SEPARATOR) + ENCODED_FILE_SEPARATOR.length()); + } + i--; + } + } + return null; + } + + @Override + public String buildRemoteURL(String baseRepositoryURL, String referencePath) { + // https://gitlab.com/api/v4/projects/35980862/repository/files/folder%2Fsubfolder%2Ffilename/raw?ref=branch + + // https://gitlab.com/api/v4/projects/35980862/repository/files + String rootURL = baseRepositoryURL.substring(0, + baseRepositoryURL.indexOf(REPOSITORY_FILES_MARKER) + REPOSITORY_FILES_MARKER.length() - 1); + + // folder%2Fsubfolder%2Ffilename + String basePath = baseRepositoryURL.substring( + baseRepositoryURL.indexOf(REPOSITORY_FILES_MARKER) + REPOSITORY_FILES_MARKER.length(), + baseRepositoryURL.lastIndexOf("/")); + + // raw?ref=branch + String formatOptions = baseRepositoryURL.substring(baseRepositoryURL.lastIndexOf("/") + 1); + + // Now do a simple reference url build. We need to ensure that there's a root + // by adding a starting /. We'll remove it after the build when recomposing result. + String pathFragment = super.buildRemoteURL("/" + URLDecoder.decode(basePath, StandardCharsets.UTF_8), + referencePath); + + return rootURL + "/" + URLEncoder.encode(pathFragment.substring(1), StandardCharsets.UTF_8) + "/" + formatOptions; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/HTTPDownloader.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/HTTPDownloader.java new file mode 100644 index 000000000..d2f28d963 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/HTTPDownloader.java @@ -0,0 +1,289 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Secret; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +/** + * This is a utility class for accessing HTTP content using diverse security authentication mechanisms and output + * formats + * + * @author laurent + */ +public class HTTPDownloader { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(HTTPDownloader.class); + + /** Constant representing the header line in a custom CA Cert in PEM format. */ + private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; + /** Constant representing the footer line in a custom CA Cert in PEM format. */ + private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; + + private HTTPDownloader() { + // Private constrcutor to hide the implicit public one. + } + + /** + * Manage the retrieval of Etag / ETag header on remote url. Depending on secret content, HTTP connection is prepared + * for handling proxy username/password, target service authentication (through basic and bearer authorization or + * customer request header), remote SSL connection through installation of CA certificate or disabling SSL validation + * (ie. accepting all certificate and hostname verifications). + * + * @param remoteUrl The remote URL to check + * @param secret The secret associated with this remote URL (if any. Can be null) + * @param disableSSLValidation Whether to disable SSL validation. If true, all SSL related information from secret + * will be ignored. + * @return The value of Etag / ETag header if any. null if none. + * @throws IOException if anything goes wrong (request preparation or execution). + */ + public static String getURLEtag(String remoteUrl, Secret secret, boolean disableSSLValidation) throws IOException { + + // Build remote URLConnection and the read response headers. + HttpURLConnection connection = prepareURLConnection(remoteUrl, secret, disableSSLValidation); + + try { + // Try simple syntax. + String etag = connection.getHeaderField("Etag"); + if (etag != null) { + log.debug("Found an Etag for {} : {}", remoteUrl, etag); + return etag; + } + // Try other syntax. + etag = connection.getHeaderField("ETag"); + if (etag != null) { + log.debug("Found an Etag for {} : {}", remoteUrl, etag); + return etag; + } + } catch (Exception e) { + log.error("Caught an exception while retrieving Etag for {}", remoteUrl, e); + } + log.debug("No Etag found for {} !", remoteUrl); + return null; + } + + /** + * Handle the HTTP/HTTPS download of remote url as a local temporary file. Depending on secret content, HTTP + * connection is prepared for handling proxy username/password, target service authentication (through basic and + * bearer authorization or customer request header), remote SSL connection through installation of CA certificate or + * disabling SSL validation (ie. accepting all certificate and hostname verifications). + * + * @param remoteUrl The remote URL to download and transfer into resulting file + * @param secret The secret associated with this remote URL (if any. Can be null) + * @param disableSSLValidation Whether to disable SSL validation. If true, all SSL related information from secret + * will be ignored. + * @return A temporary file containing downloaded content. + * @throws IOException if anything goes wrong (request preparation or execution). + */ + public static File handleHTTPDownloadToFile(String remoteUrl, Secret secret, boolean disableSSLValidation) + throws IOException { + + // Build remote URLConnection and local target file. + HttpURLConnection connection = prepareURLConnection(remoteUrl, secret, disableSSLValidation); + File localFile = File.createTempFile("microcks-" + System.currentTimeMillis(), ".download"); + + try (ReadableByteChannel rbc = Channels.newChannel(connection.getInputStream()); + // Transfer file to local. + FileOutputStream fos = new FileOutputStream(localFile);) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } + return localFile; + } + + /** + * Handle the HTTP/HTTPS download of remote url as a local temporary file. Depending on secret content, HTTP + * connection is prepared for handling proxy username/password, target service authentication (through basic and + * bearer authorization or customer request header), remote SSL connection through installation of CA certificate or + * disabling SSL validation (ie. accepting all certificate and hostname verifications). + * + * @param remoteUrl The remote URL to download and transfer into resulting file + * @param secret The secret associated with this remote URL (if any. Can be null) + * @param disableSSLValidation Whether to disable SSL validation. If true, all SSL related information from secret + * will be ignored. + * @return A temporary file containing downloaded content as well as Http download headers. + * @throws IOException if anything goes wrong (request preparation or execution). + */ + public static FileAndHeaders handleHTTPDownloadToFileAndHeaders(String remoteUrl, Secret secret, + boolean disableSSLValidation) throws IOException { + + // Build remote URLConnection and local target file. + HttpURLConnection connection = prepareURLConnection(remoteUrl, secret, disableSSLValidation); + File localFile = File.createTempFile("microcks-" + System.currentTimeMillis(), ".download"); + + Map> responseHeaders = null; + try (ReadableByteChannel rbc = Channels.newChannel(connection.getInputStream()); + // Transfer file to local. + FileOutputStream fos = new FileOutputStream(localFile);) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + responseHeaders = connection.getHeaderFields(); + } + return new FileAndHeaders(localFile, responseHeaders); + } + + /** + * Prepare an URLConnection with all the security related stuffs specified by optional secret. + */ + private static HttpURLConnection prepareURLConnection(String remoteUrl, Secret secret, boolean disableSSLValidation) + throws IOException { + + // Build remote URL and connection to prepare. + URL website = new URL(remoteUrl); + + HttpURLConnection connection = (HttpURLConnection) website.openConnection(); + + // If HTTPS and SSL validation is disabled, trust everything. + if ("https".equals(website.getProtocol())) { + try { + if (disableSSLValidation) { + log.debug("SSL Validation is disabled for {}, installing accept everything TrustManager", remoteUrl); + installAcceptEverythingTrustManager(connection); + } else if (secret != null && secret.getCaCertPem() != null && secret.getCaCertPem().trim().length() > 0) { + log.debug("Secret for {} contains a CA Cert, installing certificate into TrustManager", remoteUrl); + installCustomCaCertTrustManager(secret.getCaCertPem(), connection); + } + } catch (Exception e) { + log.error("Caught exception while preparing TrustManager for connecting {}: {}", remoteUrl, e.getMessage()); + throw new IOException("SSL Connection with " + remoteUrl + " failed during preparation", e); + } + } + + if (secret != null) { + // If Basic authentication required, set request property. + if (secret.getUsername() != null && secret.getPassword() != null) { + log.debug("Secret for {} contains username/password, assuming Authorization Basic", remoteUrl); + // Building a base64 string. + String encoded = Base64.getEncoder() + .encodeToString((secret.getUsername() + ":" + secret.getPassword()).getBytes(StandardCharsets.UTF_8)); + connection.setRequestProperty("Authorization", "Basic " + encoded); + } + + // If Token authentication required, set request property. + if (secret.getToken() != null) { + if (secret.getTokenHeader() != null && secret.getTokenHeader().trim().length() > 0) { + log.debug("Secret for {} contains token and token header, adding them as request header", remoteUrl); + connection.setRequestProperty(secret.getTokenHeader().trim(), secret.getToken()); + } else { + log.debug("Secret for {} contains token only, assuming Authorization Bearer", remoteUrl); + connection.setRequestProperty("Authorization", "Bearer " + secret.getToken()); + } + } + } + + return connection; + } + + /** + * Install a TrustManager that accept every verification of host name. + */ + private static void installAcceptEverythingTrustManager(HttpURLConnection connection) throws Exception { + // Create a trust manager that does not validate certificate chains + TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // No check to do here as we must accept everything. + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // No check to do here as we must accept everything. + } + } }; + + // Install the all-trusting trust manager. + final SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + ((HttpsURLConnection) connection).setSSLSocketFactory(sslContext.getSocketFactory()); + + // Create and install all-trusting host name verifier. + HostnameVerifier allHostsValid = (hostname, session) -> true; + ((HttpsURLConnection) connection).setHostnameVerifier(allHostsValid); + } + + /** + * Install a TrustManager that validates the CA certificate. + */ + private static void installCustomCaCertTrustManager(String caCertPem, HttpURLConnection connection) + throws Exception { + // First compute a stripped PEM certificate and decode it from base64. + String strippedPem = caCertPem.replace(BEGIN_CERTIFICATE, "").replace(END_CERTIFICATE, ""); + InputStream is = new ByteArrayInputStream(org.apache.commons.codec.binary.Base64.decodeBase64(strippedPem)); + + // Generate a new x509 certificate from the stripped decoded pem. + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate caCert = (X509Certificate) cf.generateCertificate(is); + + // Set a new certificate into keystore. + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null); // You don't need the KeyStore instance to come from a file. + ks.setCertificateEntry("caCert", caCert); + + tmf.init(ks); + + // Install the new TrustManager. + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + ((HttpsURLConnection) connection).setSSLSocketFactory(sslContext.getSocketFactory()); + } + + + /** Simple wrapper around a downloaded local file and the header we received during the download. */ + public static class FileAndHeaders { + private File localFile; + private Map> responseHeaders; + + public FileAndHeaders(File localFile, Map> responseHeaders) { + this.localFile = localFile; + this.responseHeaders = responseHeaders; + } + + public File getLocalFile() { + return localFile; + } + + public Map> getResponseHeaders() { + return responseHeaders; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExportException.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExportException.java new file mode 100644 index 000000000..41e328cf1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExportException.java @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util; + +/** + * Typed exception for reporting problem during a mock repository export. + * @author laurent + */ +public class MockRepositoryExportException extends Exception { + + public MockRepositoryExportException(String message) { + super(message); + } + + public MockRepositoryExportException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExporter.java new file mode 100644 index 000000000..b2ddf2187 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExporter.java @@ -0,0 +1,59 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; + +import java.util.List; + +/** + * Interface definition for exporting Microcks Service and Message definitions into a specific format. You should call + * the following methods in order : + *
    + *
  • addServiceDefinition() one or many time (some implementation may support multiple services)
  • + *
  • addMessageDefinitions() one or many time (some implementation may support multiple services)
  • + *
  • exportAsString() to get the final export as a String
  • + *
+ * @author laurent + */ +public interface MockRepositoryExporter { + + /** + * Add a Service definition to the exporter. + * @param service The service to add + * @throws MockRepositoryExportException if something goes wrong during export + */ + void addServiceDefinition(Service service) throws MockRepositoryExportException; + + /** + * Add a list of Message definitions to the exporter. + * @param service The service to add messages for + * @param operation The service operation/actions to add messages for + * @param messages The list of messages to add + * @throws MockRepositoryExportException if something goes wrong during export + */ + void addMessageDefinitions(Service service, Operation operation, List messages) + throws MockRepositoryExportException; + + /** + * Export the whole repository as a String (in the format of the exporter). + * @return The repository export as a String + * @throws MockRepositoryExportException if something goes wrong during export + */ + String exportAsString() throws MockRepositoryExportException; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExporterFactory.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExporterFactory.java new file mode 100644 index 000000000..368223c28 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryExporterFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.util.metadata.ExamplesExporter; +import io.github.microcks.util.openapi.OpenAPIOverlayExporter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory for building/retrieving mock repository exporter implementations. + * @author laurent + */ +public class MockRepositoryExporterFactory { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(MockRepositoryExporterFactory.class); + + /** A constant representing the APIExamples export format option. */ + public static final String API_EXAMPLES_FORMAT = "APIExamples"; + /** A constant representing the OpenAPI Overlay export format option. */ + public static final String OPENAPI_OVERLAY_FORMAT = "OAS Overlay"; + + private MockRepositoryExporterFactory() { + // Private constructor to hide the implicit one as it's a utility class. + } + + /** + * Create the right MockRepositoryExporter implementation depending on requested format. + * @param format The requested export format. + * @return An instance of MockRepositoryExporter implementation + */ + public static MockRepositoryExporter getMockRepositoryExporter(String format) { + if (API_EXAMPLES_FORMAT.equalsIgnoreCase(format)) { + return new ExamplesExporter(); + } else if (OPENAPI_OVERLAY_FORMAT.equalsIgnoreCase(format)) { + return new OpenAPIOverlayExporter(); + } else { + log.info("The request format {} is unknown. Returning an APIExamples exporter as the default.", format); + return new ExamplesExporter(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImportException.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImportException.java new file mode 100644 index 000000000..cf9b0a4e8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImportException.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +/** + * Typed exception for reporting problem during a mock repository import. + * @author laurent + */ +public class MockRepositoryImportException extends Exception { + + public MockRepositoryImportException(String message) { + super(message); + } + + public MockRepositoryImportException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImporter.java new file mode 100644 index 000000000..e298e353f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImporter.java @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.Operation; + +import java.util.List; + +/** + * Interface definition for loading Microcks domain objects definitions from a source repository. Source repositories + * may have different forms : SoapUI project file, Postman collection v2 file, custom XML format, directory structure + * with naming conventions, and so on ...
+ * After usage of the companion factory, user should call the following methods in order : + *
    + *
  • getServiceDefinitions(),
  • + *
  • getResourceDefinitions(),
  • + *
  • getMessageDefinitions(),
  • + *
+ * in order to incrementally populate the domain objects. + * @author laurent + */ +public interface MockRepositoryImporter { + + /** + * Just after repository importer initialization, this method should return the definitions of Service domain objects + * as found into the target imported repository. + * @return The list of found Services into repository. May be empty. + * @throws MockRepositoryImportException if something goes wrong during import + */ + List getServiceDefinitions() throws MockRepositoryImportException; + + /** + * Once Service definition has been initialized, attahed resources may be identified and retrieved. + * @param service The service to get resources for + * @return The list of found Resources into repository. May be empty. + * @throws MockRepositoryImportException if something goes wrong during import + */ + List getResourceDefinitions(Service service) throws MockRepositoryImportException; + + /** + * For any operation of a service a map of associated Request and Response should be retrieve for full definition of + * a Service. + * @param service The service to get messages for + * @param operation The service operation/actions to get messages for + * @return A list of Exchange messages + * @throws MockRepositoryImportException if something goes wrong during import + */ + List getMessageDefinitions(Service service, Operation operation) throws MockRepositoryImportException; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImporterFactory.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImporterFactory.java new file mode 100644 index 000000000..b79dd11a8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/MockRepositoryImporterFactory.java @@ -0,0 +1,154 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.util.asyncapi.AsyncAPI3Importer; +import io.github.microcks.util.asyncapi.AsyncAPIImporter; +import io.github.microcks.util.graphql.GraphQLImporter; +import io.github.microcks.util.grpc.ProtobufImporter; +import io.github.microcks.util.har.HARImporter; +import io.github.microcks.util.metadata.ExamplesImporter; +import io.github.microcks.util.metadata.MetadataImporter; +import io.github.microcks.util.openapi.OpenAPIImporter; +import io.github.microcks.util.postman.PostmanCollectionImporter; +import io.github.microcks.util.postman.PostmanWorkspaceCollectionImporter; +import io.github.microcks.util.soapui.SoapUIProjectImporter; +import io.github.microcks.util.openapi.SwaggerImporter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** + * Factory for building/retrieving mock repository importer implementations. For now, it implements a very simple + * algorithm : if repository is a JSON file (guess on first lines content), it assume repository it implemented as a + * Postman collection and then uses PostmanCollectionImporter; otherwise it uses SoapUIProjectImporter. + * @author laurent + */ +public class MockRepositoryImporterFactory { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(MockRepositoryImporterFactory.class); + + /** A RegExp for detecting a line containing the openapi: 3 pragma. */ + public static final String OPENAPI_3_REGEXP = ".*['\\\"]?openapi['\\\"]?\\s*:\\s*['\\\"]?[3\\.].*"; + + /** A RegExp for detecting a line containing the asyncapi: 2 pragma. */ + public static final String ASYNCAPI_2_REGEXP = ".*['\\\"]?asyncapi['\\\"]?\\s*:\\s*['\\\"]?[2\\.].*"; + + /** A RegExp for detecting a line containing the asyncapi: 3 pragma. */ + public static final String ASYNCAPI_3_REGEXP = ".*['\\\"]?asyncapi['\\\"]?\\s*:\\s*['\\\"]?[3\\.].*"; + + /** A RegExp for detecting a line containing the swagger pragma. */ + public static final String SWAGGER_REGEXP = ".*['\\\"]?swagger['\\\"]?\\s*:\\s*.*"; + + private MockRepositoryImporterFactory() { + // Private constructor to hide the implicit one as it's a utility class. + } + + /** + * Create the right MockRepositoryImporter implementation depending on repository type. + * @param mockRepository The file representing the repository type + * @param referenceResolver The Resolver to be used during import (may be null). + * @return An instance of MockRepositoryImporter implementation + * @throws IOException in case of file access + */ + public static MockRepositoryImporter getMockRepositoryImporter(File mockRepository, + ReferenceResolver referenceResolver) throws IOException { + MockRepositoryImporter importer = null; + + // Analyse first lines of file content to guess repository type. + String line = null; + try (BufferedReader reader = Files.newBufferedReader(mockRepository.toPath(), StandardCharsets.UTF_8)) { + while ((line = reader.readLine()) != null && importer == null) { + line = line.trim(); + // Check with basic Postman formats.. + importer = checkPostmanImporters(line, mockRepository); + // Then try OpenAPI related ones... + if (importer == null) { + importer = checkOpenAPIImporters(line, mockRepository, referenceResolver); + } + // Then try any other else. + if (importer == null) { + importer = checkOtherImporters(line, mockRepository, referenceResolver); + } + } + } + + // Otherwise, default to HAR project importer implementation because it has no proper identity marker. + if (importer == null) { + log.info("Have not found any explicit marker so applying the default HTTP Archive (HAR) importer..."); + importer = new HARImporter(mockRepository.getPath()); + } + + return importer; + } + + private static MockRepositoryImporter checkPostmanImporters(String line, File mockRepository) throws IOException { + if (line.startsWith("\"_postman_id\":")) { + log.info("Found a _postman_id in file so assuming it's a Postman Collection to import"); + return new PostmanCollectionImporter(mockRepository.getPath()); + } else if (line.startsWith("\"collection\":") || line.startsWith("{\"collection\":")) { + log.info("Found a collection in file so assuming it's a Postman Workspace Collection to import"); + return new PostmanWorkspaceCollectionImporter(mockRepository.getPath()); + } + return null; + } + + private static MockRepositoryImporter checkOpenAPIImporters(String line, File mockRepository, + ReferenceResolver referenceResolver) throws IOException { + if (line.matches(OPENAPI_3_REGEXP)) { + log.info("Found an openapi: 3 pragma in file so assuming it's an OpenAPI spec to import"); + return new OpenAPIImporter(mockRepository.getPath(), referenceResolver); + } else if (line.matches(SWAGGER_REGEXP)) { + log.info("Found an swagger: pragma in file so assuming it's a Swagger spec to import"); + return new SwaggerImporter(mockRepository.getPath(), referenceResolver); + } + return null; + } + + private static MockRepositoryImporter checkOtherImporters(String line, File mockRepository, + ReferenceResolver referenceResolver) throws IOException { + if (line.startsWith(" codePointLimit) { + codePointLimit = codePointLimitCandidate; + } + } + + /** @return A Jackson ObjectMapper configured for JSON parsing with common settings. */ + public static ObjectMapper getJsonObjectMapper() { + return new ObjectMapper(); + } + + /** @return A Jackson ObjectMapper configured for YAML parsing with common settings. */ + public static ObjectMapper getYamlObjectMapper() { + LoaderOptions options = new LoaderOptions(); + options.setCodePointLimit(codePointLimit); + YAMLFactory yamlFactory = YAMLFactory.builder().loaderOptions(options).build(); + return new ObjectMapper(yamlFactory); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ParameterConstraintUtil.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ParameterConstraintUtil.java new file mode 100644 index 000000000..819380006 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ParameterConstraintUtil.java @@ -0,0 +1,60 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.ParameterLocation; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.regex.Pattern; + +/** + * Utility class for holding various case around ParameterConstraints. + * @author laurent + */ +public class ParameterConstraintUtil { + + /** Private constructor to hide the implicit public one and prevent instantiation. */ + private ParameterConstraintUtil() { + // Hidden constructor + } + + /** + * Validate that a parameter constraint it respected or violated. Return a message if violated. + * @param request HttpServlet request holding parameters to validate + * @param constraint Constraint to apply to one request parameter. + * @return A string representing constraint violation if any. null otherwise. + */ + public static String validateConstraint(HttpServletRequest request, ParameterConstraint constraint) { + String value = null; + if (ParameterLocation.header == constraint.getIn()) { + value = request.getHeader(constraint.getName()); + } else if (ParameterLocation.query == constraint.getIn()) { + value = request.getParameter(constraint.getName()); + } + + if (value != null) { + if (constraint.getMustMatchRegexp() != null && !Pattern.matches(constraint.getMustMatchRegexp(), value)) { + return "Parameter " + constraint.getName() + " should match " + constraint.getMustMatchRegexp(); + } + } else { + if (constraint.isRequired()) { + return "Parameter " + constraint.getName() + " is required"; + } + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ReferenceResolver.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ReferenceResolver.java new file mode 100644 index 000000000..8db90cea4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ReferenceResolver.java @@ -0,0 +1,187 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Secret; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +/** + * Helper object that can be used to fully resolved references that are placed within a specification or a schema (think + * of the $ref within OpenAPI or AsyncAPI documents). A resolver is built with a base repository URL (that should point + * to a "folder") and some security parameters on how to access this repository. It then takes care of retrieving + * reference content using their relative path from base repository URL. + * @author laurent + */ +public class ReferenceResolver { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ReferenceResolver.class); + + private String baseRepositoryUrl; + private final Secret repositorySecret; + private final boolean disableSSLValidation; + private boolean cleanResolvedFiles = true; + + private RelativeReferenceURLBuilder urlBuilder; + + private Map resolvedReferences = new HashMap<>(); + private Map relativeResolvedReferences = new HashMap<>(); + + /** + * Build a new reference resolver. + * @param baseRepositoryUrl The root folder representing a remote repository we want to resolved references to + * @param repositorySecret An optional Secret containing connection credentials to the repository + * @param disableSSLValidation Whether to disable or enable the SSL trusting of certificates + */ + public ReferenceResolver(String baseRepositoryUrl, Secret repositorySecret, boolean disableSSLValidation) { + this.setBaseRepositoryUrl(baseRepositoryUrl); + this.repositorySecret = repositorySecret; + this.disableSSLValidation = disableSSLValidation; + this.urlBuilder = new SimpleReferenceURLBuilder(); + } + + /** + * Build a new reference resolver. + * @param baseRepositoryUrl The root folder representing a remote repository we want to resolved references to + * @param repositorySecret An optional Secret containing connection credentials to the repository + * @param disableSSLValidation Whether to disable or enable the SSL trusting of certificates + */ + public ReferenceResolver(String baseRepositoryUrl, Secret repositorySecret, boolean disableSSLValidation, + RelativeReferenceURLBuilder urlBuilder) { + this.setBaseRepositoryUrl(baseRepositoryUrl); + this.repositorySecret = repositorySecret; + this.disableSSLValidation = disableSSLValidation; + this.urlBuilder = urlBuilder; + } + + /** @return the current base repository url. */ + public String getBaseRepositoryUrl() { + return baseRepositoryUrl; + } + + /** + * Update the base repository url to use as root for relative reference resolution. + * @param baseRepositoryUrl The new base repository url. + */ + public void setBaseRepositoryUrl(String baseRepositoryUrl) { + // Remove trailing / to ease things later. + if (baseRepositoryUrl.endsWith("/")) { + this.baseRepositoryUrl = baseRepositoryUrl.substring(0, baseRepositoryUrl.length() - 1); + } else { + this.baseRepositoryUrl = baseRepositoryUrl; + } + } + + /** + * Check if resolved files should be cleaned up after usage. Default behavior is true. + * @return The current value for this flag. + */ + public boolean isCleanResolvedFiles() { + return cleanResolvedFiles; + } + + /** + * Set if resolved files should be cleaned up after usage. If set to false, resolvedReferences map will be clear but + * files will not be deleted from the local filesystem. + * @param cleanResolvedFiles The new value for this flag. + */ + public void setCleanResolvedFiles(boolean cleanResolvedFiles) { + this.cleanResolvedFiles = cleanResolvedFiles; + } + + /** + * Get the full URL corresponding to a reference relative path + * @param referenceRelativePath The reference relative path to resolve + * @return The absolute URL if a relative path, the orginal path/URL otherwise. + */ + public String getReferenceURL(String referenceRelativePath) { + String remoteUrl = referenceRelativePath; + if (!referenceRelativePath.startsWith("http")) { + remoteUrl = urlBuilder.buildRemoteURL(this.baseRepositoryUrl, referenceRelativePath); + } + return remoteUrl; + } + + /** + * Retrieve a reference content from remote repository using its relative path? + * @param relativePath The relative path of the reference to retrieve + * @param encoding The encoding to use for building a string representation of content. + * @return A string representation of reference content. + * @throws IOException if access to remote reference fails (not found or connection issues) + */ + public String getReferenceContent(String relativePath, Charset encoding) throws IOException { + // Check the file first. + String remoteUrl = getReferenceURL(relativePath); + File referenceFile = resolvedReferences.get(remoteUrl); + if (referenceFile == null) { + log.info("Downloading a reference file at {}", remoteUrl); + + if (remoteUrl.startsWith("http")) { + // We have a remote URL, let's download it. + referenceFile = HTTPDownloader.handleHTTPDownloadToFile(remoteUrl, repositorySecret, disableSSLValidation); + } else { + // We have a local file, let's just use it. + if (remoteUrl.startsWith("file://")) { + remoteUrl = remoteUrl.substring("file://".length()); + } + log.debug("Reading local file {}", remoteUrl); + referenceFile = new File(remoteUrl); + } + // Store this relative reference into the cache. + resolvedReferences.put(remoteUrl, referenceFile); + } + // Keep track on how we resolved this relativePath. + relativeResolvedReferences.put(relativePath, referenceFile); + + return Files.readString(referenceFile.toPath(), encoding); + } + + /** + * Get resolved references map. + * @return The map of resolved references with keys being absolute paths. + */ + public Map getResolvedReferences() { + return resolvedReferences; + } + + /** + * Get relative resolved references map. + * @return The map of resolved references with keys being relative paths. + */ + public Map getRelativeResolvedReferences() { + return relativeResolvedReferences; + } + + /** Cleans up already resolved references. */ + public void cleanResolvedReferences() { + if (cleanResolvedFiles) { + for (File referenceFile : resolvedReferences.values()) { + referenceFile.delete(); + } + } + resolvedReferences.clear(); + relativeResolvedReferences.clear(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/RelativeReferenceURLBuilder.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/RelativeReferenceURLBuilder.java new file mode 100644 index 000000000..a4d7492a9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/RelativeReferenceURLBuilder.java @@ -0,0 +1,45 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import java.util.List; +import java.util.Map; + +/** + * Builds remote URL for relative references found into a base file located at specified repository URL. This will allow + * different implementations depending on where and how the file path is encoded into base repository URL. + * @author laurent + */ +public interface RelativeReferenceURLBuilder { + + /** + * Returns the name of the file denoted by baseRepositoryUrl. Addiotional headers (like download + * headers) can be optionally provided. + * @param baseRepositoryURL The URL in repository of the file to get name for + * @param headers The optional additional file properties or headers + * @return The name of this file + */ + String getFileName(String baseRepositoryURL, Map> headers); + + /** + * Build a suitable download URL for a reference found into a base file. + * @param baseRepositoryURL The URL in repository of the file that serves as base file + * @param referencePath A path like my-file.yaml or ../my-file.yaml to get reference + * from base + * @return The URL to download reference from + */ + String buildRemoteURL(String baseRepositoryURL, String referencePath); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/RelativeReferenceURLBuilderFactory.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/RelativeReferenceURLBuilderFactory.java new file mode 100644 index 000000000..1e4aab35f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/RelativeReferenceURLBuilderFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * Factory for building/retrieving a relative reference url builder. + * @author laurent + */ +public class RelativeReferenceURLBuilderFactory { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(RelativeReferenceURLBuilderFactory.class); + + /** + * Create the right RelativeReferenceURLBuilder implementation depending on the base file properties. + * @param baseFileProperties A map of properties (most of the time headers-like structure) + * @return A RelativeReferenceURLBuilder that could be used to build URL for relative references in this base file. + */ + public static RelativeReferenceURLBuilder getRelativeReferenceURLBuilder( + Map> baseFileProperties) { + if (baseFileProperties != null) { + if (baseFileProperties.containsKey(GitLabReferenceURLBuilder.GITLAB_FILE_NAME_HEADER) + || baseFileProperties.containsKey(GitLabReferenceURLBuilder.GITLAB_FILE_NAME_HEADER.toLowerCase())) { + log.debug("Found a GitLab File specific header, returning a GitLabReferenceURLBuilder"); + return new GitLabReferenceURLBuilder(); + } + } + log.debug("Returning a SimpleReferenceURLBuilder"); + return new SimpleReferenceURLBuilder(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ResourceUtil.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ResourceUtil.java new file mode 100644 index 000000000..4f9761949 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ResourceUtil.java @@ -0,0 +1,133 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Service; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.util.stream.Stream; + +/** + * Utility class to retrieve classpath resource and replace content in template resources (like OpenAPI, AsyncAPI spec + * templates). + * @author laurent + */ +public class ResourceUtil { + + /** + * A simple logger for diagnostic messages. + */ + private static final Logger log = LoggerFactory.getLogger(ResourceUtil.class); + private static final String SERVICE_PLACEHOLDER = "{service}"; + private static final String VERSION_PLACEHOLDER = "{version}"; + private static final String RESOURCE_PLACEHOLDER = "{resource}"; + private static final String SCHEMA_PLACEHOLDER = "{resourceSchema}"; + private static final String REFERENCE_PLACEHOLDER = "{reference}"; + + private ResourceUtil() { + // Private constructor to hide implicit public one. + } + + /** + * Load a resource from classspath using its path. + * + * @param resourcePath The path of resource to load. + * @return The resource input stream + * @throws IOException if resource cannot be found or opened + */ + public static InputStream getClasspathResource(String resourcePath) throws IOException { + Resource template = new ClassPathResource(resourcePath); + return template.getInputStream(); + } + + /** + * Given a resource stream holding placeholder patterns (aka {placeholder}), replace the patterns with actual value + * coming from Service, an API resource name, an API schema and a reference payload. + * + * @param stream The stream to scan for patterns and substitute in. + * @param service The Service corresponding to API + * @param resource The API resource + * @param referenceSchema An optional reference API schema + * @param referencePayload An optional reference resource payload + * @return The stream content with placeholders replaced by actual values. + */ + public static String replaceTemplatesInSpecStream(InputStream stream, Service service, String resource, + JsonNode referenceSchema, String referencePayload) { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + StringWriter writer = new StringWriter(); + + try (Stream lines = reader.lines()) { + lines.map(line -> replaceInLine(line, service, resource, referenceSchema, referencePayload)) + .forEach(line -> writer.write(line + "\n")); + } + return writer.toString(); + } + + /** + * Do the replacement within a given stream line. + */ + private static String replaceInLine(String line, Service service, String resource, JsonNode referenceSchema, + String referencePayload) { + line = line.replace(SERVICE_PLACEHOLDER, service.getName()); + line = line.replace(VERSION_PLACEHOLDER, service.getVersion()); + line = line.replace(RESOURCE_PLACEHOLDER, resource); + if (line.contains(SCHEMA_PLACEHOLDER)) { + if (referenceSchema != null) { + // Serialize reference schema and replace it. + try { + ObjectMapper mapper = new ObjectMapper( + new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES).disable(YAMLGenerator.Feature.INDENT_ARRAYS)); + String schema = mapper.writeValueAsString(referenceSchema); + // find the indentation level of the schema placeholder + int indentation = line.indexOf(SCHEMA_PLACEHOLDER); + // add the indentation to the schema + schema = schema.replace("\n", "\n" + line.substring(0, indentation)); + // remove the last indentation and the last newline + schema = schema.substring(0, schema.length() - indentation - 1); + line = line.replace(SCHEMA_PLACEHOLDER, schema); + } catch (Exception e) { + log.warn("Exception while replacing resource schema", e); + } + } else { + // Stick to the default: an empty type definition. + line = line.replace(SCHEMA_PLACEHOLDER, ""); + } + } + if (line.contains(REFERENCE_PLACEHOLDER)) { + if (referencePayload != null) { + // Inline Json and escape quotes. + line = line.replace(REFERENCE_PLACEHOLDER, referencePayload.replace("\n", "")); + } else { + // Stick to the default: an empty reference. + line = line.replace(REFERENCE_PLACEHOLDER, ""); + } + } + return line; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/SafeLogger.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/SafeLogger.java new file mode 100644 index 000000000..18d330a56 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/SafeLogger.java @@ -0,0 +1,248 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +/** + * An encapsulation of a SL4J logger that provides safe logging methods that encodes user-controller data before + * actually logging (see Sonar rule javasecurity:S5145). This Logger does not strictly implement the SLF4J Logger + * interface, but provides a subset of its methods. + * @author laurent + */ +public class SafeLogger { + + private final Logger log; + + private SafeLogger(Logger log) { + this.log = log; + } + + /** + * Return a logger named corresponding to the class passed as parameter, using the statically bound ILoggerFactory + * instance. + * @param clazz the returned logger will be named after clazz + * @return logger + */ + public static SafeLogger getLogger(Class clazz) { + return new SafeLogger(LoggerFactory.getLogger(clazz)); + } + + /** + * Is the logger instance enabled for the DEBUG level? + * @return True if this Logger is enabled for the DEBUG level, false otherwise. + */ + public boolean isDebugEnabled() { + return log.isDebugEnabled(); + } + + /** + * Log a message at the DEBUG level. + * @param message the message string to be logged + */ + public void debug(String message) { + if (log.isDebugEnabled()) { + log.debug(encode(message)); + } + } + + /** + * Log a message at the DEBUG level according to the specified format and argument. This form avoids superfluous + * object creation when the logger is disabled for the DEBUG level. + * @param format the format string + * @param arg the argument + */ + public void debug(String format, Object arg) { + if (log.isDebugEnabled()) { + log.debug(encode(format), encode(arg)); + } + } + + /** + * Log a message at the DEBUG level according to the specified format and arguments. This form avoids superfluous + * object creation when the logger is disabled for the DEBUG level. + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void debug(String format, Object arg1, Object arg2) { + if (log.isDebugEnabled()) { + log.debug(encode(format), encode(arg1), encode(arg2)); + } + } + + /** + * Log a message at the DEBUG level according to the specified format and arguments. This form avoids superfluous + * string concatenation when the logger is disabled for the DEBUG level. However, this variant incurs the hidden (and + * relatively small) cost of creating an Object[] before invoking the method, even if this logger is disabled for + * DEBUG. The variants taking one and two arguments exist solely in order to avoid this hidden cost. + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void debug(String format, Object... arguments) { + if (log.isDebugEnabled()) { + log.debug(encode(format), encode(arguments)); + } + } + + /** + * Is the logger instance enabled for the INFO level? + * @return True if this Logger is enabled for the INFO level, false otherwise. + */ + public boolean isInfoEnabled() { + return log.isInfoEnabled(); + } + + /** + * Log a message at the INFO level. + * @param message the message string to be logged + */ + public void info(String message) { + if (log.isInfoEnabled()) { + log.info(encode(message)); + } + } + + /** + * Log a message at the INFO level according to the specified format and argument. This form avoids superfluous + * object creation when the logger is disabled for the INFO level. + * @param format the format string + * @param arg the argument + */ + public void info(String format, Object arg) { + if (log.isInfoEnabled()) { + log.info(encode(format), encode(arg)); + } + } + + /** + * Log a message at the INFO level according to the specified format and arguments. This form avoids superfluous + * object creation when the logger is disabled for the INFO level. + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void info(String format, Object arg1, Object arg2) { + if (log.isInfoEnabled()) { + log.info(encode(format), encode(arg1), encode(arg2)); + } + } + + /** + * Log a message at the INFO level according to the specified format and arguments. This form avoids superfluous + * string concatenation when the logger is disabled for the INFO level. However, this variant incurs the hidden (and + * relatively small) cost of creating an Object[] before invoking the method, even if this logger is disabled for + * INFO. The variants taking one and two arguments exist solely in order to avoid this hidden cost. + * @param format the format string + * @param args a list of 3 or more arguments + */ + public void info(String format, Object... args) { + if (log.isInfoEnabled()) { + log.info(encode(format), encode(args)); + } + } + + /** + * Is the logger instance enabled for the WARN level? + * @return True if this Logger is enabled for the WARN level, false otherwise. + */ + public boolean isWarnEnabled() { + return log.isWarnEnabled(); + } + + /** + * Log an exception (throwable) at the WARN level with an accompanying message. + * @param message the message string to be logged + * @param t the exception (throwable) to log + */ + public void warn(String message, Throwable t) { + if (log.isWarnEnabled()) { + log.warn(encode(message), t); + } + } + + /** + * Is the logger instance enabled for the ERROR level? + * @return True if this Logger is enabled for the ERROR level, false otherwise. + */ + public boolean isErrorEnabled() { + return log.isErrorEnabled(); + } + + /** + * Log a message at the ERROR level. + * @param message the message string to be logged + */ + public void error(String message) { + if (log.isErrorEnabled()) { + log.error(encode(message)); + } + } + + /** + * Log an exception (throwable) at the ERROR level with an accompanying message. + * @param message the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void error(String message, Throwable t) { + if (log.isErrorEnabled()) { + log.error(encode(message), t); + } + } + + /** + * Log a message at the ERROR level according to the specified format and arguments. This form avoids superfluous + * object creation when the logger is disabled for the ERROR level. + * @param format the format string + * @param arg the argument + */ + public void error(String format, Object arg) { + if (log.isErrorEnabled()) { + log.error(encode(format), encode(arg)); + } + } + + /** + * Log a message at the ERROR level according to the specified format and arguments. This form avoids superfluous + * object creation when the logger is disabled for the ERROR level. + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void error(String format, Object arg1, Object arg2) { + if (log.isErrorEnabled()) { + log.error(encode(format), encode(arg1), encode(arg2)); + } + } + + private String encode(String message) { + return message.replaceAll("[\n\r]", "_"); + } + + private Object encode(Object argument) { + if (argument instanceof String stringArg) { + return encode(stringArg); + } + return argument; + } + + private Object[] encode(Object... arguments) { + return Arrays.stream(arguments).sequential().map(this::encode).toArray(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/SimpleReferenceURLBuilder.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/SimpleReferenceURLBuilder.java new file mode 100644 index 000000000..0bcaaa9ac --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/SimpleReferenceURLBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import java.util.List; +import java.util.Map; + +/** + * A simple implementation of RelativeReferenceURLBuilder that considers that base file name and path are + * not encoded and are located at the end of remote URL of repository. + * @author laurent + */ +public class SimpleReferenceURLBuilder implements RelativeReferenceURLBuilder { + + @Override + public String getFileName(String baseRepositoryURL, Map> headers) { + return baseRepositoryURL.substring(baseRepositoryURL.lastIndexOf("/") + 1); + } + + @Override + public String buildRemoteURL(String baseRepositoryURL, String referencePath) { + // Rebuild a downloadable URL to retrieve file. + String remoteUrl = baseRepositoryURL.substring(0, baseRepositoryURL.lastIndexOf("/")); + String pathToAppend = referencePath; + while (pathToAppend.startsWith("../")) { + remoteUrl = remoteUrl.substring(0, remoteUrl.lastIndexOf("/")); + pathToAppend = pathToAppend.substring(3); + } + if (pathToAppend.startsWith("./")) { + pathToAppend = pathToAppend.substring(2); + } + if (pathToAppend.startsWith("/")) { + pathToAppend = pathToAppend.substring(1); + } + remoteUrl += "/" + pathToAppend; + return remoteUrl; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/URIBuilder.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/URIBuilder.java new file mode 100644 index 000000000..61ab6946b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/URIBuilder.java @@ -0,0 +1,137 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Parameter; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.springframework.web.util.UriUtils; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * Helper class for building URIs from various objects. + * @author laurent + */ +public class URIBuilder { + + private URIBuilder() { + // Hide default no argument constructor as it's a utility class. + } + + /** + * Build a URI from a URI pattern (using {} or /: for marked variable parts) and using other query parameters + * @param pattern The URI pattern to use + * @param parameters The set of parameters (whether template or query based) + * @return The instanciated URI from template and parameters + */ + public static String buildURIFromPattern(String pattern, List parameters) { + if (parameters != null) { + // Browse parameters and choose between template or query one. + for (Parameter parameter : parameters) { + String wadlTemplate = "{" + parameter.getName() + "}"; + String swaggerTemplate = "/:" + parameter.getName(); + if (pattern.contains(wadlTemplate)) { + // It's a template parameter. + pattern = pattern.replace(wadlTemplate, encodePath(parameter.getValue())); + } else if (pattern.contains(swaggerTemplate)) { + // It's a template parameter. + pattern = pattern.replace(":" + parameter.getName(), encodePath(parameter.getValue())); + } else { + // It's a query parameter, ensure we have started delimiting them. + if (!pattern.contains("?")) { + pattern += "?"; + } + if (pattern.contains("=")) { + pattern += "&"; + } + pattern += parameter.getName() + "=" + encodeValue(parameter.getValue()); + } + } + } + return pattern; + } + + /** + * Build a URI from a URI pattern (using {} or /: for marked variable parts) and using other query parameters + * @param pattern The URI pattern to use + * @param parameters The map of parameters K/V (whether template or query based) + * @return The instanciated URI from template and parameters + */ + public static String buildURIFromPattern(String pattern, Map parameters) { + if (parameters != null) { + Multimap multimap = parameters.entrySet().stream().collect(ArrayListMultimap::create, + (m, e) -> m.put(e.getKey(), e.getValue()), Multimap::putAll); + return buildURIFromPattern(pattern, multimap); + } + return pattern; + } + + /** + * Build a URI from a URI pattern (using {} or /: for marked variable parts) and using other query parameters + * @param pattern The URI pattern to use + * @param parameters The Multimap of parameters K/V (whether template or query based) + * @return The instanciated URI from template and parameters + */ + public static String buildURIFromPattern(String pattern, Multimap parameters) { + if (parameters != null) { + // Browse parameters and choose from template of query one. + for (String parameterName : parameters.keySet()) { + String wadltemplate = "{" + parameterName + "}"; + String swaggerTemplate = "/:" + parameterName; + + for (String parameterValue : parameters.get(parameterName)) { + + if (pattern.contains(wadltemplate)) { + // It's a template parameter. + pattern = pattern.replace(wadltemplate, encodePath(parameterValue)); + } else if (pattern.contains(swaggerTemplate)) { + // It's a template parameter. + pattern = pattern.replace(":" + parameterName, encodePath(parameterValue)); + } else { + // It's a query parameter, ensure we have started delimiting them. + if (!pattern.contains("?")) { + pattern += "?"; + } + if (pattern.contains("=")) { + pattern += "&"; + } + pattern += parameterName + "=" + encodeValue(parameterValue); + } + } + } + } + return pattern; + } + + /** Utility method for getting URL encoding of query parameter. */ + private static String encodeValue(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + /** + * Utility method for getting URL encoding of path parameter. We cannot use JDK method that only deal with query + * parameters value. See https://stackoverflow.com/a/2678602 and https://www.baeldung.com/java-url-encoding-decoding. + */ + private static String encodePath(String path) { + return UriUtils.encodePath(path, StandardCharsets.UTF_8); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/UsernamePasswordProxyAuthenticator.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/UsernamePasswordProxyAuthenticator.java new file mode 100644 index 000000000..9333ca882 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/UsernamePasswordProxyAuthenticator.java @@ -0,0 +1,57 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.config.ProxySettings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; + +/** + * A simple Authenticator for basic network authentication handling. + * @author laurent + */ +public class UsernamePasswordProxyAuthenticator extends Authenticator { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(UsernamePasswordProxyAuthenticator.class); + + private final ProxySettings settings; + + private final PasswordAuthentication authentication; + + /** + * Constructor. + * @param settings The proxy settings for network to reach out. + */ + public UsernamePasswordProxyAuthenticator(ProxySettings settings) { + this.settings = settings; + this.authentication = new PasswordAuthentication(settings.getUsername(), settings.getPassword().toCharArray()); + } + + protected PasswordAuthentication getPasswordAuthentication() { + // Return authentication information only if requester is the identified proxy. + log.debug("Handling proxy authentication for {}", getRequestingHost()); + if (getRequestorType() == RequestorType.PROXY) { + if (getRequestingHost().equalsIgnoreCase(settings.getHost()) && getRequestingPort() == settings.getPort()) { + return authentication; + } + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/WritableNamespaceContext.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/WritableNamespaceContext.java new file mode 100644 index 000000000..442fc3511 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/WritableNamespaceContext.java @@ -0,0 +1,52 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import javax.xml.namespace.NamespaceContext; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Does not implement reverse operations for now, but not necessary for XPath processing that is our main purpose. Have + * to check Guava + * (http://guava-libraries.googlecode.com/svn/tags/release09/javadoc/com/google/common/collect/BiMap.html) for + * Bidirectional map when looking for this ? + * @author laurent + */ +public class WritableNamespaceContext implements NamespaceContext { + + private Map prefixURIMap = new HashMap<>(); + + public void addNamespaceURI(String prefix, String namespaceURI) { + prefixURIMap.put(prefix, namespaceURI); + } + + @Override + public String getNamespaceURI(String prefix) { + return prefixURIMap.get(prefix); + } + + @Override + public String getPrefix(String namespaceURI) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + throw new UnsupportedOperationException(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/AICopilot.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/AICopilot.java new file mode 100644 index 000000000..b516b6e9f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/AICopilot.java @@ -0,0 +1,44 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Service; + +import java.util.List; + +/** + * This is the service interface that holds the different features of AI Copilot in Microcks. + * @author laurent + */ +public interface AICopilot { + + /** + * Suggest/generate sample exchanges for an operation of a Service, from a specification. Depending on contract type + * (OpenAPI, AsyncAPI, GraphQL, ...) the implementation may adapt the way it asks for generation and handle the + * result parsing. + * @param service The Service to generate sample exchanges for + * @param operation The Service operation to generate sample exchanges for + * @param contract The contract on which sample exchange will be based + * @param number The number of requested samples. + * @return A list of exchanges, size of list may be equal of lower than number if generation is incomplete. + * @throws Exception If generation cannot be done (parsing errors, timeout, connection issues, reached quotas, ...) + */ + List suggestSampleExchanges(Service service, Operation operation, Resource contract, int number) + throws Exception; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/AICopilotHelper.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/AICopilotHelper.java new file mode 100644 index 000000000..3e33aa205 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/AICopilotHelper.java @@ -0,0 +1,758 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.ObjectMapperFactory; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.StreamWriteConstraints; +import com.fasterxml.jackson.core.StreamWriteFeature; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.LoaderOptions; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This helper class holds general utility constants and methods for: + *
    + *
  • interacting with LLM (prompt template, formatting specifications
  • + *
  • parsing the output of LLM interaction (converting prompt templates to Microcks domain model)
  • + *
+ * It is intended to be use by {@code AICopilot} implementations so that they can focus on configuration, connection and + * prompt refinement concerns. + * @author laurent + */ +public class AICopilotHelper { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(AICopilotHelper.class); + + protected static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + protected static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); + + protected static final String REALISTIC_AND_VALID_EXAMPLES_PROMPT = """ + Generated examples must be realistic illustrations on how to use the API and must be valid regarding the schema definition. + """; + + protected static final String OPENAPI_OPERATION_PROMPT_TEMPLATE = """ + Given the OpenAPI specification below, generate %2$d full examples (request and response) for operation '%1$s' strictly (no sub-path). + """ + + REALISTIC_AND_VALID_EXAMPLES_PROMPT; + + protected static final String GRAPHQL_OPERATION_PROMPT_TEMPLATE = """ + Given the GraphQL schema below, generate %2$d full examples (request and response) for operation '%1$s' only. + """ + REALISTIC_AND_VALID_EXAMPLES_PROMPT; + + protected static final String ASYNCAPI_OPERATION_PROMPT_TEMPLATE = """ + Given the AsyncAPI specification below, generate %2$d full examples for operation '%1$s' only. + """ + REALISTIC_AND_VALID_EXAMPLES_PROMPT; + + protected static final String GRPC_OPERATION_PROMPT_TEMPLATE = """ + Given the gRPC protobuf schema below, generate %3$d full examples (request and response) for operation '%2$s' of service '%1$s'. + """ + + REALISTIC_AND_VALID_EXAMPLES_PROMPT; + + protected static final String YAML_FORMATTING_PROMPT = """ + Use only the provided YAML format to output the list of examples (no other text or markdown): + """; + protected static final String REQUEST_RESPONSE_EXAMPLE_YAML_FORMATTING_TEMPLATE = """ + - example: + request: + url: + headers: + accept: application/json + parameters: + : + body: + response: + code: + headers: + content-type: application/json + body: + """; + protected static final String UNIDIRECTIONAL_EVENT_EXAMPLE_YAML_FORMATTING_TEMPLATE = """ + - example: + message: + headers: + : + payload: + """; + + protected static final String GRPC_REQUEST_RESPONSE_EXAMPLE_YAML_FORMATTING_TEMPLATE = """ + - example: + request: + body: + response: + body: + """; + + private static final String REQUEST_NODE = "request"; + private static final String RESPONSE_NODE = "response"; + private static final String PARAMETERS_NODE = "parameters"; + private static final String QUERY_NODE = "query"; + private static final String HEADERS_NODE = "headers"; + private static final String VARIABLES_NODE = "variables"; + private static final String BODY_NODE = "body"; + + private AICopilotHelper() { + // Hides the default implicit one as it's a utility class. + } + + /** + * Generate an OpenAPI prompt introduction, asking for generation of {@code numberOfSamples} samples for operation. + */ + protected static String getOpenAPIOperationPromptIntro(String operationName, int numberOfSamples) { + return String.format(OPENAPI_OPERATION_PROMPT_TEMPLATE, operationName, numberOfSamples); + } + + /** + * Generate a GraphQL prompt introduction, asking for generation of {@code numberOfSamples} samples for operation. + */ + protected static String getGraphQLOperationPromptIntro(String operationName, int numberOfSamples) { + return String.format(GRAPHQL_OPERATION_PROMPT_TEMPLATE, operationName, numberOfSamples); + } + + /** + * Generate an AsyncAPI prompt introduction, asking for generation of {@code numberOfSamples} samples for operation. + */ + protected static String getAsyncAPIOperationPromptIntro(String operationName, int numberOfSamples) { + return String.format(ASYNCAPI_OPERATION_PROMPT_TEMPLATE, operationName, numberOfSamples); + } + + /** Generate a GRPC prompt introduction, asking for generation of {@code numberOfSamples} samples for operation. */ + protected static String getGrpcOperationPromptIntro(String serviceName, String operationName, int numberOfSamples) { + return String.format(GRPC_OPERATION_PROMPT_TEMPLATE, serviceName, operationName, numberOfSamples); + } + + protected static String getRequestResponseExampleYamlFormattingDirective(int numberOfSamples) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < numberOfSamples; i++) { + builder.append(String.format(REQUEST_RESPONSE_EXAMPLE_YAML_FORMATTING_TEMPLATE, i + 1)); + } + return builder.toString(); + } + + protected static String getUnidirectionalEventExampleYamlFormattingDirective(int numberOfSamples) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < numberOfSamples; i++) { + builder.append(String.format(UNIDIRECTIONAL_EVENT_EXAMPLE_YAML_FORMATTING_TEMPLATE, i + 1)); + } + return builder.toString(); + } + + protected static String getGrpcRequestResponseExampleYamlFormattingDirective(int numberOfSamples) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < numberOfSamples; i++) { + builder.append(String.format(GRPC_REQUEST_RESPONSE_EXAMPLE_YAML_FORMATTING_TEMPLATE, i + 1)); + } + return builder.toString(); + } + + /** + * Transform the output respecting the {@code REQUEST_RESPONSE_EXAMPLE_YAML_FORMATTING_TEMPLATE} into Microcks domain + * exchanges. + */ + protected static List parseRequestResponseTemplateOutput(Service service, Operation operation, + String content) throws Exception { + List results = new ArrayList<>(); + + JsonNode root = YAML_MAPPER.readTree(sanitizeYamlContent(content)); + if (root.getNodeType() == JsonNodeType.ARRAY) { + Iterator examples = root.elements(); + int index = 1; + while (examples.hasNext()) { + JsonNode example = examples.next(); + String name = example.path("example").asText("Sample " + index++); + + // Deal with parsing request. + JsonNode requestNode = example.path(REQUEST_NODE); + Request request = buildRequest(requestNode, name); + + // Deal with parsing response. + JsonNode responseNode = example.path(RESPONSE_NODE); + Response response = buildResponse(responseNode, name); + + // Finally, take care about dispatchCriteria and complete operation resourcePaths. + // If we previously override the dispatcher with a Fallback, we must be sure to get wrapped elements. + DispatchCriteriaHelper.DispatcherDetails details = DispatchCriteriaHelper + .extractDispatcherWithRules(operation); + + // Finally, take care about dispatchCriteria if dispatching rules are not null. + if (details.rootDispatcherRules() != null) { + completeDispatchCriteriaAndResourcePaths(service, operation, details.rootDispatcher(), + details.rootDispatcherRules(), requestNode, request, response); + } + + if (service.getType() == ServiceType.GRAPHQL) { + adaptGraphQLRequestContent(request); + } + + results.add(new RequestResponsePair(request, response)); + } + } + return results; + } + + /** + * Transform the output respecting the {@code UNIDIRECTIONAL_EVENT_EXAMPLE_YAML_FORMATTING_TEMPLATE} into Microcks + * domain exchanges. + */ + protected static List parseUnidirectionalEventTemplateOutput(String content) throws Exception { + List results = new ArrayList<>(); + + JsonNode root = YAML_MAPPER.readTree(sanitizeYamlContent(content)); + if (root.getNodeType() == JsonNodeType.ARRAY) { + Iterator examples = root.elements(); + while (examples.hasNext()) { + JsonNode example = examples.next(); + + // Deal with parsing message. + JsonNode message = example.path("message"); + EventMessage event = new EventMessage(); + JsonNode headersNode = message.path(HEADERS_NODE); + event.setHeaders(buildHeaders(headersNode)); + event.setMediaType("application/json"); + event.setContent(getMessageContent("application/json", message.path("payload"))); + + results.add(new UnidirectionalEvent(event)); + } + } + return results; + } + + /** Sanitize the pseudo Yaml sometimes returned into plain valid Yaml. */ + private static String sanitizeYamlContent(String pseudoYaml) { + pseudoYaml = pseudoYaml.trim(); + if (!pseudoYaml.startsWith("-")) { + boolean inYaml = false; // Are we currently in Yaml content? + boolean nextIsYaml = false; // May the next line be Yaml content? + boolean addPadding = false; // Do we have to add padding? + StringBuilder yaml = new StringBuilder(); + String[] lines = pseudoYaml.split("\\r?\\n|\\r"); + for (String line : lines) { + if (line.startsWith("-")) { + inYaml = true; + } + if (line.trim().length() == 0) { + inYaml = false; + } + if (nextIsYaml && !line.startsWith("-")) { + inYaml = true; + nextIsYaml = false; + addPadding = true; + yaml.append("- ").append(line).append("\n"); + continue; + } + if (line.startsWith("```")) { + // Starting or ending markdown block. + if (inYaml) { + inYaml = false; + nextIsYaml = false; + addPadding = false; + } else { + nextIsYaml = true; + } + } + if (inYaml) { + yaml.append(addPadding ? " " : "").append(line).append("\n"); + // We don't know what next is going to be... + nextIsYaml = false; + } + } + return yaml.toString(); + } + return pseudoYaml; + } + + private static Request buildRequest(JsonNode requestNode, String name) throws Exception { + Request request = new Request(); + request.setName(name); + JsonNode requestHeadersNode = requestNode.path(HEADERS_NODE); + request.setHeaders(buildHeaders(requestHeadersNode)); + request.setContent(getRequestContent(requestHeadersNode, requestNode.path(BODY_NODE))); + return request; + } + + private static Multimap buildRequestParameters(Request request, JsonNode parametersNode) { + Multimap result = ArrayListMultimap.create(); + Iterator> parameters = parametersNode.fields(); + while (parameters.hasNext()) { + Map.Entry parameterNode = parameters.next(); + + // Depending on node type, extract different representations to MultiMap result. + if (parameterNode.getValue().isArray()) { + for (JsonNode current : parameterNode.getValue()) { + result.put(parameterNode.getKey(), getSerializedValue(current)); + } + } else if (parameterNode.getValue().isObject()) { + final var fieldsIterator = parameterNode.getValue().fields(); + while (fieldsIterator.hasNext()) { + var current = fieldsIterator.next(); + result.put(current.getKey(), getSerializedValue(current.getValue())); + } + } else { + result.put(parameterNode.getKey(), getSerializedValue(parameterNode.getValue())); + } + } + return result; + } + + private static Response buildResponse(JsonNode responseNode, String name) throws Exception { + Response response = new Response(); + response.setName(name); + JsonNode responseHeadersNode = responseNode.path(HEADERS_NODE); + response.setHeaders(buildHeaders(responseHeadersNode)); + response.setContent(getResponseContent(responseHeadersNode, responseNode.path(BODY_NODE))); + response.setMediaType(responseHeadersNode.path("content-type").asText(null)); + response.setStatus(responseNode.path("code").asText("200")); + response.setFault(response.getStatus().startsWith("4") || response.getStatus().startsWith("5")); + return response; + } + + private static void completeDispatchCriteriaAndResourcePaths(Service service, Operation operation, + String rootDispatcher, String rootDispatcherRules, JsonNode requestNode, Request request, Response response) + throws Exception { + + String dispatchCriteria = null; + String resourcePathPattern = operation.getName().contains(" ") ? operation.getName().split(" ")[1] + : operation.getName(); + + // Extract parameters if LLM provided some. + Multimap parameters = null; + if (requestNode.has(PARAMETERS_NODE)) { + parameters = buildRequestParameters(request, requestNode.get(PARAMETERS_NODE)); + } else { + parameters = ArrayListMultimap.create(); + } + // Extract parameters from URL if LLM provided some. + if (requestNode.has("url")) { + String url = requestNode.get("url").asText(); + Map pathParameters = DispatchCriteriaHelper.extractMapFromURIPattern(rootDispatcherRules, + resourcePathPattern, url); + Multimap queryParameters = DispatchCriteriaHelper.extractMapFromURIParams(rootDispatcherRules, + url); + + // Complete parameter map with both content. + for (Map.Entry entry : pathParameters.entrySet()) { + if (!parameters.containsKey(entry.getKey())) { + parameters.put(entry.getKey(), entry.getValue()); + } + } + for (Map.Entry entry : queryParameters.entries()) { + if (!parameters.containsKey(entry.getKey())) { + parameters.put(entry.getKey(), entry.getValue()); + } + } + } + + // Now that we should have all parameters, we can complete the + for (Map.Entry entry : parameters.entries()) { + Parameter parameter = new Parameter(); + parameter.setName(entry.getKey()); + parameter.setValue(entry.getValue()); + request.addQueryParameter(parameter); + } + + // Compute a dispatch criteria based on dispatcher and dispatcher rules but + // don't update the operation resource path as the request/response has not been reviewed by user yet. + if (DispatchStyles.URI_PARAMS.equals(rootDispatcher)) { + dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, parameters); + } else if (DispatchStyles.URI_PARTS.equals(rootDispatcher)) { + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parameters); + } else if (DispatchStyles.URI_ELEMENTS.equals(rootDispatcher)) { + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parameters); + dispatchCriteria += DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, parameters); + } else if (DispatchStyles.QUERY_HEADER.equals(rootDispatcher)) { + if (requestNode.has(HEADERS_NODE)) { + JsonNode headersNode = requestNode.get(HEADERS_NODE); + Map headersMap = extractHeaders(headersNode); + dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, parameters); + } + } else if (DispatchStyles.QUERY_ARGS.equals(rootDispatcher)) { + // This dispatcher is used for GraphQL or gRPC + if (ServiceType.GRAPHQL.equals(service.getType())) { + Map variables = getGraphQLVariables(request.getContent()); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(operation.getDispatcherRules(), variables); + } else if (ServiceType.GRPC.equals(service.getType())) { + Map bodyAsMap = JSON_MAPPER.readValue(request.getContent(), + TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class)); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(operation.getDispatcherRules(), bodyAsMap); + } + } + response.setDispatchCriteria(dispatchCriteria); + } + + private static Set
buildHeaders(JsonNode headersNode) { + Set
headers = new HashSet<>(); + Iterator> headerNodes = headersNode.fields(); + while (headerNodes.hasNext()) { + Map.Entry headerNodeEntry = headerNodes.next(); + Header header = new Header(); + header.setName(headerNodeEntry.getKey()); + header.setValues(Set.of(headerNodeEntry.getValue().asText())); + headers.add(header); + } + return headers; + } + + private static String getRequestContent(JsonNode headersNode, JsonNode bodyNode) throws Exception { + String contentType = headersNode.path("accept").asText(null); + return getMessageContent(contentType, bodyNode); + } + + private static String getResponseContent(JsonNode headersNode, JsonNode bodyNode) throws Exception { + String contentType = headersNode.path("content-type").asText(null); + return getMessageContent(contentType, bodyNode); + } + + private static String getMessageContent(String contentType, JsonNode bodyNode) throws Exception { + if (!bodyNode.isMissingNode()) { + + if (!bodyNode.isTextual() && !bodyNode.isEmpty() + && (contentType == null || contentType.contains("application/json"))) { + return JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(bodyNode); + } else if (bodyNode.isTextual()) { + return bodyNode.asText(); + } + } + return null; + } + + /** Get the serialized value of a content node, or null if node is null ;-) */ + private static String getSerializedValue(JsonNode contentNode) { + if (contentNode != null) { + // Get string representation if array or object. + if (contentNode.getNodeType() == JsonNodeType.ARRAY || contentNode.getNodeType() == JsonNodeType.OBJECT) { + return contentNode.toString(); + } + // Else get raw representation. + return contentNode.asText(); + } + return null; + } + + /** Extract headers from a node to be processed for dispatch criteria computing. */ + private static Map extractHeaders(JsonNode headersNode) { + Map result = new HashMap<>(); + Iterator> headers = headersNode.fields(); + while (headers.hasNext()) { + Map.Entry headerNode = headers.next(); + result.put(headerNode.getKey(), getSerializedValue(headerNode.getValue())); + } + return result; + } + + private static Map getGraphQLVariables(String requestContent) throws Exception { + JsonNode graphQL = JSON_MAPPER.readTree(requestContent); + + if (graphQL.has(VARIABLES_NODE)) { + JsonNode variablesNode = graphQL.path(VARIABLES_NODE); + Map results = new HashMap<>(); + Set> elements = variablesNode.properties(); + for (Map.Entry element : elements) { + results.put(element.getKey(), element.getValue().asText()); + } + return results; + } else { + log.warn("GraphQL request do not contain variables..."); + } + return Collections.emptyMap(); + } + + private static void adaptGraphQLRequestContent(Request request) throws Exception { + JsonNode graphQL = JSON_MAPPER.readTree(request.getContent()); + if (graphQL.has(QUERY_NODE)) { + // GraphQL query may have \n we'd like to escape for better display. + String query = graphQL.path(QUERY_NODE).asText(); + if (query.contains("\n")) { + query = query.replace("\n", "\\n"); + ((ObjectNode) graphQL).put(QUERY_NODE, query); + request.setContent(JSON_MAPPER.writeValueAsString(graphQL)); + } + } + } + + /** Follow the $ref if we have one. Otherwise return given node. */ + private static JsonNode followRefIfAny(JsonNode spec, JsonNode referencableNode) { + if (referencableNode.has("$ref")) { + String ref = referencableNode.path("$ref").asText(); + return getNodeForRef(spec, ref); + } + return referencableNode; + } + + private static JsonNode getNodeForRef(JsonNode spec, String reference) { + return spec.at(reference.substring(1)); + } + + /** + * Some AI will reuse already present examples and x-microcks-operation in the spec. This method cleans a + * specification from its examples. + */ + protected static String removeTokensFromSpec(String specification, String operationName) throws Exception { + // Parse and remove unrelated operations from the spec. + JsonNode specNode = parseAndFilterSpec(specification, operationName); + + Map resolvedReferences = new HashMap<>(); + try { + // 1.5MB is a good threshold to avoid too many references resolution. + if (specification.length() < 1500000) { + resolveReferenceAndRemoveTokensInNode(specNode, specNode, new HashMap<>(), new HashSet<>(), true); + } else { + resolveReferenceAndRemoveTokensInNode(specNode, specNode, resolvedReferences, new HashSet<>(), false); + } + return filterAndSerializeSpec(specification, specNode, + resolvedReferences.keySet().stream().filter(ref -> ref.startsWith("#/components/schemas")) + .map(ref -> ref.substring("#/components/schemas".length() + 1)).toList()); + } catch (Throwable t) { + log.error( + "Exception while resolving references in spec. This is likely due to circular or too many references in the spec: {}", + t.getMessage()); + log.error("Trying with the raw spec..."); + specNode = parseAndFilterSpec(specification, operationName); + return filterAndSerializeSpec(specification, specNode, List.of()); + } + } + + /** */ + protected static JsonNode parseAndFilterSpec(String specification, String operationName) throws Exception { + // Parse the specification to a JsonNode. + boolean isJson = specification.trim().startsWith("{"); + JsonNode specNode; + if (isJson) { + specNode = JSON_MAPPER.readTree(specification); + } else { + specNode = ObjectMapperFactory.getYamlObjectMapper().readTree(specification); + } + + // Filter the operation to avoid resolving references on the whole spec. + List specTokenNames = getTokenNames(specNode); + if (specTokenNames.contains("openapi")) { + filterOpenAPISpecOnOperation(specNode, operationName); + } else if (specTokenNames.contains("asyncapi")) { + filterAsyncAPISpecOnOperation(specNode, operationName); + } + return specNode; + } + + /** */ + protected static String filterAndSerializeSpec(String originalSpec, JsonNode specNode, List schemasToKeep) + throws IOException { + // Remove schemas if not in the list of components/schemas to keep. + if (!schemasToKeep.isEmpty() && specNode.has("components")) { + ObjectNode componentsNode = (ObjectNode) specNode.get("components"); + if (componentsNode.has("schemas")) { + ObjectNode schemasNode = (ObjectNode) componentsNode.get("schemas"); + removeTokensInNode(schemasNode, schemasToKeep); + } + } else { + ((ObjectNode) specNode).remove("components"); + } + + // Filter the remaining elements in spec. + List specTokenNames = getTokenNames(specNode); + if (specTokenNames.contains("openapi")) { + filterOpenAPISpec(specNode); + } else if (specTokenNames.contains("asyncapi")) { + filterAsyncAPISpec(specNode); + } + + // Serialize the spec back to a string. + boolean isJson = originalSpec.trim().startsWith("{"); + if (isJson) { + return JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(specNode); + } + return ObjectMapperFactory.getYamlObjectMapper().writeValueAsString(specNode); + } + + protected static void resolveReferenceAndRemoveTokensInNode(JsonNode specNode, JsonNode node, + Map resolvedReferences, Set resolvingReferences) { + resolveReferenceAndRemoveTokensInNode(specNode, node, resolvedReferences, resolvingReferences, true); + } + + protected static void resolveReferenceAndRemoveTokensInNode(JsonNode specNode, JsonNode node, + Map resolvedReferences, Set resolvingReferences, boolean perBranchResolution) { + // Are we currently resolving a reference? + String resolvingRef = null; + boolean resolveRef = false; + boolean skipChildren = false; + + if (node.getNodeType() == JsonNodeType.OBJECT) { + if (node.has("$ref")) { + resolvingRef = node.path("$ref").asText(); + + // The reference may have already been resolved globally. + JsonNode target = resolvedReferences.get(resolvingRef); + if (target != null) { + // Dereference the $ref and merge the target into the node. + ((ObjectNode) node).setAll((ObjectNode) target); + ((ObjectNode) node).remove("$ref"); + // Mark to skip children as we have already processed the target. + skipChildren = true; + } else { + // Check the reference is not currently tried to be resolved. + // In this case, we are in a circular reference situation, just let the $ref in place if it is not already resolved. + if (!resolvingReferences.contains(resolvingRef)) { + target = getNodeForRef(specNode, resolvingRef); + if (target != null) { + // Dereference the $ref and merge the target into the node. + ((ObjectNode) node).setAll((ObjectNode) target); + ((ObjectNode) node).remove("$ref"); + // Add the resolving reference to the set of currently resolving references. + resolvingReferences.add(resolvingRef); + resolveRef = true; + } else { + log.warn("Reference '{}' could not be resolved in the spec.", resolvingRef); + } + } + } + } + + // Remove common fields that are not needed in the context of AI copilot. + removeTokensInNode((ObjectNode) node); + + Iterator> fields = node.fields(); + while (fields.hasNext() && !skipChildren) { + Map.Entry field = fields.next(); + // Do not browse the root specNode components. + // Do not recurse on un-resolved $ref field as it has been already processed. + if ((specNode == node && !"components".equals(field.getKey())) + || (specNode != node && !"$ref".equals(field.getKey()))) { + resolveReferenceAndRemoveTokensInNode(specNode, field.getValue(), resolvedReferences, + resolvingReferences, perBranchResolution); + } + } + } else if (node.getNodeType() == JsonNodeType.ARRAY) { + // If node is an array, we need to iterate over its elements. + Iterator elements = node.elements(); + while (elements.hasNext()) { + resolveReferenceAndRemoveTokensInNode(specNode, elements.next(), resolvedReferences, resolvingReferences, + perBranchResolution); + } + } + + // Track the resolved reference if we were currently resolving one. + if (resolvingRef != null && resolveRef) { + // If we are in per-branch resolution mode, we can pop the resolving reference from the set of resolved references. + if (perBranchResolution) { + resolvingReferences.remove(resolvingRef); + } + // In any case, save the resolved reference in the map of resolved references. + resolvedReferences.put(resolvingRef, node); + } + } + + protected static void removeTokensInNode(ObjectNode node) { + // Remove common tokens that are not needed in the context of AI copilot. + node.remove("examples"); + node.remove("example"); + node.remove("x-microcks"); + node.remove("x-microcks-operation"); + } + + protected static void filterOpenAPISpecOnOperation(JsonNode specNode, String operationName) throws Exception { + String[] operationPathName = operationName.split(" "); + String verb = operationPathName[0].toLowerCase(); + String path = operationPathName[1]; + + JsonNode pathsSpec = specNode.get("paths"); + JsonNode pathSpec = pathsSpec.get(path); + + removeTokensInNode(pathsSpec, List.of(path)); + removeTokensInNode(pathSpec, List.of(verb)); + removeSecurityTokenInNode(pathSpec, verb); + } + + protected static void filterOpenAPISpec(JsonNode specNode) { + List keysToKeepInRoot = List.of("openapi", "paths", "info", "components"); + removeTokensInNode(specNode, keysToKeepInRoot); + } + + protected static void filterAsyncAPISpecOnOperation(JsonNode specNode, String channelName) { + String[] operationPathName = channelName.split(" "); + String channel = operationPathName[1]; + + JsonNode channelsSpec = ((ObjectNode) specNode).get("channels"); + removeTokensInNode(channelsSpec, List.of(channel)); + } + + protected static void filterAsyncAPISpec(JsonNode specNode) { + List keysToKeepInRoot = List.of("asyncapi", "channels", "info", "components"); + removeTokensInNode(specNode, keysToKeepInRoot); + } + + protected static void removeSecurityTokenInNode(JsonNode specNode, String tokenName) { + JsonNode verbSpec = ((ObjectNode) specNode).get(tokenName); + if (verbSpec.has("security")) { + ((ObjectNode) verbSpec).remove("security"); + } + } + + protected static List getTokenNames(JsonNode specNode) { + List fieldNames = new ArrayList<>(); + Iterator specNodeFieldNames = specNode.fieldNames(); + while (specNodeFieldNames.hasNext()) { + fieldNames.add(specNodeFieldNames.next()); + } + return fieldNames; + } + + protected static void removeTokensInNode(JsonNode specNode, List keysToKeep) { + List fieldNames = getTokenNames(specNode); + for (String fieldName : fieldNames) { + if (!keysToKeep.contains(fieldName)) { + ((ObjectNode) specNode).remove(fieldName); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/ChatCompletionRequestMixIn.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/ChatCompletionRequestMixIn.java new file mode 100644 index 000000000..f17eb17ea --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/ChatCompletionRequestMixIn.java @@ -0,0 +1,34 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; + +/** + * This is a Jackson serialization MixIn coming from https://github.com/TheoKanning/openai-java. This is an extract from + * https://github.com/TheoKanning/openai-java/blob/main/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestMixIn.java + * to avoid pulling the whole 'service' package and its Http clients libraries. Credits to + * Theo Kanning! + * @author laurent + */ +public abstract class ChatCompletionRequestMixIn { + + @JsonSerialize(using = ChatCompletionRequestSerializerAndDeserializer.Serializer.class) + @JsonDeserialize(using = ChatCompletionRequestSerializerAndDeserializer.Deserializer.class) + abstract ChatCompletionRequest.ChatCompletionRequestFunctionCall getFunctionCall(); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/ChatCompletionRequestSerializerAndDeserializer.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/ChatCompletionRequestSerializerAndDeserializer.java new file mode 100644 index 000000000..d7418e4c1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/ChatCompletionRequestSerializerAndDeserializer.java @@ -0,0 +1,66 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; + +import java.io.IOException; + +/** + * This is a Jackson serializer/deserializer config coming from https://github.com/TheoKanning/openai-java. This is an + * extract from + * https://github.com/TheoKanning/openai-java/blob/main/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestSerializerAndDeserializer.java + * to avoid pulling the whole 'service' package and its Http clients libraries. Credits to + * Theo Kanning! + * @author laurent + */ +public class ChatCompletionRequestSerializerAndDeserializer { + + public static class Serializer extends JsonSerializer { + @Override + public void serialize(ChatCompletionRequest.ChatCompletionRequestFunctionCall value, JsonGenerator gen, + SerializerProvider serializers) throws IOException { + if (value == null || value.getName() == null) { + gen.writeNull(); + } else if ("none".equals(value.getName()) || "auto".equals(value.getName())) { + gen.writeString(value.getName()); + } else { + gen.writeStartObject(); + gen.writeFieldName("name"); + gen.writeString(value.getName()); + gen.writeEndObject(); + } + } + } + + public static class Deserializer extends JsonDeserializer { + @Override + public ChatCompletionRequest.ChatCompletionRequestFunctionCall deserialize(JsonParser p, + DeserializationContext ctxt) throws IOException { + if (p.getCurrentToken().isStructStart()) { + p.nextToken(); //key + p.nextToken(); //value + } + return new ChatCompletionRequest.ChatCompletionRequestFunctionCall(p.getValueAsString()); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpError.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpError.java new file mode 100644 index 000000000..e6cc019f4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpError.java @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +/** + * Utility class for handling errors from MCP Tool invocations. + * @author laurent + */ +public class McpError extends RuntimeException { + + private McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError; + + /** + * Constructor for creating a new McpError from a JSONRPCError. + * @param jsonRpcError The JSONRPCError to create the McpError from. + */ + public McpError(McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError) { + super(jsonRpcError.message()); + this.jsonRpcError = jsonRpcError; + } + + /** + * Constructor for creating a new McpError from an error object string representation. + * @param error The error object to create the McpError from. + */ + public McpError(Object error) { + super(error.toString()); + } + + /** + * Get the JSONRPCError associated with this McpError. + * @return The JSONRPCError associated with this McpError. + */ + public McpSchema.JSONRPCResponse.JSONRPCError getJsonRpcError() { + return jsonRpcError; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpSchema.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpSchema.java new file mode 100644 index 000000000..0e03e90f6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpSchema.java @@ -0,0 +1,421 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Based on the JSON-RPC 2.0 specification and the + * Model Context + * Protocol Schema. + * @author laurent + */ +public class McpSchema { + + public static final String LATEST_PROTOCOL_VERSION = "2025-03-26"; + public static final String FIRST_PROTOCOL_VERSION = "2024-11-05"; + + public static final String JSONRPC_VERSION = "2.0"; + + // --------------------------- + // Method Names + // --------------------------- + + // Lifecycle Methods + public static final String METHOD_INITIALIZE = "initialize"; + public static final String METHOD_NOTIFICATION_INITIALIZED = "notifications/initialized"; + public static final String METHOD_PING = "ping"; + + // Tool Methods + public static final String METHOD_TOOLS_LIST = "tools/list"; + public static final String METHOD_TOOLS_CALL = "tools/call"; + public static final String METHOD_NOTIFICATION_TOOLS_LIST_CHANGED = "notifications/tools/list_changed"; + + // Resources Methods + public static final String METHOD_RESOURCES_LIST = "resources/list"; + public static final String METHOD_RESOURCES_READ = "resources/read"; + public static final String METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED = "notifications/resources/list_changed"; + public static final String METHOD_RESOURCES_TEMPLATES_LIST = "resources/templates/list"; + public static final String METHOD_RESOURCES_SUBSCRIBE = "resources/subscribe"; + public static final String METHOD_RESOURCES_UNSUBSCRIBE = "resources/unsubscribe"; + + // Prompt Methods + public static final String METHOD_PROMPT_LIST = "prompts/list"; + public static final String METHOD_PROMPT_GET = "prompts/get"; + public static final String METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed"; + + // Logging Methods + public static final String METHOD_LOGGING_SET_LEVEL = "logging/setLevel"; + public static final String METHOD_NOTIFICATION_MESSAGE = "notifications/message"; + + // Roots Methods + public static final String METHOD_ROOTS_LIST = "roots/list"; + public static final String METHOD_NOTIFICATION_ROOTS_LIST_CHANGED = "notifications/roots/list_changed"; + + // Sampling Methods + public static final String METHOD_SAMPLING_CREATE_MESSAGE = "sampling/createMessage"; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + + // --------------------------- + // JSON-RPC Error Codes + // --------------------------- + /** + * Standard error codes used in MCP JSON-RPC responses. + */ + public static final class ErrorCodes { + + /** Invalid JSON was received by the server. */ + public static final int PARSE_ERROR = -32700; + + /** The JSON sent is not a valid Request object. */ + public static final int INVALID_REQUEST = -32600; + + /** The method does not exist / is not available. */ + public static final int METHOD_NOT_FOUND = -32601; + + /** Invalid method parameter(s). */ + public static final int INVALID_PARAMS = -32602; + + /** Internal JSON-RPC error. */ + public static final int INTERNAL_ERROR = -32603; + + } + + /** JSON-RPC Message Types. */ + public sealed interface JSONRPCMessage permits JSONRPCRequest, JSONRPCNotification, JSONRPCResponse { + String jsonrpc(); + } + + // spotless:off + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record JSONRPCRequest( + @JsonProperty("jsonrpc") String jsonrpc, + @JsonProperty("method") String method, + @JsonProperty("id") Object id, + @JsonProperty("params") Object params) implements JSONRPCMessage { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record JSONRPCNotification( + @JsonProperty("jsonrpc") String jsonrpc, @ + JsonProperty("method") String method, + @JsonProperty("params") Map params) implements JSONRPCMessage { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record JSONRPCResponse( + @JsonProperty("jsonrpc") String jsonrpc, + @JsonProperty("id") Object id, + @JsonProperty("result") Object result, + @JsonProperty("error") JSONRPCError error) implements JSONRPCMessage { + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record JSONRPCError( + @JsonProperty("code") int code, + @JsonProperty("message") String message, + @JsonProperty("data") Object data) { + } + } + // spotless:on + + // spotless:off + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record Implementation( + @JsonProperty("name") String name, + @JsonProperty("version") String version) { + } + + public enum Role { + @JsonProperty("user") USER, + @JsonProperty("assistant") ASSISTANT + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record ClientCapabilities( + @JsonProperty("experimental") Map experimental, + @JsonProperty("roots") ClientCapabilities.RootCapabilities roots, + @JsonProperty("sampling") ClientCapabilities.Sampling sampling) { + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record RootCapabilities( + @JsonProperty("listChanged") Boolean listChanged) { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record Sampling() { + } + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record ServerCapabilities( + @JsonProperty("experimental") Map experimental, + @JsonProperty("logging") ServerCapabilities.LoggingCapabilities logging, + @JsonProperty("prompts") ServerCapabilities.PromptCapabilities prompts, + @JsonProperty("resources") ServerCapabilities.ResourceCapabilities resources, + @JsonProperty("tools") ServerCapabilities.ToolCapabilities tools) { + + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record LoggingCapabilities() { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record PromptCapabilities( + @JsonProperty("listChanged") Boolean listChanged) { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record ResourceCapabilities( + @JsonProperty("subscribe") Boolean subscribe, + @JsonProperty("listChanged") Boolean listChanged) { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record ToolCapabilities( + @JsonProperty("listChanged") Boolean listChanged) { + } + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record ModelPreferences( + @JsonProperty("hints") List hints, + @JsonProperty("costPriority") Double costPriority, + @JsonProperty("speedPriority") Double speedPriority, + @JsonProperty("intelligencePriority") Double intelligencePriority) { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record ModelHint(@JsonProperty("name") String name) { + public static ModelHint of(String name) { + return new ModelHint(name); + } + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record SamplingMessage( + @JsonProperty("role") Role role, + @JsonProperty("content") Content content) { + } + // spotless:on + + /** MCP Request Types. */ + public sealed interface Request permits InitializeRequest, CallToolRequest, CreateMessageRequest { + + } + + // spotless:off + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record InitializeRequest( + @JsonProperty("protocolVersion") String protocolVersion, + @JsonProperty("capabilities") ClientCapabilities capabilities, + @JsonProperty("clientInfo") Implementation clientInfo) implements Request { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record InitializeResult( + @JsonProperty("protocolVersion") String protocolVersion, + @JsonProperty("capabilities") ServerCapabilities capabilities, + @JsonProperty("serverInfo") Implementation serverInfo, + @JsonProperty("instructions") String instructions) { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record ListToolsResult( + @JsonProperty("tools") List tools, + @JsonProperty("nextCursor") String nextCursor) { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record CallToolRequest( + @JsonProperty("name") String name, + @JsonProperty("arguments") Map arguments) implements Request { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record CallToolResult( + @JsonProperty("content") List content, + @JsonProperty("isError") Boolean isError) { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record CreateMessageRequest( + @JsonProperty("messages") List messages, + @JsonProperty("modelPreferences") ModelPreferences modelPreferences, + @JsonProperty("systemPrompt") String systemPrompt, + @JsonProperty("includeContext") CreateMessageRequest.ContextInclusionStrategy includeContext, + @JsonProperty("temperature") Double temperature, + @JsonProperty("maxTokens") int maxTokens, + @JsonProperty("stopSequences") List stopSequences, + @JsonProperty("metadata") Map metadata) implements Request { + + public enum ContextInclusionStrategy { + @JsonProperty("none") NONE, + @JsonProperty("thisServer") THIS_SERVER, + @JsonProperty("allServers") ALL_SERVERS + } + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record CreateMessageResult( + @JsonProperty("role") Role role, + @JsonProperty("content") Content content, + @JsonProperty("model") String model, + @JsonProperty("stopReason") CreateMessageResult.StopReason stopReason) { + + public enum StopReason { + @JsonProperty("endTurn") END_TURN, + @JsonProperty("stopSequence") STOP_SEQUENCE, + @JsonProperty("maxTokens") MAX_TOKENS + } + } + // spotless:on + + /** Tools model. */ + // spotless:off + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record Tool( + @JsonProperty("name") String name, + @JsonProperty("description") String description, + @JsonProperty("inputSchema") JsonSchema inputSchema) { + + public Tool(String name, String description, String schema) { + this(name, description, parseSchema(schema)); + } + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record JsonSchema( + @JsonProperty("type") String type, + @JsonProperty("properties") Map properties, + @JsonProperty("required") List required, + @JsonProperty("additionalProperties") Boolean additionalProperties) { + } + + private static JsonSchema parseSchema(String schema) { + try { + return OBJECT_MAPPER.readValue(schema, JsonSchema.class); + } + catch (IOException e) { + throw new IllegalArgumentException("Invalid schema: " + schema, e); + } + } + // spotless:on + + /** Result Content Types. */ + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + @JsonSubTypes({ @JsonSubTypes.Type(value = TextContent.class, name = "text"), + @JsonSubTypes.Type(value = ImageContent.class, name = "image"), + @JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource") }) + public sealed interface Content permits TextContent, ImageContent, EmbeddedResource { + + default String type() { + if (this instanceof McpSchema.TextContent) { + return "text"; + } else if (this instanceof McpSchema.ImageContent) { + return "image"; + } else if (this instanceof McpSchema.EmbeddedResource) { + return "resource"; + } + throw new IllegalArgumentException("Unknown content type: " + this); + } + } + + // spotless:off + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record TextContent( + @JsonProperty("audience") List audience, + @JsonProperty("priority") Double priority, + @JsonProperty("text") String text) implements Content { + + public TextContent(String content) { + this(null, null, content); + } + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record ImageContent( + @JsonProperty("audience") List audience, + @JsonProperty("priority") Double priority, + @JsonProperty("data") String data, + @JsonProperty("mimeType") String mimeType) implements Content { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record EmbeddedResource( + @JsonProperty("audience") List audience, + @JsonProperty("priority") Double priority, + @JsonProperty("resource") ResourceContents resource) implements Content { + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, include = JsonTypeInfo.As.PROPERTY) + @JsonSubTypes({ @JsonSubTypes.Type(value = TextResourceContents.class, name = "text"), + @JsonSubTypes.Type(value = BlobResourceContents.class, name = "blob") }) + public sealed interface ResourceContents permits TextResourceContents, BlobResourceContents { + String uri(); + String mimeType(); + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record TextResourceContents( + @JsonProperty("uri") String uri, + @JsonProperty("mimeType") String mimeType, + @JsonProperty("text") String text) implements ResourceContents { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record BlobResourceContents( + @JsonProperty("uri") String uri, + @JsonProperty("mimeType") String mimeType, + @JsonProperty("blob") String blob) implements ResourceContents { + } + // spotless:on +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpToolConverter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpToolConverter.java new file mode 100644 index 000000000..6eefe3034 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/McpToolConverter.java @@ -0,0 +1,108 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.web.ResponseResult; + +import org.springframework.http.HttpHeaders; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; + +/** + * Utility base class for converting a Microcks Service and Operation into an MCP Tool. + * @author laurent + */ +public abstract class McpToolConverter { + protected final Service service; + protected final Resource resource; + + public McpToolConverter(Service service, Resource resource) { + this.service = service; + this.resource = resource; + } + + /** + * Extract the name of the tool from the operation. + * @param operation The operation to extract the name from. + * @return The tool name + */ + public String getToolName(Operation operation) { + return operation.getName(); + } + + /** + * Extract the description of the tool from the operation. + * @param operation The operation to extract the description from. + * @return The tool description + */ + public abstract String getToolDescription(Operation operation); + + /** + * Extract the input schema of the tool from the operation. + * @param operation The operation to extract the input schema from. + * @return The tool input schema following the 2024-11-05 MCP spec + */ + public abstract McpSchema.JsonSchema getInputSchema(Operation operation); + + /** + * Invoke the tool with the given request and return the response in Microcks domain object. + * @param operation The operation to invoke the tool on. + * @param request The request to send to the tool. + * @param headers Simple representation of headers transmitted at the protocol level. + * @return The response from the tool in Microcks domain object. + */ + public abstract Response getCallResponse(Operation operation, McpSchema.CallToolRequest request, + Map> headers); + + /** Prepare the Http headers by sanitizing them. */ + protected Map> sanitizeHttpHeaders(Map> headers) { + return headers.entrySet().stream().filter(entry -> !"content-length".equalsIgnoreCase(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** Depending on result encoding, extract the response content as a string. */ + protected String extractResponseContent(ResponseResult result) throws IOException { + String responseContent = null; + if (result.headers() != null) { + // Response content can be compressed with gzip if we used the proxy. + List encodings = result.headers().get(HttpHeaders.CONTENT_ENCODING); + if (encodings != null && encodings.contains("gzip")) { + // Unzip the response content. + try (BufferedInputStream bis = new BufferedInputStream( + new GZIPInputStream(new ByteArrayInputStream(result.content())))) { + byte[] uncompressedContent = bis.readAllBytes(); + responseContent = new String(uncompressedContent, StandardCharsets.UTF_8); + } + } + } + if (responseContent == null) { + // If no response content here, we can assume it's not compressed and can be read directly. + responseContent = new String(result.content(), StandardCharsets.UTF_8); + } + return responseContent; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/OpenAICopilot.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/OpenAICopilot.java new file mode 100644 index 000000000..d878ddd43 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/ai/OpenAICopilot.java @@ -0,0 +1,283 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchStyles; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.theokanning.openai.completion.chat.ChatCompletionChoice; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatCompletionResult; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This is an implementation of {@code AICopilot} using OpenAI API. + * @author laurent + */ +public class OpenAICopilot implements AICopilot { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(OpenAICopilot.class); + + + /** Configuration parameter holding the OpenAI API key. */ + public static final String API_KEY_CONFIG = "api-key"; + + /** Configuration parameter holding the OpenAI API URL. */ + public static final String API_URL_CONFIG = "api-url"; + + /** Configuration parameters holding the timeout in seconds for API calls. */ + public static final String TIMEOUT_KEY_CONFIG = "timeout"; + + /** Configuration parameter holding the name of model to use. */ + public static final String MODEL_KEY_CONFIG = "model"; + + /** Configuration parameter holding the maximum number of tokens to use. */ + public static final String MAX_TOKENS_KEY_CONFIG = "maxTokens"; + + /** The mandatory configuration keys required by this implementation. */ + protected static final String[] MANDATORY_CONFIG_KEYS = { API_KEY_CONFIG }; + + + /** Default online URL for OpenAI API. */ + private static final String OPENAI_BASE_URL = "https://api.openai.com/"; + + private static final String SECTION_DELIMITER = "\n#####\n"; + + private RestTemplate restTemplate; + + private String apiUrl = OPENAI_BASE_URL; + + private String apiKey; + + private Duration timeout = Duration.ofSeconds(20); + + private String model = "gpt-3.5-turbo"; + + private int maxTokens = 2000; + + + /** + * Build a new OpenAICopilot with its configuration. + * @param configuration The configuration for connecting to OpenAI services. + */ + public OpenAICopilot(Map configuration) { + if (configuration.containsKey(TIMEOUT_KEY_CONFIG)) { + try { + timeout = Duration.ofSeconds(Integer.parseInt(configuration.get(TIMEOUT_KEY_CONFIG))); + } catch (Exception e) { + log.warn("Timeout was provided but cannot be parsed. Sticking to the default."); + } + } + if (configuration.containsKey(MAX_TOKENS_KEY_CONFIG)) { + try { + maxTokens = Integer.parseInt(configuration.get(MAX_TOKENS_KEY_CONFIG)); + } catch (Exception e) { + log.warn("MaxTokens was provided but cannot be parsed. Sticking to the default."); + } + } + if (configuration.containsKey(MODEL_KEY_CONFIG)) { + model = configuration.get(MODEL_KEY_CONFIG); + } + if (configuration.containsKey(API_URL_CONFIG)) { + apiUrl = configuration.get(API_URL_CONFIG); + } + // Finally retrieve the OpenAI Api key. + apiKey = configuration.get(API_KEY_CONFIG); + + // Initialize a Rest template for interacting with OpenAI API. + // We need to register a custom Jackson converter to handle serialization of name and function_call of messages. + restTemplate = new RestTemplateBuilder().setReadTimeout(timeout) + .additionalMessageConverters(mappingJacksonHttpMessageConverter()).build(); + } + + /** + * Get mandatory configuration parameters. + * @return The mandatory configuration keys required by this implementation + */ + public static final String[] getMandatoryConfigKeys() { + return MANDATORY_CONFIG_KEYS; + } + + @Override + public List suggestSampleExchanges(Service service, Operation operation, Resource contract, + int number) throws Exception { + String prompt = ""; + + if (service.getType() == ServiceType.REST) { + prompt = preparePromptForOpenAPI(operation, contract, number); + } else if (service.getType() == ServiceType.GRAPHQL) { + prompt = preparePromptForGraphQL(operation, contract, number); + } else if (service.getType() == ServiceType.EVENT) { + prompt = preparePromptForAsyncAPI(operation, contract, number); + } else if (service.getType() == ServiceType.GRPC) { + prompt = preparePromptForGrpc(service, operation, contract, number); + } + + log.debug("Asking OpenAI to suggest samples for this prompt: {}", prompt); + + final List messages = new ArrayList<>(); + final ChatMessage assistantMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), prompt); + messages.add(assistantMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder().model(model).messages(messages).n(1) + .maxTokens(maxTokens).logitBias(new HashMap<>()).build(); + + // Build a full HttpEntity as we need to specify authentication headers. + HttpEntity request = new HttpEntity<>(chatCompletionRequest, + createAuthenticationHeaders()); + ChatCompletionResult completionResult = restTemplate + .exchange(apiUrl + "/v1/chat/completions", HttpMethod.POST, request, ChatCompletionResult.class).getBody(); + + if (completionResult != null) { + ChatCompletionChoice choice = completionResult.getChoices().get(0); + log.debug("Got this raw output from OpenAI: {}", choice.getMessage().getContent()); + + if (service.getType() == ServiceType.EVENT) { + return AICopilotHelper.parseUnidirectionalEventTemplateOutput(choice.getMessage().getContent()); + } else { + return AICopilotHelper.parseRequestResponseTemplateOutput(service, operation, + choice.getMessage().getContent()); + } + } + // Return empty list. + return new ArrayList<>(); + } + + private String preparePromptForOpenAPI(Operation operation, Resource contract, int number) throws Exception { + StringBuilder prompt = new StringBuilder( + AICopilotHelper.getOpenAPIOperationPromptIntro(operation.getName(), number)); + + // Build a prompt reusing templates and elements from AICopilotHelper. + prompt.append("\n"); + prompt.append(AICopilotHelper.YAML_FORMATTING_PROMPT); + prompt.append("\n"); + prompt.append(AICopilotHelper.getRequestResponseExampleYamlFormattingDirective(number)); + prompt.append(SECTION_DELIMITER); + prompt.append(AICopilotHelper.removeTokensFromSpec(contract.getContent(), operation.getName())); + + return prompt.toString(); + } + + private String preparePromptForGraphQL(Operation operation, Resource contract, int number) { + StringBuilder prompt = new StringBuilder( + AICopilotHelper.getGraphQLOperationPromptIntro(operation.getName(), number)); + + // We need to indicate the name or variables we want. + if (DispatchStyles.QUERY_ARGS.equals(operation.getDispatcher())) { + StringBuilder variablesList = new StringBuilder(); + if (operation.getDispatcherRules().contains("&&")) { + String[] variables = operation.getDispatcherRules().split("&&"); + for (int i = 0; i < variables.length; i++) { + String variable = variables[i]; + variablesList.append("$").append(variable.trim()); + if (i < variables.length - 1) { + variablesList.append(", "); + } + } + } else { + variablesList.append("$").append(operation.getDispatcherRules()); + } + prompt.append("Use only '").append(variablesList).append("' as variable identifiers."); + } + + // Build a prompt reusing templates and elements from AICopilotHelper. + prompt.append("\n"); + prompt.append(AICopilotHelper.YAML_FORMATTING_PROMPT); + prompt.append("\n"); + prompt.append(AICopilotHelper.getRequestResponseExampleYamlFormattingDirective(number)); + prompt.append(SECTION_DELIMITER); + prompt.append(contract.getContent()); + + return prompt.toString(); + } + + private String preparePromptForAsyncAPI(Operation operation, Resource contract, int number) throws Exception { + StringBuilder prompt = new StringBuilder( + AICopilotHelper.getAsyncAPIOperationPromptIntro(operation.getName(), number)); + + // Build a prompt reusing templates and elements from AICopilotHelper. + prompt.append("\n"); + prompt.append(AICopilotHelper.YAML_FORMATTING_PROMPT); + prompt.append("\n"); + prompt.append(AICopilotHelper.getUnidirectionalEventExampleYamlFormattingDirective(number)); + prompt.append(SECTION_DELIMITER); + prompt.append(AICopilotHelper.removeTokensFromSpec(contract.getContent(), operation.getName())); + + return prompt.toString(); + } + + private String preparePromptForGrpc(Service service, Operation operation, Resource contract, int number) + throws Exception { + StringBuilder prompt = new StringBuilder( + AICopilotHelper.getGrpcOperationPromptIntro(service.getName(), operation.getName(), number)); + + // Build a prompt reusing templates and elements from AICopilotHelper. + prompt.append("\n"); + prompt.append(AICopilotHelper.YAML_FORMATTING_PROMPT); + prompt.append("\n"); + prompt.append(AICopilotHelper.getGrpcRequestResponseExampleYamlFormattingDirective(number)); + prompt.append(SECTION_DELIMITER); + prompt.append(contract.getContent()); + + return prompt.toString(); + } + + private MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(customObjectMapper()); + return converter; + } + + private static ObjectMapper customObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class); + return mapper; + } + + private HttpHeaders createAuthenticationHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + apiKey); + return headers; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPI3Importer.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPI3Importer.java new file mode 100644 index 000000000..c6d5ee02b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPI3Importer.java @@ -0,0 +1,432 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.asyncapi; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Metadata; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.AbstractJsonRepositoryImporter; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; +import io.github.microcks.util.ReferenceResolver; +import io.github.microcks.util.URIBuilder; +import io.github.microcks.util.metadata.MetadataExtensions; +import io.github.microcks.util.metadata.MetadataExtractor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static io.github.microcks.util.asyncapi.AsyncAPICommons.*; + +/** + * An implementation of MockRepositoryImporter that deals with AsyncAPI v3.0.x specification file ; whether encoding + * into JSON or YAML documents. + * @author laurent + */ +public class AsyncAPI3Importer extends AbstractJsonRepositoryImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(AsyncAPI3Importer.class); + + private static final List VALID_VERBS = Arrays.asList("send", "receive"); + + + /** + * Build a new importer. + * @param specificationFilePath The path to local AsyncAPI spec file + * @param referenceResolver An optional resolver for references present into the AsyncAPI file + * @throws IOException if project file cannot be found or read. + */ + public AsyncAPI3Importer(String specificationFilePath, ReferenceResolver referenceResolver) throws IOException { + super(specificationFilePath, referenceResolver); + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List result = new ArrayList<>(); + + // Build a new service. + Service service = new Service(); + + service.setName(rootSpecification.path("info").path("title").asText()); + service.setVersion(rootSpecification.path("info").path("version").asText()); + service.setType(ServiceType.EVENT); + + // Complete metadata if specified via extension. + if (rootSpecification.path("info").has(MetadataExtensions.MICROCKS_EXTENSION)) { + Metadata metadata = new Metadata(); + MetadataExtractor.completeMetadata(metadata, + rootSpecification.path("info").path(MetadataExtensions.MICROCKS_EXTENSION)); + service.setMetadata(metadata); + } + + // Before extraction operations, we need to get and build external reference if we have a resolver. + initializeReferencedResources(service); + + // Then build its operations. + service.setOperations(extractOperations()); + + result.add(service); + return result; + } + + @Override + public List getResourceDefinitions(Service service) { + List results = new ArrayList<>(); + + // Build a suitable name. + String name = service.getName() + "-" + service.getVersion(); + if (Boolean.TRUE.equals(isYaml)) { + name += ".yaml"; + } else { + name += ".json"; + } + + // Build a brand-new resource just with spec content. + Resource resource = new Resource(); + resource.setName(name); + resource.setType(ResourceType.ASYNC_API_SPEC); + results.add(resource); + // Set the content of main OpenAPI that may have been updated with normalized dependencies with initializeReferencedResources(). + resource.setContent(rootSpecificationContent); + + // Add the external resources that were imported during service discovery. + results.addAll(externalResources); + + return results; + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + List messageDefs = new ArrayList<>(); + + // Retrieve default content type, defaulting to application/json. + String defaultContentType = "application/json"; + if (rootSpecification.has("defaultContentType")) { + defaultContentType = rootSpecification.get("defaultContentType").asText("application/json"); + } + + // Iterate on specification "operations" nodes. + Iterator> operations = rootSpecification.path("operations").fields(); + while (operations.hasNext()) { + Map.Entry operationEntry = operations.next(); + JsonNode operationNode = operationEntry.getValue(); + + // Got to filter out for current operation only. + String action = operationNode.path("action").asText(); + String operationName = action.toUpperCase() + " " + operationEntry.getKey(); + + if (operationName.equals(operation.getName()) && operationNode.path(MESSAGES).isArray()) { + // Search for event messages. + List eventMessages = buildEventMessages(operationNode, defaultContentType); + + if (eventMessages != null && !eventMessages.isEmpty()) { + // Update dispatch information if necessary. + completeDispatchingCriteria(operation, operationNode, eventMessages); + + eventMessages.stream().forEach(eventMessage -> messageDefs.add(new UnidirectionalEvent(eventMessage))); + } + break; + } + } + return messageDefs; + } + + /** Extract the list of operations from Specification. */ + private List extractOperations() { + List results = new ArrayList<>(); + + // Iterate on specification "operations" nodes. + Iterator> operations = rootSpecification.path("operations").fields(); + while (operations.hasNext()) { + Map.Entry operationEntry = operations.next(); + String operationShortName = operationEntry.getKey(); + + JsonNode operationNode = operationEntry.getValue(); + String action = operationNode.path("action").asText(); + if (VALID_VERBS.contains(action)) { + // Build and add this operation to the list. + Operation operation = buildValidOperation(operationShortName, action, operationNode); + results.add(operation); + } + } + + return results; + } + + /** Build a single operation having its name, action and Json. */ + private Operation buildValidOperation(String name, String action, JsonNode operationNode) { + String operationName = action.toUpperCase() + " " + name; + + Operation operation = new Operation(); + operation.setName(operationName); + operation.setMethod(action.toUpperCase()); + + // Complete operation properties if any. + if (operationNode.has(MetadataExtensions.MICROCKS_OPERATION_EXTENSION)) { + MetadataExtractor.completeOperationProperties(operation, + operationNode.path(MetadataExtensions.MICROCKS_OPERATION_EXTENSION)); + } + + // Look for bindings at the operation level. + if (operationNode.has(BINDINGS)) { + AsyncAPICommons.completeOperationLevelBindings(operation, operationNode.get(BINDINGS)); + } + + // Then, check the related channels and extract dispatching info and bindings. + if (operationNode.path(CHANNEL_NODE).isObject()) { + JsonNode channel = operationNode.get(CHANNEL_NODE); + JsonNode channelNode = followRefIfAny(channel); + + if (channelNode.has(ADDRESS_NODE)) { + String address = channelNode.get(ADDRESS_NODE).asText(); + if (AsyncAPICommons.channelAddressHasParts(address)) { + operation.setDispatcher(DispatchStyles.URI_PARTS); + operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromStringPattern(address)); + } + operation.addResourcePath(address); + } + + if (channelNode.has(BINDINGS)) { + AsyncAPICommons.completeChannelLevelBindings(operation, channelNode.get(BINDINGS)); + } + } + + // Then, check the related messages and complete bindings. + if (operationNode.path(MESSAGES).isArray()) { + Iterator messages = operationNode.path(MESSAGES).elements(); + while (messages.hasNext()) { + JsonNode messageInChannelNode = followRefIfAny(messages.next()); + JsonNode messageNode = followRefIfAny(messageInChannelNode); + if (messageNode.has(BINDINGS)) { + AsyncAPICommons.completeMessageLevelBindings(operation, messageNode.get(BINDINGS)); + } + } + } + + return operation; + } + + /** If necessary, complete an operation and its messages dispatch information. */ + private void completeDispatchingCriteria(Operation operation, JsonNode operationNode, + List eventMessages) { + // Update dispatch information if necessary. + if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher())) { + + // Retrieve information on channel address and parameters. + JsonNode channelNode = followRefIfAny(operationNode.get(CHANNEL_NODE)); + String address = channelNode.get(ADDRESS_NODE).asText(); + + // We have 2 cases here: parameter value can be dynamic, expressed with a location in message + // or parameter value can be static, expressed as an examples. + List dynamicParameters = getDynamicParameters(channelNode); + Map> parametersByMessage = getParametersByMessage(channelNode); + ObjectMapper mapper = getObjectMapper(true); + + for (EventMessage eventMessage : eventMessages) { + // Start initializing message parameter values with the static ones. + Map parameterValues = parametersByMessage.getOrDefault(eventMessage.getName(), + new HashMap<>()); + + // Extract each dynamic parameter with its value coming from message. + for (AsyncAPIParameter parameter : dynamicParameters) { + String parameterValue = null; + + try { + if (parameter.location().startsWith("$message.payload#")) { + String location = parameter.location().substring("$message.payload#".length()); + JsonNode eventMessageRootNode = mapper.readTree(eventMessage.getContent()); + parameterValue = eventMessageRootNode.at(location).asText(); + } + } catch (Exception e) { + log.warn("Failed to extract the location value in {} from message {} for operation {}", + parameter.location(), eventMessage.getName(), operation.getName()); + log.warn("Pursuing with the other ones but dispatch will be incomplete"); + } + if (parameterValue != null) { + parameterValues.put(parameter.name(), parameterValue); + } + } + + // UPdate operation resource paths and message dispatch criteria. + String resourcePath = URIBuilder.buildURIFromPattern(address, parameterValues); + operation.addResourcePath(resourcePath); + eventMessage.setDispatchCriteria(DispatchCriteriaHelper.buildFromPartsMap(address, parameterValues)); + } + } + } + + /** Given a Channel Json node, get its dynamic parameter definitions (those having a location). */ + private List getDynamicParameters(JsonNode channelNode) { + List results = new ArrayList<>(); + + if (channelNode.path(PARAMETERS_NODE).isObject()) { + Iterator> parameters = channelNode.get(PARAMETERS_NODE).fields(); + while (parameters.hasNext()) { + Entry parameterEntry = parameters.next(); + JsonNode parameter = followRefIfAny(parameterEntry.getValue()); + + String parameterName = parameterEntry.getKey(); + + if (parameter.has(LOCATION_NODE)) { + if (log.isDebugEnabled()) { + log.debug("Processing param {} for channel {}, with location {}", parameterName, + channelNode.get("address").asText(), parameter.get(LOCATION_NODE).asText()); + } + results.add(new AsyncAPIParameter(parameterName, parameter.get(LOCATION_NODE).asText())); + } + } + } + return results; + } + + /** + * Given a Channel Json node, get its static parameter values (those without a location), organized by message + * example name. Key of value map is parameter name. Value of value map is parameter value ;-) + */ + private Map> getParametersByMessage(JsonNode channelNode) { + Map> results = new HashMap<>(); + + if (channelNode.path(PARAMETERS_NODE).isObject()) { + Iterator> parameters = channelNode.get(PARAMETERS_NODE).fields(); + while (parameters.hasNext()) { + Entry parameterEntry = parameters.next(); + JsonNode parameter = followRefIfAny(parameterEntry.getValue()); + String parameterName = parameterEntry.getKey(); + if (!parameter.has(LOCATION_NODE) && parameter.path(EXAMPLES_NODE).isArray()) { + Iterator examples = parameter.get(EXAMPLES_NODE).elements(); + while (examples.hasNext()) { + String example = examples.next().asText(); + if (example.contains(":")) { + String exampleKey = example.substring(0, example.indexOf(":")); + String exampleValue = example.substring(example.indexOf(":") + 1); + + if (log.isDebugEnabled()) { + log.debug("Processing param {} for channel {} for message {}", parameterName, + channelNode.get("address").asText(), exampleKey); + } + + Map exampleParams = results.getOrDefault(exampleKey, new HashMap<>()); + exampleParams.put(parameterName, exampleValue); + results.put(exampleKey, exampleParams); + } + } + } + } + } + return results; + } + + + /** Build a list of EventMessages from an operation Json node. */ + private List buildEventMessages(JsonNode operationNode, String defaultContentType) { + List eventMessages = null; + Iterator messages = operationNode.path(MESSAGES).elements(); + while (messages.hasNext()) { + JsonNode operationMessageNode = messages.next(); + JsonNode messageInChannelNode = followRefIfAny(operationMessageNode); + JsonNode messageNode = followRefIfAny(messageInChannelNode); + + // Get message content type. + String contentType = defaultContentType; + if (messageNode.has("contentType")) { + contentType = messageNode.path("contentType").asText(); + } + + // Retrieve the messageName from message ref found in operation. + String messageName = operationMessageNode.path("$ref").textValue(); + + if (messageName != null && messageNode.has(EXAMPLES_NODE)) { + // Compute a short message name if examples have no name attribute. + messageName = messageName.substring(messageName.lastIndexOf("/") + 1); + eventMessages = buildEventMessageFromExamples(messageName, contentType, messageNode.get(EXAMPLES_NODE)); + } + } + return eventMessages; + } + + /** Build a list of EventMessages from a Message examples Json node. */ + private List buildEventMessageFromExamples(String messageName, String contentType, + JsonNode examplesNode) { + List exchanges = new ArrayList<>(); + + Iterator examples = examplesNode.elements(); + while (examples.hasNext()) { + JsonNode exampleNode = examples.next(); + + EventMessage eventMessage = new EventMessage(); + // Use name attribute if present, otherwise generate from message name. + if (exampleNode.has("name")) { + eventMessage.setName(exampleNode.get("name").asText()); + } else { + eventMessage.setName(messageName + "-" + (exchanges.size() + 1)); + } + + eventMessage.setMediaType(contentType); + eventMessage.setContent(getExamplePayload(exampleNode)); + + // Now complete with specified headers. + List
headers = AsyncAPICommons.getExampleHeaders(exampleNode); + for (Header header : headers) { + eventMessage.addHeader(header); + } + + exchanges.add(eventMessage); + } + return exchanges; + } + + /** + * Get the value of an example. This can be direct value field or those of followed $ref. + */ + private String getExamplePayload(JsonNode example) { + if (example.has(EXAMPLE_PAYLOAD_NODE)) { + return getValueString(example.path(EXAMPLE_PAYLOAD_NODE)); + } + if (example.has("$payloadRef")) { + // $ref: '#/components/examples/param_laurent' + String ref = example.path("$payloadRef").asText(); + JsonNode component = rootSpecification.at(ref.substring(1)); + return getExamplePayload(component); + } + return null; + } + + private record AsyncAPIParameter(String name, String location) { + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPICommons.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPICommons.java new file mode 100644 index 000000000..5eae9c94e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPICommons.java @@ -0,0 +1,232 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.asyncapi; + +import io.github.microcks.domain.Binding; +import io.github.microcks.domain.BindingType; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Utility class that holds constants definition found in AsyncAPI v2 and v3 specifications as well as utility methods + * for discovering bindings and examples. + * @author laurent + */ +public class AsyncAPICommons { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(AsyncAPICommons.class); + + public static final String MESSAGES = "messages"; + public static final String BINDINGS = "bindings"; + + public static final String SCHEMA_NODE = "schema"; + public static final String CHANNEL_NODE = "channel"; + public static final String ADDRESS_NODE = "address"; + public static final String PARAMETERS_NODE = "parameters"; + public static final String LOCATION_NODE = "location"; + public static final String EXAMPLES_NODE = "examples"; + public static final String EXAMPLE_VALUE_NODE = "value"; + public static final String EXAMPLE_PAYLOAD_NODE = "payload"; + public static final String EXAMPLE_HEADERS_NODE = "headers"; + public static final String QUEUE_VALUE = "queue"; + public static final String TOPIC_VALUE = "topic"; + + private AsyncAPICommons() { + // Private constructor to hide the implicit one as it's a utility class. + } + + /** + * Browse and complete an operation bindings with the bindings information at Channel level. + * @param operation The operation whose bindings should be completed + * @param bindings The Channel level bindings node + */ + public static void completeChannelLevelBindings(Operation operation, JsonNode bindings) { + Iterator bindingNames = bindings.fieldNames(); + while (bindingNames.hasNext()) { + String bindingName = bindingNames.next(); + JsonNode bindingNode = bindings.path(bindingName); + + switch (bindingName) { + case "ws": + Binding b = retrieveOrInitOperationBinding(operation, BindingType.WS); + b.setMethod(bindingNode.path("method").asText(null)); + break; + case "amqp": + b = retrieveOrInitOperationBinding(operation, BindingType.AMQP); + if (bindingNode.has("is")) { + String is = bindingNode.path("is").asText(); + if (QUEUE_VALUE.equals(is)) { + b.setDestinationType(QUEUE_VALUE); + JsonNode queue = bindingNode.get(QUEUE_VALUE); + b.setDestinationName(queue.get("name").asText()); + } else if ("routingKey".equals(is)) { + JsonNode exchange = bindingNode.get("exchange"); + b.setDestinationType(exchange.get("type").asText()); + } + } + break; + case "googlepubsub": + b = retrieveOrInitOperationBinding(operation, BindingType.GOOGLEPUBSUB); + b.setDestinationName(bindingNode.path(TOPIC_VALUE).asText(null)); + b.setPersistent(bindingNode.path("messageRetentionDuration").asBoolean(false)); + break; + default: + break; + } + } + } + + /** + * Browse and complete an operation bindings with the bindings information at Operation level. + * @param operation The operation whose bindings should be completed + * @param bindings The Operation level bindings node + */ + public static void completeOperationLevelBindings(Operation operation, JsonNode bindings) { + Iterator bindingNames = bindings.fieldNames(); + while (bindingNames.hasNext()) { + String bindingName = bindingNames.next(); + JsonNode bindingNode = bindings.path(bindingName); + + switch (bindingName) { + case "kafka": + break; + case "mqtt": + Binding b = retrieveOrInitOperationBinding(operation, BindingType.MQTT); + b.setQoS(bindingNode.path("qos").asText(null)); + b.setPersistent(bindingNode.path("retain").asBoolean(false)); + break; + case "amqp1": + b = retrieveOrInitOperationBinding(operation, BindingType.AMQP1); + b.setDestinationName(bindingNode.path("destinationName").asText(null)); + b.setDestinationType(bindingNode.path("destinationType").asText(null)); + break; + case "nats": + b = retrieveOrInitOperationBinding(operation, BindingType.NATS); + b.setDestinationName(bindingNode.path(QUEUE_VALUE).asText(null)); + break; + case "sqs": + b = retrieveOrInitOperationBinding(operation, BindingType.SQS); + if (bindingNode.has(QUEUE_VALUE)) { + b.setDestinationName(bindingNode.get(QUEUE_VALUE).path("name").asText(null)); + b.setPersistent(bindingNode.path("messageRetentionPeriod").asBoolean(false)); + } + break; + case "sns": + b = retrieveOrInitOperationBinding(operation, BindingType.SNS); + if (bindingNode.has(TOPIC_VALUE) && bindingNode.get(TOPIC_VALUE).has("name")) { + b.setDestinationName(bindingNode.get(TOPIC_VALUE).path("name").asText(null)); + } + break; + default: + break; + } + } + } + + /** + * Browse and complete an operation bindings with the bindings information at Message level. + * @param operation The operation whose bindings should be completed + * @param bindings The Message level bindings node + */ + public static void completeMessageLevelBindings(Operation operation, JsonNode bindings) { + Iterator bindingNames = bindings.fieldNames(); + while (bindingNames.hasNext()) { + String bindingName = bindingNames.next(); + JsonNode bindingNode = bindings.path(bindingName); + + switch (bindingName) { + case "kafka": + Binding b = retrieveOrInitOperationBinding(operation, BindingType.KAFKA); + if (bindingNode.has("key")) { + b.setKeyType(bindingNode.path("key").path("type").asText()); + } + break; + default: + break; + } + } + } + + /** Check variables parts presence into given channel address. */ + public static boolean channelAddressHasParts(String address) { + return (address.indexOf("{") != -1); + } + + /** Extract the list of Header from an example node. */ + public static List
getExampleHeaders(JsonNode example) { + List
results = new ArrayList<>(); + + if (example.has(EXAMPLE_HEADERS_NODE)) { + Iterator> headers = null; + + if (example.path(EXAMPLE_HEADERS_NODE).isObject()) { + headers = example.path(EXAMPLE_HEADERS_NODE).fields(); + } else if (example.path(EXAMPLE_HEADERS_NODE).isTextual()) { + // Try to parse string as a JSON Object... + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode headersNode = mapper.readTree(example.path(EXAMPLE_HEADERS_NODE).asText()); + headers = headersNode.fields(); + } catch (Exception e) { + log.warn("Headers value {} is a string but not JSON, skipping it", + example.path(EXAMPLE_HEADERS_NODE).asText()); + } + } + + if (headers != null) { + while (headers.hasNext()) { + Map.Entry property = headers.next(); + + Header header = new Header(); + header.setName(property.getKey()); + // Values may be multiple and CSV. + Set headerValues = Arrays.stream(property.getValue().asText().split(",")).map(String::trim) + .collect(Collectors.toSet()); + header.setValues(headerValues); + results.add(header); + } + } + } + return results; + } + + /** Get existing operation binding type or initialize a new one. */ + private static Binding retrieveOrInitOperationBinding(Operation operation, BindingType type) { + Binding binding = null; + if (operation.getBindings() != null) { + binding = operation.getBindings().get(type.toString()); + } + if (binding == null) { + binding = new Binding(type); + operation.addBinding(type.toString(), binding); + } + return binding; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPIImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPIImporter.java new file mode 100644 index 000000000..bca1f1490 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPIImporter.java @@ -0,0 +1,584 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.asyncapi; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Metadata; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.AbstractJsonRepositoryImporter; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; +import io.github.microcks.util.ReferenceResolver; +import io.github.microcks.util.URIBuilder; +import io.github.microcks.util.metadata.MetadataExtensions; +import io.github.microcks.util.metadata.MetadataExtractor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static io.github.microcks.util.asyncapi.AsyncAPICommons.*; + +/** + * An implementation of MockRepositoryImporter that deals with AsyncAPI v2.0.x specification file ; whether encoding + * into JSON or YAML documents. + * @author laurent + */ +public class AsyncAPIImporter extends AbstractJsonRepositoryImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(AsyncAPIImporter.class); + + private static final String[] MULTI_STRUCTURES = { "allOf", "anyOf", "oneOf" }; + private static final List VALID_VERBS = Arrays.asList("subscribe", "publish"); + + /** + * Build a new importer. + * @param specificationFilePath The path to local AsyncAPI spec file + * @param referenceResolver An optional resolver for references present into the AsyncAPI file + * @throws IOException if project file cannot be found or read. + */ + public AsyncAPIImporter(String specificationFilePath, ReferenceResolver referenceResolver) throws IOException { + super(specificationFilePath, referenceResolver); + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List result = new ArrayList<>(); + + // Build a new service. + Service service = new Service(); + + service.setName(rootSpecification.path("info").path("title").asText()); + service.setVersion(rootSpecification.path("info").path("version").asText()); + service.setType(ServiceType.EVENT); + + // Complete metadata if specified via extension. + if (rootSpecification.path("info").has(MetadataExtensions.MICROCKS_EXTENSION)) { + Metadata metadata = new Metadata(); + MetadataExtractor.completeMetadata(metadata, + rootSpecification.path("info").path(MetadataExtensions.MICROCKS_EXTENSION)); + service.setMetadata(metadata); + } + + // Then build its operations. + service.setOperations(extractOperations()); + + result.add(service); + return result; + } + + @Override + public List getResourceDefinitions(Service service) { + List results = new ArrayList<>(); + Map resolvedExternalRefResources = new HashMap<>(); + + // Build a suitable name. + String name = service.getName() + "-" + service.getVersion(); + if (Boolean.TRUE.equals(isYaml)) { + name += ".yaml"; + } else { + name += ".json"; + } + + // Build a brand-new resource just with spec content. + Resource resource = new Resource(); + resource.setName(name); + resource.setType(ResourceType.ASYNC_API_SPEC); + results.add(resource); + + // Browser operations messages and message traits to see if we have + // references for external schemas only if we have a resolver available. + if (referenceResolver != null) { + for (Operation operation : service.getOperations()) { + String[] operationElements = operation.getName().split(" "); + String messageNamePtr = "/channels/" + operationElements[1].replace("/", "~1"); + messageNamePtr += "/" + operationElements[0].toLowerCase() + "/message"; + + JsonNode messageNode = rootSpecification.at(messageNamePtr); + if (messageNode != null) { + // If it's a $ref, then navigate to it. + messageNode = followRefIfAny(messageNode); + + // Extract payload schema here. + if (messageNode.has(EXAMPLE_PAYLOAD_NODE)) { + JsonNode payloadNode = messageNode.path(EXAMPLE_PAYLOAD_NODE); + + // Check we have a reference that is not a local one. + if (payloadNode.has("$ref")) { + + Set references = new HashSet<>(); + findAllExternalRefsAsyncAPI(payloadNode, references); + + for (String ref : references) { + // We may have already resolved it if referenced more than once. + Resource schemaResource = resolvedExternalRefResources.get(ref); + if (schemaResource == null) { + try { + // Remove trailing anchor marker (we may have this in Avro schema to point exact + // Resource) + if (ref.contains("#")) { + ref = ref.substring(0, ref.indexOf("#")); + } + + // Extract content using resolver. + String content = referenceResolver.getReferenceContent(ref, StandardCharsets.UTF_8); + String resourceName = ref.substring(ref.lastIndexOf('/') + 1); + + // Build a new resource from content. Use the escaped operation path. + schemaResource = new Resource(); + schemaResource.setName(IdBuilder.buildResourceFullName(service, resourceName)); + schemaResource.setPath(ref); + schemaResource.setContent(content); + + // We have to look at schema format to know the type. + if (messageNode.has("schemaFormat")) { + String schemaFormat = messageNode.path("schemaFormat").asText(); + + if (schemaFormat.startsWith("application/vnd.aai.asyncapi")) { + schemaResource.setType(ResourceType.ASYNC_API_SCHEMA); + } else if (schemaFormat.startsWith("application/vnd.oai.openapi")) { + schemaResource.setType(ResourceType.OPEN_API_SCHEMA); + } else if (schemaFormat.startsWith("application/schema+json") + || schemaFormat.startsWith("application/schema+yaml")) { + schemaResource.setType(ResourceType.JSON_SCHEMA); + } else if (schemaFormat.startsWith("application/vnd.apache.avro")) { + schemaResource.setType(ResourceType.AVRO_SCHEMA); + } + } else { + // We should probably go deeper here and inspect the content of resolved + // resource + // to actually get the real schema type... + schemaResource.setType(ResourceType.JSON_SCHEMA); + } + + if (!ref.startsWith("http")) { + // If a relative resource, replace with new name. + rootSpecificationContent = rootSpecificationContent.replace(ref, + URLEncoder.encode(schemaResource.getName(), "UTF-8")); + } + + results.add(schemaResource); + } catch (IOException ioe) { + log.error("IOException while trying to resolve reference " + ref, ioe); + log.info("Ignoring the reference {} cause it could not be resolved", ref); + } + // Mark it as resolved. + resolvedExternalRefResources.put(ref, schemaResource); + } + + if (schemaResource != null) { + schemaResource.addOperation(operation.getName()); + } else { + log.warn("Cannot add operation because schema resourse is null"); + } + } + } + } + } + } + // Finally try to clean up resolved references and associated resources (files) + referenceResolver.cleanResolvedReferences(); + } + // Set the content of main AsyncAPI that may have been updated with dereferenced + // dependencies. + resource.setContent(rootSpecificationContent); + + return results; + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + List result = new ArrayList<>(); + + // Retrieve default content type, defaulting to application/json. + String defaultContentType = "application/json"; + if (rootSpecification.has("defaultContentType")) { + defaultContentType = rootSpecification.get("defaultContentType").asText("application/json"); + } + + // Iterate on specification "channels" nodes. + Iterator> channels = rootSpecification.path("channels").fields(); + while (channels.hasNext()) { + Entry channel = channels.next(); + String channelName = channel.getKey(); + Map> pathParametersByExample = extractParametersByExample(channel.getValue()); + + // Iterate on specification path, "verbs" nodes. + Iterator> verbs = channel.getValue().fields(); + while (verbs.hasNext()) { + Entry verb = verbs.next(); + String verbName = verb.getKey(); + + // Find the correct operation. + if (operation.getName().equals(verbName.toUpperCase() + " " + channelName.trim())) { + JsonNode messageBody = verb.getValue().path("message"); + + // If it's a $ref or multi-structure (oneOf, anyOf, allOf), then navigate to + // them. + List messageBodies = followRefsIfAny(messageBody); + + for (JsonNode extractedMsgBody : messageBodies) { + // Get message content type. + String contentType = defaultContentType; + if (extractedMsgBody.has("contentType")) { + contentType = extractedMsgBody.path("contentType").asText(); + } + // No need to go further if no examples. + if (extractedMsgBody.has(EXAMPLES_NODE)) { + Iterator examples = extractedMsgBody.path(EXAMPLES_NODE).elements(); + int exampleIndex = 0; + while (examples.hasNext()) { + JsonNode exampleNode = examples.next(); + + EventMessage eventMessage = null; + if (exampleNode.has("name")) { + // As of AsyncAPI 2.1.0 () we can now have a 'name' property for examples! + eventMessage = extractFromAsyncAPI21Example(contentType, exampleNode); + } else if (exampleNode.has(EXAMPLE_PAYLOAD_NODE)) { + // As of https://github.com/microcks/microcks/issues/385, we should support the restriction + // coming from AsyncAPI GItHub master revision and associated tooling... + eventMessage = extractFromAsyncAPIExample(contentType, exampleNode, + channelName.trim() + "-" + exampleIndex); + } else { + eventMessage = extractFromMicrocksExample(contentType, exampleNode); + } + // If import succeed, deal with the dispatching criteria stuffs and + // add this event message as a valid event in results exchanges. + if (eventMessage != null) { + if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher())) { + String resourcePathPattern = channelName; + Map parts = pathParametersByExample.get(eventMessage.getName()); + String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parts); + operation.addResourcePath(resourcePath); + eventMessage.setDispatchCriteria( + DispatchCriteriaHelper.buildFromPartsMap(operation.getDispatcherRules(), parts)); + } + + result.add(new UnidirectionalEvent(eventMessage)); + } + exampleIndex++; + } + } + } + } + } + } + return result; + } + + /** Extract the list of operations from Specification. */ + private List extractOperations() { + List results = new ArrayList<>(); + + // Iterate on specification "channels" nodes. + Iterator> channels = rootSpecification.path("channels").fields(); + while (channels.hasNext()) { + Entry channel = channels.next(); + String channelName = channel.getKey(); + + // Iterate on specification path, "verbs" nodes. + Iterator> verbs = channel.getValue().fields(); + while (verbs.hasNext()) { + Entry verb = verbs.next(); + String verbName = verb.getKey(); + + // Only deal with real verbs for now. + if (VALID_VERBS.contains(verbName)) { + String operationName = verbName.toUpperCase() + " " + channelName.trim(); + + Operation operation = new Operation(); + operation.setName(operationName); + operation.setMethod(verbName.toUpperCase()); + + // Complete operation properties if any. + if (verb.getValue().has(MetadataExtensions.MICROCKS_OPERATION_EXTENSION)) { + MetadataExtractor.completeOperationProperties(operation, + verb.getValue().path(MetadataExtensions.MICROCKS_OPERATION_EXTENSION)); + } + + // Deal with dispatcher stuffs. + if (operation.getDispatcher() == null && channelHasParts(channelName)) { + operation.setDispatcher(DispatchStyles.URI_PARTS); + operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(channelName)); + } else { + operation.addResourcePath(channelName); + } + + // We have to look also for bindings. First at the upper channel level. + if (channel.getValue().has(BINDINGS)) { + AsyncAPICommons.completeChannelLevelBindings(operation, channel.getValue().get(BINDINGS)); + } + + // Then look for bindings at the operation level. + if (verb.getValue().has(BINDINGS)) { + AsyncAPICommons.completeOperationLevelBindings(operation, verb.getValue().get(BINDINGS)); + } + + // Then look for bindings at the message level. + JsonNode messageBody = verb.getValue().path("message"); + messageBody = followRefIfAny(messageBody); + if (messageBody.has(BINDINGS)) { + AsyncAPICommons.completeMessageLevelBindings(operation, messageBody.get(BINDINGS)); + } + + results.add(operation); + } + } + } + + return results; + } + + /** Browse Json node to extract references and store them into externalRefs. */ + private void findAllExternalRefsAsyncAPI(JsonNode node, Set externalRefs) { + // If node as a $ref child, it's a stop condition. + if (node.has("$ref")) { + String ref = node.path("$ref").asText(); + if (!ref.startsWith("#")) { + externalRefs.add(ref); + } else { + findAllExternalRefsAsyncAPI(followRefIfAny(node), externalRefs); + } + } else { + // Iterate on all other children. + Iterator children = node.elements(); + while (children.hasNext()) { + findAllExternalRefsAsyncAPI(children.next(), externalRefs); + } + } + } + + /** Extract example using the AsyncAPI 2.1 new 'name' property. */ + private EventMessage extractFromAsyncAPI21Example(String contentType, JsonNode exampleNode) { + // Retrieve name & payload value. + String exampleName = exampleNode.path("name").asText(); + String exampleValue = getExamplePayload(exampleNode); + + // Build and store a request object. + EventMessage eventMessage = new EventMessage(); + eventMessage.setName(exampleName); + eventMessage.setContent(exampleValue); + eventMessage.setMediaType(contentType); + + // Now complete with specified headers. + List
headers = AsyncAPICommons.getExampleHeaders(exampleNode); + for (Header header : headers) { + eventMessage.addHeader(header); + } + + return eventMessage; + } + + /** Extract example using the AsyncAPI master branch restrictions. */ + private EventMessage extractFromAsyncAPIExample(String contentType, JsonNode exampleNode, String exampleName) { + // Retrieve payload value. + String exampleValue = getExamplePayload(exampleNode); + + // Build and store a request object. + EventMessage eventMessage = new EventMessage(); + eventMessage.setName(exampleName); + eventMessage.setContent(exampleValue); + eventMessage.setMediaType(contentType); + + // Now complete with specified headers. + List
headers = AsyncAPICommons.getExampleHeaders(exampleNode); + for (Header header : headers) { + eventMessage.addHeader(header); + } + + return eventMessage; + } + + /** Extract example using the Microcks (and Apicurio) extended notation. */ + private EventMessage extractFromMicrocksExample(String contentType, JsonNode exampleNode) { + EventMessage eventMessage = null; + + Iterator exampleNames = exampleNode.fieldNames(); + while (exampleNames.hasNext()) { + String exampleName = exampleNames.next(); + JsonNode example = exampleNode.path(exampleName); + + // No need to go further if no payload. + if (example.has(EXAMPLE_PAYLOAD_NODE)) { + String exampleValue = getExamplePayload(example); + + // Build and store a request object. + eventMessage = new EventMessage(); + eventMessage.setName(exampleName); + eventMessage.setContent(exampleValue); + eventMessage.setMediaType(contentType); + + // Now complete with specified headers. + List
headers = AsyncAPICommons.getExampleHeaders(example); + for (Header header : headers) { + eventMessage.addHeader(header); + } + } + } + return eventMessage; + } + + /** + * Get the value of an example. This can be direct value field or those of followed $ref. + */ + private String getExamplePayload(JsonNode example) { + if (example.has(EXAMPLE_PAYLOAD_NODE)) { + return getValueString(example.path(EXAMPLE_PAYLOAD_NODE)); + } + if (example.has("$payloadRef")) { + // $ref: '#/components/examples/param_laurent' + String ref = example.path("$payloadRef").asText(); + JsonNode component = rootSpecification.at(ref.substring(1)); + return getExamplePayload(component); + } + return null; + } + + /** Check variables parts presence into given channel. */ + private static boolean channelHasParts(String channel) { + return (channel.indexOf("/{") != -1); + } + + /** + * Extract parameters within a channel node and organize them by example. Key of value map is param name. Value of + * value map is param value ;-) + */ + private Map> extractParametersByExample(JsonNode node) { + Map> results = new HashMap<>(); + Iterator> parameters = node.path("parameters").fields(); + while (parameters.hasNext()) { + Entry parameterEntry = parameters.next(); + JsonNode parameter = parameterEntry.getValue(); + + // If parameter is a $ref, navigate to it first. + parameter = followRefIfAny(parameter); + + String parameterName = parameterEntry.getKey(); + log.debug("Processing param {}", parameterName); + + if (parameter.has(SCHEMA_NODE) && parameter.path(SCHEMA_NODE).has(EXAMPLES_NODE)) { + JsonNode examplesNode = parameter.path(SCHEMA_NODE).path(EXAMPLES_NODE); + + if (examplesNode.isObject()) { + Iterator exampleNames = parameter.path(SCHEMA_NODE).path(EXAMPLES_NODE).fieldNames(); + + while (exampleNames.hasNext()) { + String exampleName = exampleNames.next(); + log.debug("Processing example {}", exampleName); + + JsonNode example = parameter.path(SCHEMA_NODE).path(EXAMPLES_NODE).path(exampleName); + String exampleValue = getExampleValue(example); + log.debug("{} {} {}", parameterName, exampleName, exampleValue); + + Map exampleParams = results.computeIfAbsent(exampleName, k -> new HashMap<>()); + exampleParams.put(parameterName, exampleValue); + } + } else if (examplesNode.isArray()) { + Iterator examples = examplesNode.elements(); + while (examples.hasNext()) { + JsonNode exampleNode = examples.next(); + + String exampleName = null; + String exampleValue = null; + + // Try processing the "name:value" form + if (exampleNode.asText().contains(":")) { + String example = exampleNode.asText(); + exampleName = example.substring(0, example.indexOf(":")); + exampleValue = example.substring(example.indexOf(":") + 1); + } else { + // Try processing the "name: > value: value" form + exampleName = exampleNode.fieldNames().next(); + exampleValue = getExampleValue(exampleNode.fields().next().getValue()); + } + log.debug("{} {} {}", parameterName, exampleName, exampleValue); + + Map exampleParams = results.computeIfAbsent(exampleName, k -> new HashMap<>()); + exampleParams.put(parameterName, exampleValue); + } + } + } + } + return results; + } + + /** + * Get the value of an example. This can be direct value field or those of followed $ref + */ + private String getExampleValue(JsonNode example) { + if (example.has(EXAMPLE_VALUE_NODE)) { + JsonNode valueNode = followRefIfAny(example.path(EXAMPLE_VALUE_NODE)); + return getValueString(valueNode); + } + if (example.has("$ref")) { + JsonNode component = followRefIfAny(example); + return getExampleValue(component); + } + return null; + } + + /** */ + private List followRefsIfAny(JsonNode referencableNode) { + List results = new ArrayList<>(); + if (referencableNode.has("$ref")) { + // Extract single reference. + String ref = referencableNode.path("$ref").asText(); + results.add(rootSpecification.at(ref.substring(1))); + } else { + // Check for multi-structures. + for (String structure : MULTI_STRUCTURES) { + if (referencableNode.has(structure) && referencableNode.path(structure).isArray()) { + ArrayNode arrayNode = (ArrayNode) referencableNode.path(structure); + for (int i = 0; i < arrayNode.size(); i++) { + JsonNode structureNode = arrayNode.get(i); + results.add(followRefIfAny(structureNode)); + } + } + } + } + // If no reference found, put the node itself. + if (results.isEmpty()) { + results.add(referencableNode); + } + return results; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPITestRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPITestRunner.java new file mode 100644 index 000000000..9c7116ff8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/asyncapi/AsyncAPITestRunner.java @@ -0,0 +1,150 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.asyncapi; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.domain.TestRunnerType; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.SecretRepository; +import io.github.microcks.util.test.AbstractTestRunner; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +/** + * This is an implementation of HttpTestRunner that deals with AsyncAPI schema testing. It delegates the consumption of + * asynchronous messages and the actual validation to the microcks-async-minion component, triggering it through + * an API call. + * @author laurent + */ +public class AsyncAPITestRunner extends AbstractTestRunner { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(AsyncAPITestRunner.class); + + private ObjectMapper mapper = new ObjectMapper(); + + private ResourceRepository resourceRepository; + + private SecretRepository secretRepository; + + private ClientHttpRequestFactory clientHttpRequestFactory; + + private String asyncMinionUrl = null; + + /** + * Build a new AsyncAPITestRunner using a resource repository for retrieving AsyncAPI specification. + * @param resourceRepository The repository that contains AsyncAPI specification to validate + * @param secretRepository The repository for accessing secrets for connecting test endpoints + */ + public AsyncAPITestRunner(ResourceRepository resourceRepository, SecretRepository secretRepository) { + this.resourceRepository = resourceRepository; + this.secretRepository = secretRepository; + } + + /** + * Set the ClientHttpRequestFactory used for reaching endpoint. + * @param clientHttpRequestFactory The ClientHttpRequestFactory used for reaching endpoint + */ + public void setClientHttpRequestFactory(ClientHttpRequestFactory clientHttpRequestFactory) { + this.clientHttpRequestFactory = clientHttpRequestFactory; + } + + public void setAsyncMinionUrl(String asyncMinionUrl) { + this.asyncMinionUrl = asyncMinionUrl; + } + + @Override + public List runTest(Service service, Operation operation, TestResult testResult, List requests, + String endpointUrl, HttpMethod method) throws URISyntaxException, IOException { + + if (log.isDebugEnabled()) { + log.debug("Launching test run on " + endpointUrl + " for ms"); + } + + // Retrieve the resource corresponding to OpenAPI specification if any. + Resource asyncAPISpecResource = null; + List resources = resourceRepository.findByServiceId(service.getId()); + for (Resource resource : resources) { + if (ResourceType.ASYNC_API_SPEC.equals(resource.getType())) { + asyncAPISpecResource = resource; + break; + } + } + + // Microcks-async-minion interface object building. + ObjectNode jsonArg = mapper.createObjectNode(); + jsonArg.put("runnerType", TestRunnerType.ASYNC_API_SCHEMA.toString()); + jsonArg.put("testResultId", testResult.getId()); + jsonArg.put("serviceId", service.getId()); + jsonArg.put("operationName", operation.getName()); + jsonArg.put("endpointUrl", endpointUrl); + jsonArg.put("timeoutMS", testResult.getTimeout()); + jsonArg.put("asyncAPISpec", asyncAPISpecResource.getContent()); + + if (testResult.getSecretRef() != null) { + Secret secret = secretRepository.findById(testResult.getSecretRef().getSecretId()).orElse(null); + if (secret != null) { + log.debug("Adding the secret '{}' to test specification request", secret.getName()); + jsonArg.set("secret", mapper.valueToTree(secret)); + } + } + + URI asyncMinionURI = new URI(asyncMinionUrl + "/api/tests"); + ClientHttpRequest httpRequest = clientHttpRequestFactory.createRequest(asyncMinionURI, HttpMethod.POST); + httpRequest.getBody().write(mapper.writeValueAsBytes(jsonArg)); + httpRequest.getHeaders().add("Content-Type", "application/json"); + + // Actually execute request. + ClientHttpResponse httpResponse = null; + try { + httpResponse = httpRequest.execute(); + } catch (IOException ioe) { + log.error("IOException while executing request ", ioe); + } finally { + if (httpResponse != null) { + httpResponse.close(); + } + } + + return new ArrayList<>(); + } + + @Override + public HttpMethod buildMethod(String method) { + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/DispatchCases.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/DispatchCases.java new file mode 100644 index 000000000..5277fcab8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/DispatchCases.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +import java.util.HashMap; + +/** + * A structure for holding dispatch cases with a default case. + * @author laurent + */ +public class DispatchCases extends HashMap { + + private static final String DEFAULT_CASE = "default"; + + public String getDefault() { + return this.get(DEFAULT_CASE); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/EvaluationOperator.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/EvaluationOperator.java new file mode 100644 index 000000000..6f79340fa --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/EvaluationOperator.java @@ -0,0 +1,28 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +/** + * This is an enumeration of evaluation operators. + * @author laurent + */ +public enum EvaluationOperator { + equals, + range, + regexp, + size, + presence +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/FallbackSpecification.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/FallbackSpecification.java new file mode 100644 index 000000000..2a1ea478c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/FallbackSpecification.java @@ -0,0 +1,74 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Represent the specification of a Fallback evaluation. dispatcher should be the original dispatcher to + * apply with its dispatcherRules companion. If no response actually found by mock controller, the + * fallback will be used as the name of response to return in any cases. + * @author laurent + */ +@JsonPropertyOrder({ "dispatcher", "dispatcherRules", "fallback" }) +public class FallbackSpecification { + + private String dispatcher; + private String dispatcherRules; + private String fallback; + + public String getDispatcher() { + return dispatcher; + } + + public void setDispatcher(String dispatcher) { + this.dispatcher = dispatcher; + } + + public String getDispatcherRules() { + return dispatcherRules; + } + + public void setDispatcherRules(String dispatcherRules) { + this.dispatcherRules = dispatcherRules; + } + + public String getFallback() { + return fallback; + } + + public void setFallback(String fallback) { + this.fallback = fallback; + } + + /** + * Build a specification from a JSON string. + * @param jsonPayload The JSON payload representing valid specification + * @return a newly built FallbackSpecification + * @throws JsonMappingException if given JSON string cannot be parsed as a FallbackSpecification + */ + public static FallbackSpecification buildFromJsonString(String jsonPayload) throws JsonMappingException { + FallbackSpecification specification = null; + try { + ObjectMapper mapper = new ObjectMapper(); + specification = mapper.readValue(jsonPayload, FallbackSpecification.class); + } catch (Exception e) { + throw new JsonMappingException("Given JSON string cannot be interpreted as valid FallbackSpecification"); + } + return specification; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonEvaluationSpecification.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonEvaluationSpecification.java new file mode 100644 index 000000000..3703ca265 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonEvaluationSpecification.java @@ -0,0 +1,74 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Represent the specification of a Json payload evaluation. exp should represent a valid JSONPointer + * expression, operator is an evaluation operator found into EvaluationOperator and + * cases represents the different possible results of evaluation (along with a default choice). + * @author laurent + */ +@JsonPropertyOrder({ "exp", "operator", "cases" }) +public class JsonEvaluationSpecification { + + private String exp; + private EvaluationOperator operator; + private DispatchCases cases; + + public String getExp() { + return exp; + } + + public void setExp(String exp) { + this.exp = exp; + } + + public EvaluationOperator getOperator() { + return operator; + } + + public void setOperator(EvaluationOperator operator) { + this.operator = operator; + } + + public DispatchCases getCases() { + return cases; + } + + public void setCases(DispatchCases cases) { + this.cases = cases; + } + + /** + * Build a specification from a JSON string. + * @param jsonPayload The JSON payload representing valid specification + * @return A newly build JsonEvaluationSpecification + * @throws JsonMappingException if given JSON string cannot be parsed as a JsonEvaluationSpecification + */ + public static JsonEvaluationSpecification buildFromJsonString(String jsonPayload) throws JsonMappingException { + JsonEvaluationSpecification specification = null; + try { + ObjectMapper mapper = new ObjectMapper(); + specification = mapper.readValue(jsonPayload, JsonEvaluationSpecification.class); + } catch (Exception e) { + throw new JsonMappingException("Given JSON string cannot be interpreted as valid JsonEvaluationSpecification"); + } + return specification; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonExpressionEvaluator.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonExpressionEvaluator.java new file mode 100644 index 000000000..c10e4820b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonExpressionEvaluator.java @@ -0,0 +1,178 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.StringReader; +import java.util.regex.Pattern; + +/** + * This utility class evaluates JSON against one or more evaluation specifications. Specification may be represented + * that way and use to find a suitable response for an incoming JSON request:
+ * + *
+ *  {
+ *   "exp": "/country",							# JSONPointer expression
+ *   "operator": "equals",
+ *   "cases": {
+ *     "Belgium": "OK Created Response",        # Name of a Response for Belgium
+ *     "Germany": "Forbidden Country Response", # Name of a Response for Germany
+ *     "default": "Why not Response"			   # Name of default Response
+ *   }
+ * }
+ * 
+ * + * @author laurent + */ +public class JsonExpressionEvaluator { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(JsonExpressionEvaluator.class); + + /** + * Evaluate a Json payload regarding a specification. Basically, it checks if payload conforms to the given + * expression and then fond the suitable cases from within specification. + * @param jsonText The Json payload to evaluate + * @param specification The evaluation specification (JSONPointer expression + operator + cases) + * @return The result of evaluation is whether one of the cases, whether + * @throws JsonMappingException if incoming Json payload is malformed or invalid + */ + public static String evaluate(String jsonText, JsonEvaluationSpecification specification) + throws JsonMappingException { + // Parse json text ang get root node. + JsonNode rootNode; + try { + ObjectMapper mapper = new ObjectMapper(); + rootNode = mapper.readTree(new StringReader(jsonText)); + } catch (Exception e) { + log.error("Exception while parsing Json text", e); + throw new JsonMappingException("Exception while parsing Json payload"); + } + + // Retrieve evaluated node within JSON tree. + JsonNode evaluatedNode = rootNode.at(specification.getExp()); + String caseKey = evaluatedNode.asText(); + + switch (specification.getOperator()) { + case equals: + // Consider simple equality. + String value = specification.getCases().get(caseKey); + return (value != null ? value : specification.getCases().getDefault()); + + case range: + // Consider range evaluation. + double caseNumber = 0.000; + try { + caseNumber = Double.parseDouble(caseKey); + } catch (NumberFormatException nfe) { + log.error(caseKey + " into range expression cannot be parsed as number. Considering default case."); + return specification.getCases().getDefault(); + } + return foundRangeMatchingCase(caseNumber, specification.getCases()); + + case regexp: + // Consider regular expression evaluation for each case key. + for (String choiceKey : specification.getCases().keySet()) { + if (!"default".equals(choiceKey)) { + if (Pattern.matches(choiceKey, caseKey)) { + return specification.getCases().get(choiceKey); + } + } + } + break; + + case size: + // Consider size evaluation. + if (evaluatedNode.isArray()) { + int size = evaluatedNode.size(); + return foundRangeMatchingCase(size, specification.getCases()); + } + break; + + case presence: + // Consider presence evaluation of evaluatedNode directly. + if (evaluatedNode != null && evaluatedNode.toString().length() > 0) { + if (specification.getCases().containsKey("found")) { + return specification.getCases().get("found"); + } + } else { + if (specification.getCases().containsKey("missing")) { + return specification.getCases().get("missing"); + } + } + break; + } + return specification.getCases().getDefault(); + } + + /** Evaluate cases from dispatchCases against caseNumber, considering each key as a range expression. */ + private static String foundRangeMatchingCase(double caseNumber, DispatchCases dispatchCases) { + // Evaluating each case key. + for (String choiceKey : dispatchCases.keySet()) { + boolean match = false; + if (isValidRangeKey(choiceKey)) { + try { + double[] minAndMax = extractMinAndMaxFromRange(choiceKey); + // Fastidious part... Apply operators depending of brackets orientations. + if (choiceKey.startsWith("[") && choiceKey.endsWith("]")) { + if (caseNumber >= minAndMax[0] && caseNumber <= minAndMax[1]) { + match = true; + } + } else if (choiceKey.startsWith("]") && choiceKey.endsWith("]")) { + if (caseNumber > minAndMax[0] && caseNumber <= minAndMax[1]) { + match = true; + } + } else if (choiceKey.startsWith("[") && choiceKey.endsWith("[")) { + if (caseNumber >= minAndMax[0] && caseNumber < minAndMax[1]) { + match = true; + } + } else if (choiceKey.startsWith("]") && choiceKey.endsWith("[")) { + if (caseNumber > minAndMax[0] && caseNumber < minAndMax[1]) { + match = true; + } + } + } catch (NumberFormatException nfe) { + log.warn(choiceKey + " expression cannot be parsed as number for min and max range."); + } + } + if (match) { + return dispatchCases.get(choiceKey); + } + } + return dispatchCases.getDefault(); + } + + /** Validate key is a correct range expression. */ + private static boolean isValidRangeKey(String key) { + boolean hasCorrectStart = key.startsWith("[") || key.startsWith("]"); + boolean hasCorrectEnd = key.endsWith("[") || key.endsWith("]"); + boolean hasDelimiter = key.contains(";"); + return hasCorrectStart && hasDelimiter && hasCorrectEnd; + } + + /** Extract minimum and maximum from range expression. Considering min on the left side and max on the right side. */ + private static double[] extractMinAndMaxFromRange(String range) throws NumberFormatException { + double[] results = new double[2]; + String[] minAndMax = range.split(";"); + results[0] = Double.parseDouble(minAndMax[0].substring(1)); + results[1] = Double.parseDouble(minAndMax[1].substring(0, minAndMax[1].length() - 1)); + return results; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonMappingException.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonMappingException.java new file mode 100644 index 000000000..ffb1ded4c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/JsonMappingException.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +/** + * Custom exception for tracking an Object-to-Json mapping exception. + * @author laurent + */ +public class JsonMappingException extends Exception { + + /** + * Create a new JsonMappingException. + * @param message Exception message + */ + public JsonMappingException(String message) { + super(message); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/ProxyFallbackSpecification.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/ProxyFallbackSpecification.java new file mode 100644 index 000000000..79ed0cca3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/dispatcher/ProxyFallbackSpecification.java @@ -0,0 +1,71 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Represent the specification of a Proxy-Fallback evaluation. dispatcher should be the original dispatcher + * to apply with its dispatcherRules companion. If no response actually found by mock controller, the + * original request will be forwarded to the proxyUrl. + */ +@JsonPropertyOrder({ "dispatcher", "dispatcherRules", "proxyUrl" }) +public class ProxyFallbackSpecification { + private static final ObjectMapper mapper = new ObjectMapper(); + + private String dispatcher; + private String dispatcherRules; + private String proxyUrl; + + public String getDispatcher() { + return dispatcher; + } + + public void setDispatcher(String dispatcher) { + this.dispatcher = dispatcher; + } + + public String getDispatcherRules() { + return dispatcherRules; + } + + public void setDispatcherRules(String dispatcherRules) { + this.dispatcherRules = dispatcherRules; + } + + public String getProxyUrl() { + return proxyUrl; + } + + public void setProxyUrl(String proxyUrl) { + this.proxyUrl = proxyUrl; + } + + /** + * Build a specification from a JSON string. + * @param jsonPayload The JSON payload representing valid specification + * @return a newly built ProxyFallbackSpecification + * @throws JsonMappingException if given JSON string cannot be parsed as a ProxySpecification + */ + public static ProxyFallbackSpecification buildFromJsonString(String jsonPayload) throws JsonMappingException { + try { + return mapper.readValue(jsonPayload, ProxyFallbackSpecification.class); + } catch (Exception e) { + throw new JsonMappingException("Given JSON string cannot be interpreted as valid ProxyFallbackSpecification"); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLHttpRequest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLHttpRequest.java new file mode 100644 index 000000000..1411ab31f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLHttpRequest.java @@ -0,0 +1,92 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * A wrapper for parsing GraphQL requests over Http. + * @author laurent + */ +public class GraphQLHttpRequest { + + String query; + String operationName; + JsonNode variables; + + public String getQuery() { + return query; + } + + public String getOperationName() { + return operationName; + } + + public JsonNode getVariables() { + return variables; + } + + /** + * Build a GraphQLHttpRequest from Http servlet request and body content. + * @param body The content of Http request. + * @param request The servlet request + * @return The wrapper object + * @throws Exception if request is no valid Json as expected by GraphQL over Http spec. + */ + public static GraphQLHttpRequest from(String body, HttpServletRequest request) throws Exception { + // We'll need a Json mapper. + ObjectMapper mapper = new ObjectMapper(); + GraphQLHttpRequest parameters = new GraphQLHttpRequest(); + + if ("POST".equalsIgnoreCase(request.getMethod())) { + // If it's a post, everything is in body. + JsonNode json = mapper.readTree(body); + parameters.query = json.path("query").asText(); + parameters.operationName = json.path("operationName").asText(null); + parameters.variables = json.get("variables"); + } else { + // If it's a get, we're using parameters. + parameters.query = request.getParameter("query"); + parameters.operationName = request.getParameter("operationName"); + parameters.variables = getVariables(mapper, request.getParameter("variables")); + } + return parameters; + } + + /** + * Build a simple GraphQLHttpRequest with a query and variables. + * @param operationName Is used as both query and operationName. + * @param variables The variables to use for the query. + * @return The wrapper object + */ + public static GraphQLHttpRequest from(String operationName, JsonNode variables) { + GraphQLHttpRequest parameters = new GraphQLHttpRequest(); + parameters.query = operationName; + parameters.operationName = operationName; + parameters.variables = variables; + return parameters; + } + + private static JsonNode getVariables(ObjectMapper mapper, String variables) throws Exception { + if (variables != null) { + return mapper.readTree(variables); + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLImporter.java new file mode 100644 index 000000000..58bdb34a1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLImporter.java @@ -0,0 +1,235 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import graphql.schema.idl.ScalarInfo; +import graphql.schema.idl.TypeInfo; +import graphql.schema.idl.TypeUtil; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; + +import graphql.language.Comment; +import graphql.language.Definition; +import graphql.language.Document; +import graphql.language.FieldDefinition; +import graphql.language.InputValueDefinition; +import graphql.language.ListType; +import graphql.language.ObjectTypeDefinition; +import graphql.language.Type; +import graphql.language.TypeName; +import graphql.parser.Parser; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * An implementation of MockRepositoryImporter that deals with GraphQL Schema documents. + * @author laurent + */ +public class GraphQLImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(GraphQLImporter.class); + + /** The starter marker for the comment referencing microck service and version identifiers. */ + public static final String MICROCKS_ID_STARTER = "microcksId:"; + + /** The list of valid operation types. */ + public static final List VALID_OPERATION_TYPES = Arrays.asList("query", "mutation"); + + private final String specContent; + private Document graphqlSchema; + + /** + * Build a new importer. + * @param graphqlFilePath The path to local GraphQL schema file + * @throws IOException if project file cannot be found or read. + */ + public GraphQLImporter(String graphqlFilePath) throws IOException { + try { + // Read spec bytes. + byte[] bytes = Files.readAllBytes(Paths.get(graphqlFilePath)); + specContent = new String(bytes, StandardCharsets.UTF_8); + + // Parse schema file to a dom. + graphqlSchema = Parser.parse(specContent); + } catch (Exception e) { + log.error("Exception while parsing GraphQL schema file " + graphqlFilePath, e); + throw new IOException("GraphQL schema file parsing error"); + } + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List results = new ArrayList<>(); + + Service service = new Service(); + service.setType(ServiceType.GRAPHQL); + + // 1st thing: look for comments to get service and version identifiers. + for (Comment comment : graphqlSchema.getComments()) { + String content = comment.getContent().trim(); + if (content.startsWith(MICROCKS_ID_STARTER)) { + String identifiers = content.substring(MICROCKS_ID_STARTER.length()); + + if (identifiers.indexOf(":") != -1) { + String[] serviceAndVersion = identifiers.split(":"); + service.setName(serviceAndVersion[0].trim()); + service.setVersion(serviceAndVersion[1].trim()); + break; + } + log.error("microcksId comment is malformed. Expecting \'microcksId: :\'"); + throw new MockRepositoryImportException( + "microcksId comment is malformed. Expecting \'microcksId: :\'"); + } + } + if (service.getName() == null || service.getVersion() == null) { + log.error("No microcksId: comment found into GraphQL schema to get API name and version"); + throw new MockRepositoryImportException( + "No microcksId: comment found into GraphQL schema to get API name and version"); + } + + // We found a service, build its operations. + service.setOperations(extractOperations()); + + results.add(service); + return results; + } + + @Override + public List getResourceDefinitions(Service service) throws MockRepositoryImportException { + List results = new ArrayList<>(); + + // Just one resource: The GraphQL schema file. + Resource schema = new Resource(); + schema.setName(service.getName() + "-" + service.getVersion() + ".graphql"); + schema.setType(ResourceType.GRAPHQL_SCHEMA); + schema.setContent(specContent); + results.add(schema); + + return results; + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + return new ArrayList<>(); + } + + /** + * Extract the operations from GraphQL schema document. + */ + private List extractOperations() { + List results = new ArrayList<>(); + + for (Definition definition : graphqlSchema.getDefinitions()) { + if (definition instanceof ObjectTypeDefinition typeDefinition) { + + if (VALID_OPERATION_TYPES.contains(typeDefinition.getName().toLowerCase())) { + List operations = extractOperations(typeDefinition); + results.addAll(operations); + } + } + } + return results; + } + + private List extractOperations(ObjectTypeDefinition typeDef) { + List results = new ArrayList<>(); + + for (FieldDefinition fieldDef : typeDef.getFieldDefinitions()) { + Operation operation = new Operation(); + operation.setName(fieldDef.getName()); + operation.setMethod(typeDef.getName().toUpperCase()); + + // Deal with input names if any. + if (fieldDef.getInputValueDefinitions() != null && !fieldDef.getInputValueDefinitions().isEmpty()) { + operation.setInputName(getInputNames(fieldDef.getInputValueDefinitions())); + + boolean hasOnlyPrimitiveArgs = true; + for (InputValueDefinition inputValueDef : fieldDef.getInputValueDefinitions()) { + Type inputValueType = inputValueDef.getType(); + if (TypeUtil.isNonNull(inputValueType)) { + inputValueType = TypeUtil.unwrapOne(inputValueType); + } + if (TypeUtil.isList(inputValueType)) { + hasOnlyPrimitiveArgs = false; + } + TypeInfo inputValueTypeInfo = TypeInfo.typeInfo(inputValueType); + if (!ScalarInfo.isGraphqlSpecifiedScalar(inputValueTypeInfo.getName())) { + hasOnlyPrimitiveArgs = false; + } + } + if (hasOnlyPrimitiveArgs) { + operation.setDispatcher(DispatchStyles.QUERY_ARGS); + operation.setDispatcherRules(extractOperationParams(fieldDef.getInputValueDefinitions())); + } + } + // Deal with output names if any. + if (fieldDef.getType() != null) { + operation.setOutputName(getTypeName(fieldDef.getType())); + } + + results.add(operation); + } + return results; + } + + /** Build a string representing comma separated inputs (eg. 'arg1, arg2'). */ + private String getInputNames(List inputsDef) { + StringBuilder builder = new StringBuilder(); + + for (InputValueDefinition inputDef : inputsDef) { + builder.append(getTypeName(inputDef.getType())).append(", "); + } + return builder.substring(0, builder.length() - 2); + } + + /** Build a string representing operation parameters as used in dispatcher rules (arg1 && arg2). */ + private String extractOperationParams(List inputsDef) { + StringBuilder builder = new StringBuilder(); + + for (InputValueDefinition inputDef : inputsDef) { + builder.append(inputDef.getName()).append(" && "); + } + return builder.substring(0, builder.length() - 4); + } + + /** Get the short string representation of a type. eg. 'Film' or '[Films]'. */ + private String getTypeName(Type type) { + if (type instanceof ListType listType) { + return "[" + getTypeName(listType.getType()) + "]"; + } else if (type instanceof TypeName typeName) { + return typeName.getName(); + } + return type.toString(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLMcpToolConverter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLMcpToolConverter.java new file mode 100644 index 000000000..6ad697dcc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLMcpToolConverter.java @@ -0,0 +1,398 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.util.ai.McpSchema; +import io.github.microcks.util.ai.McpToolConverter; +import io.github.microcks.web.BasicHttpServletRequest; +import io.github.microcks.web.GraphQLInvocationProcessor; +import io.github.microcks.web.MockControllerCommons; +import io.github.microcks.web.MockInvocationContext; +import io.github.microcks.web.ResponseResult; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import graphql.language.Document; +import graphql.language.EnumTypeDefinition; +import graphql.language.EnumValueDefinition; +import graphql.language.FieldDefinition; +import graphql.language.InputObjectTypeDefinition; +import graphql.language.InputValueDefinition; +import graphql.language.ObjectTypeDefinition; +import graphql.language.ScalarTypeDefinition; +import graphql.language.Type; +import graphql.parser.Parser; +import graphql.schema.idl.ScalarInfo; +import graphql.schema.idl.TypeInfo; +import graphql.schema.idl.TypeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_ADD_PROPERTIES_ELEMENT; +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_ITEMS_ELEMENT; +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_PROPERTIES_ELEMENT; +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_REQUIRED_ELEMENT; +import static io.github.microcks.util.graphql.GraphQLImporter.VALID_OPERATION_TYPES; +import static io.github.microcks.util.graphql.JsonSchemaBuilderQueryVisitor.JSON_SCHEMA_ENUM; + +/** + * Implementation of McpToolConverter for GraphQL services. + * @author laurent + */ +public class GraphQLMcpToolConverter extends McpToolConverter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(GraphQLMcpToolConverter.class); + + private final GraphQLInvocationProcessor invocationProcessor; + private final ObjectMapper mapper; + + private Document graphqlDocument; + + /** + * Build a new instance of GraphQLMcpToolConverter. + * @param service The service to which this converter is attached + * @param resource The resource used for GraphQL service conversion + * @param invocationProcessor The invocation processor to use for processing the call + * @param mapper The ObjectMapper to use for JSON serialization + */ + public GraphQLMcpToolConverter(Service service, Resource resource, GraphQLInvocationProcessor invocationProcessor, + ObjectMapper mapper) { + super(service, resource); + this.invocationProcessor = invocationProcessor; + this.mapper = mapper; + } + + @Override + public String getToolDescription(Operation operation) { + try { + if (graphqlDocument == null) { + graphqlDocument = new Parser().parseDocument(resource.getContent()); + } + FieldDefinition operationDefinition = getOperationDefinition(operation.getName()); + + if (operationDefinition != null) { + // #1 Look for a description in the operation definition. + if (operationDefinition.getDescription() != null) { + return operationDefinition.getDescription().content; + } else if (operationDefinition.getComments() != null && !operationDefinition.getComments().isEmpty()) { + // #2 Look for comments in the operation definition. + StringBuilder result = new StringBuilder(); + operationDefinition.getComments().forEach(comment -> { result.append(comment.getContent()); }); + return result.toString().trim(); + } + } + } catch (Exception e) { + log.error("Exception while trying to get tool description", e); + } + return null; + } + + @Override + public McpSchema.JsonSchema getInputSchema(Operation operation) { + ObjectNode inputSchemaNode = mapper.createObjectNode(); + ObjectNode schemaPropertiesNode = mapper.createObjectNode(); + ArrayNode requiredPropertiesNode = mapper.createArrayNode(); + + // Initialize input schema with empty object. + inputSchemaNode.put("type", "object"); + inputSchemaNode.set(JSON_SCHEMA_PROPERTIES_ELEMENT, schemaPropertiesNode); + inputSchemaNode.set(JSON_SCHEMA_REQUIRED_ELEMENT, requiredPropertiesNode); + inputSchemaNode.put(JSON_SCHEMA_ADD_PROPERTIES_ELEMENT, false); + + try { + if (graphqlDocument == null) { + graphqlDocument = new Parser().parseDocument(resource.getContent()); + } + FieldDefinition operationDefinition = getOperationDefinition(operation.getName()); + + if (operationDefinition != null && operationDefinition.getInputValueDefinitions() != null + && !operationDefinition.getInputValueDefinitions().isEmpty()) { + for (InputValueDefinition inputValueDef : operationDefinition.getInputValueDefinitions()) { + visitProperty(inputValueDef.getName(), inputValueDef.getType(), schemaPropertiesNode, + requiredPropertiesNode); + } + } + } catch (Exception e) { + log.error("Exception while trying to get input schema", e); + } + return mapper.convertValue(inputSchemaNode, McpSchema.JsonSchema.class); + } + + @Override + public Response getCallResponse(Operation operation, McpSchema.CallToolRequest request, + Map> headers) { + // Build a mock invocation context needed for the invocation processor. + MockInvocationContext ic = new MockInvocationContext(service, operation, null); + + try { + String query = null; + if (operation.getDispatcher() != null && !operation.getDispatcher().startsWith("PROXY")) { + // We target Microcks so query can be simple due to permissive parsing. + query = operation.getName(); + } else { + // We target a real GraphQL service so query, we must build a full and correct query. + query = buildCompleteGraphQLQuery(operation, request).replace("\"", "\\\""); + } + + // De-serialize remaining arguments as the body variables. + String body = "{\"variables\": " + mapper.writeValueAsString(request.arguments()) + ", \"query\": \"" + query + + "\" }"; + + ObjectNode variables = mapper.convertValue(request.arguments(), ObjectNode.class); + GraphQLHttpRequest graphqlRequest = GraphQLHttpRequest.from(operation.getName(), variables); + + // Query parameters are actually the body but simplified to a map of string values. + Map queryParams = request.arguments().entrySet().stream() + .map(entry -> Map.entry(entry.getKey(), entry.getValue().toString())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + // Execute the invocation processor after having cleaned the headers to propagate. + headers = sanitizeHttpHeaders(headers); + ResponseResult result = invocationProcessor.processInvocation(ic, System.currentTimeMillis(), queryParams, + body, graphqlRequest, headers, + new BasicHttpServletRequest( + "http://localhost:8080/graphql/" + + MockControllerCommons.composeServiceAndVersion(service.getName(), service.getVersion()), + "POST", "", "", Map.of(), headers)); + + // Build a Microcks Response from the result. + Response response = new Response(); + response.setStatus(result.status().toString()); + response.setHeaders(null); + + String resultContent = extractResponseContent(result); + + // As we're no longer tied to the GraphQL semantics, we can get rid of the data/ wrapper. + JsonNode responseNode = mapper.readTree(resultContent); + if (responseNode.has("data") && responseNode.get("data").has(operation.getName())) { + response.setContent(mapper.writeValueAsString(responseNode.get("data").get(operation.getName()))); + } else { + // Default to the full response. + response.setContent(resultContent); + } + + if (result.status().isError()) { + response.setFault(true); + } + return response; + } catch (Exception e) { + log.error("Exception while processing the MCP call invocation", e); + } + return null; + } + + /** Retrieve the correct operation definition in GraphQL schema document. */ + private FieldDefinition getOperationDefinition(String operationName) { + for (ObjectTypeDefinition typeDefinition : graphqlDocument.getDefinitionsOfType(ObjectTypeDefinition.class)) { + if (VALID_OPERATION_TYPES.contains(typeDefinition.getName().toLowerCase())) { + for (FieldDefinition fieldDef : typeDefinition.getFieldDefinitions()) { + if (fieldDef.getName().equals(operationName)) { + return fieldDef; + } + } + } + } + return null; + } + + /** Check if the type is a scalar type. */ + private boolean isScalarType(String typeName) { + return ScalarInfo.isGraphqlSpecifiedScalar(typeName) + || graphqlDocument.getDefinitionsOfType(ScalarTypeDefinition.class).stream() + .anyMatch(scalarTypeDefinition -> scalarTypeDefinition.getName().equals(typeName)); + } + + /** Retrieve the correct type definition in GraphQL schema document. */ + private ObjectTypeDefinition getTypeDefinition(String typeName) { + for (ObjectTypeDefinition typeDefinition : graphqlDocument.getDefinitionsOfType(ObjectTypeDefinition.class)) { + if (typeDefinition.getName().equals(typeName)) { + return typeDefinition; + } + } + return null; + } + + /** Retrieve the correct enum type definition in GraphQL schema document. */ + private EnumTypeDefinition getEnumTypeDefinition(String typeName) { + for (EnumTypeDefinition enumTypeDefinition : graphqlDocument.getDefinitionsOfType(EnumTypeDefinition.class)) { + if (enumTypeDefinition.getName().equals(typeName)) { + return enumTypeDefinition; + } + } + return null; + } + + /** Retrieve the correct input value definition in GraphQL schema document. */ + private InputObjectTypeDefinition getInputValueDefinition(String typeName) { + for (InputObjectTypeDefinition inputObjectTypeDefinition : graphqlDocument + .getDefinitionsOfType(InputObjectTypeDefinition.class)) { + if (inputObjectTypeDefinition.getName().equals(typeName)) { + return inputObjectTypeDefinition; + } + } + return null; + } + + /** Visit a property and add it to the input schema elements (properties and required properties). */ + private void visitProperty(String propertyName, Type propertyType, ObjectNode propertiesNode, + ArrayNode requiredPropertiesNode) { + if (TypeUtil.isNonNull(propertyType)) { + requiredPropertiesNode.add(propertyName); + propertyType = TypeUtil.unwrapOne(propertyType); + } + + TypeInfo propertyTypeInfo = TypeInfo.typeInfo(propertyType); + + // Check if the field is a scalar. + if (isScalarType(propertyTypeInfo.getName())) { + ObjectNode propertySchemaNode = mapper.createObjectNode(); + propertySchemaNode.put("type", "string"); + if (propertyName != null) { + propertiesNode.set(propertyName, propertySchemaNode); + } else { + // We may have no name if inside an array items. + propertiesNode.setAll(propertySchemaNode); + } + + } else if (TypeUtil.isList(propertyType)) { + // We must convert to a type array. + ObjectNode arraySchemaNode = mapper.createObjectNode(); + ObjectNode subitemsNode = mapper.createObjectNode(); + + arraySchemaNode.put("type", "array"); + arraySchemaNode.set(JSON_SCHEMA_ITEMS_ELEMENT, subitemsNode); + propertiesNode.set(propertyName, arraySchemaNode); + + visitProperty(null, TypeUtil.unwrapAll(propertyType), subitemsNode, requiredPropertiesNode); + } else { + // We must check if it's an enum type. + EnumTypeDefinition enumTypeDefinition = getEnumTypeDefinition(propertyTypeInfo.getName()); + if (enumTypeDefinition != null) { + ObjectNode enumSchemaNode = mapper.createObjectNode(); + enumSchemaNode.put("type", "string"); + ArrayNode enumValuesNode = mapper.createArrayNode(); + for (EnumValueDefinition valueDef : enumTypeDefinition.getEnumValueDefinitions()) { + enumValuesNode.add(valueDef.getName()); + } + enumSchemaNode.set(JSON_SCHEMA_ENUM, enumValuesNode); + if (propertyName != null) { + propertiesNode.set(propertyName, enumSchemaNode); + } else { + // We may have no name if inside an array items. + propertiesNode.setAll(enumSchemaNode); + } + } else { + // So finally, it should be an object type. + // Initialize a new subschema node we must visit to resolve all possible references. + ObjectNode subschemaNode = mapper.createObjectNode(); + ObjectNode subpropertiesNode = mapper.createObjectNode(); + ArrayNode requiredSubpropertiesNode = mapper.createArrayNode(); + + subschemaNode.put("type", "object"); + subschemaNode.set(JSON_SCHEMA_PROPERTIES_ELEMENT, subpropertiesNode); + subschemaNode.set(JSON_SCHEMA_REQUIRED_ELEMENT, requiredSubpropertiesNode); + subschemaNode.put(JSON_SCHEMA_ADD_PROPERTIES_ELEMENT, false); + if (propertyName != null) { + propertiesNode.set(propertyName, subschemaNode); + } else { + // We may have no name if inside an array items. + propertiesNode.setAll(subschemaNode); + } + + ObjectTypeDefinition typeDef = getTypeDefinition(propertyTypeInfo.getName()); + if (typeDef != null) { + visitObjectTypeDefinition(typeDef, subpropertiesNode, requiredSubpropertiesNode); + } else { + // It could be an input type definition. + InputObjectTypeDefinition inputTypeDef = getInputValueDefinition(propertyTypeInfo.getName()); + if (inputTypeDef != null) { + visitInputObjectTypeDefinition(inputTypeDef, subpropertiesNode, requiredSubpropertiesNode); + } else { + log.warn("Cannot find type definition for {}", propertyTypeInfo.getName()); + } + } + } + } + } + + /** Visit a GraphQL object type definition and add its properties to the input schema. */ + private void visitObjectTypeDefinition(ObjectTypeDefinition typeDefinition, ObjectNode propertiesNode, + ArrayNode requiredPropertiesNode) { + for (FieldDefinition fieldDef : typeDefinition.getFieldDefinitions()) { + visitProperty(fieldDef.getName(), fieldDef.getType(), propertiesNode, requiredPropertiesNode); + } + } + + /** Visit a GraphQL input object type definition and add its properties to the input schema. */ + private void visitInputObjectTypeDefinition(InputObjectTypeDefinition typeDefinition, ObjectNode propertiesNode, + ArrayNode requiredPropertiesNode) { + for (InputValueDefinition fieldDef : typeDefinition.getInputValueDefinitions()) { + visitProperty(fieldDef.getName(), fieldDef.getType(), propertiesNode, requiredPropertiesNode); + } + } + + /** Build a complete a query with all the fields to fetch. */ + private String buildCompleteGraphQLQuery(Operation operation, McpSchema.CallToolRequest request) { + // Extract the type information from the operation. + if (graphqlDocument == null) { + graphqlDocument = new Parser().parseDocument(resource.getContent()); + } + FieldDefinition operationDefinition = getOperationDefinition(operation.getName()); + Type operationOutputType = operationDefinition.getType(); + TypeInfo operationOutputTypeInfo = TypeInfo.typeInfo(operationOutputType); + ObjectTypeDefinition typeDef = getTypeDefinition(operationOutputTypeInfo.getName()); + + StringBuilder queryBuilder = new StringBuilder(); + queryBuilder.append("query {"); + queryBuilder.append(" ").append(operation.getName()).append("("); + + // Append the arguments to the operation. + queryBuilder.append(request.arguments().entrySet().stream() + .map(entry -> entry.getKey() + ": \"" + entry.getValue() + "\"").collect(Collectors.joining(", ", "", ""))); + + queryBuilder.append("){\\n"); + + if (typeDef != null) { + for (FieldDefinition fd : typeDef.getFieldDefinitions()) { + TypeInfo fdTypeInfo = TypeInfo.typeInfo(fd.getType()); + if (isScalarType(fdTypeInfo.getName())) { + queryBuilder.append(fd.getName()).append("\\n"); + } else { + // We must check if it's an enum type. + EnumTypeDefinition enumTypeDefinition = getEnumTypeDefinition(fdTypeInfo.getName()); + if (enumTypeDefinition != null) { + queryBuilder.append(fd.getName()).append("\\n"); + } + } + } + } + + // Finalize query before returning it. + queryBuilder.append("}}"); + return queryBuilder.toString(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLTestRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLTestRunner.java new file mode 100644 index 000000000..a9536ba42 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/graphql/GraphQLTestRunner.java @@ -0,0 +1,167 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.util.test.HttpTestRunner; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.util.List; + +/** + * An extension of AbstractTestRunner that deals with GraphQL calls. + * @author laurent + */ +public class GraphQLTestRunner extends HttpTestRunner { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(GraphQLTestRunner.class); + + /** Content-type for JSON that is the sole valid response type. */ + private static final String APPLICATION_JSON_TYPE = "application/json"; + + private ObjectMapper mapper = new ObjectMapper(); + + private ResourceRepository resourceRepository; + + private String lastQueryContent = null; + private List lastValidationErrors = null; + + /** + * Build a new GraphQLTestRunner. + * @param resourceRepository Access to resources repository + */ + public GraphQLTestRunner(ResourceRepository resourceRepository) { + this.resourceRepository = resourceRepository; + } + + + @Override + protected void prepareRequest(Request request) { + try { + JsonNode requestNode = mapper.readTree(request.getContent()); + lastQueryContent = requestNode.path("query").asText(); + } catch (JsonProcessingException jpe) { + log.error("JsonProcessingException while preparing GraphQL test query", jpe); + lastQueryContent = ""; + } + } + + @Override + protected int extractTestReturnCode(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse, String responseContent) { + + int code = TestReturn.SUCCESS_CODE; + + int responseCode = 0; + try { + responseCode = httpResponse.getStatusCode().value(); + log.debug("Response status code : {}", responseCode); + } catch (IOException ioe) { + log.debug("IOException while getting raw status code in response", ioe); + return TestReturn.FAILURE_CODE; + } + + // Check that we're in the 20x family. + if (!String.valueOf(responseCode).startsWith("20")) { + log.debug("Response code if not in 20x range, assuming it's a failure"); + return TestReturn.FAILURE_CODE; + } + + // Extract response content-type in any case. + String contentType = null; + if (httpResponse.getHeaders().getContentType() != null) { + log.debug("Response media-type is {}", httpResponse.getHeaders().getContentType()); + contentType = httpResponse.getHeaders().getContentType().toString(); + // Sanitize charset information from media-type. + if (contentType.contains("charset=") && contentType.indexOf(";") > 0) { + contentType = contentType.substring(0, contentType.indexOf(";")); + } + } + + // Do not try to validate response content if no content provided ;-) + // Also do not try to schema validate something that is not application/json for now... + if (responseCode != 204 && APPLICATION_JSON_TYPE.equals(contentType)) { + // Retrieve the resource corresponding to OpenAPI specification if any. + Resource graphqlSchemaResource = null; + List resources = resourceRepository.findByServiceIdAndType(service.getId(), + ResourceType.GRAPHQL_SCHEMA); + if (!resources.isEmpty()) { + graphqlSchemaResource = resources.get(0); + } + if (graphqlSchemaResource == null) { + log.debug("Found no GraphQL specification resource for service {}, so failing validating", service.getId()); + return TestReturn.FAILURE_CODE; + } + + JsonNode responseSchema = null; + try { + responseSchema = GraphQLSchemaValidator.buildResponseJsonSchema(graphqlSchemaResource.getContent(), + lastQueryContent); + log.debug("responseSchema: {}", responseSchema); + lastValidationErrors = GraphQLSchemaValidator.validateJson(responseSchema, + mapper.readTree(responseContent)); + } catch (IOException ioe) { + log.debug("Response body cannot be accessed or transformed as Json, returning failure"); + return TestReturn.FAILURE_CODE; + } + + if (!lastValidationErrors.isEmpty()) { + log.debug( + "GraphQL schema validation errors found: {}, marking test as failed." + lastValidationErrors.size()); + return TestReturn.FAILURE_CODE; + } + log.debug("GraphQL schema validation of response is successful !"); + } + return code; + } + + @Override + protected String extractTestReturnMessage(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse) { + StringBuilder builder = new StringBuilder(); + if (lastValidationErrors != null && !lastValidationErrors.isEmpty()) { + for (String error : lastValidationErrors) { + builder.append(error).append("/n"); + } + } + // Reset just after consumption so avoid side-effects. + lastValidationErrors = null; + return builder.toString(); + } + + /** + * Build the HttpMethod corresponding to string. + */ + @Override + public HttpMethod buildMethod(String method) { + return HttpMethod.POST; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMcpToolConverter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMcpToolConverter.java new file mode 100644 index 000000000..8e956d0b9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMcpToolConverter.java @@ -0,0 +1,256 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.util.ai.McpSchema; +import io.github.microcks.util.ai.McpToolConverter; +import io.github.microcks.web.GrpcInvocationProcessor; +import io.github.microcks.web.GrpcResponseResult; +import io.github.microcks.web.MockInvocationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.protobuf.Descriptors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_ADD_PROPERTIES_ELEMENT; +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_ITEMS_ELEMENT; +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_PROPERTIES_ELEMENT; +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_REQUIRED_ELEMENT; + +/** + * Implementation of McpToolConverter for gRPC services. + * @author laurent + */ +public class GrpcMcpToolConverter extends McpToolConverter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(GrpcMcpToolConverter.class); + + private final GrpcInvocationProcessor invocationProcessor; + private final ObjectMapper mapper; + + private Descriptors.ServiceDescriptor sd; + + /** + * Build a new instance of GrpcMcpToolConverter. + * @param service The service to which this converter is attached + * @param resource The resource used for OpenAPI service conversion + * @param invocationProcessor The invocation processor to use for processing the call + * @param mapper The ObjectMapper to use for JSON serialization + */ + public GrpcMcpToolConverter(Service service, Resource resource, GrpcInvocationProcessor invocationProcessor, + ObjectMapper mapper) { + super(service, resource); + this.invocationProcessor = invocationProcessor; + this.mapper = mapper; + } + + @Override + public String getToolDescription(Operation operation) { + return null; + } + + @Override + public McpSchema.JsonSchema getInputSchema(Operation operation) { + ObjectNode inputSchemaNode = mapper.createObjectNode(); + ObjectNode schemaPropertiesNode = mapper.createObjectNode(); + ArrayNode requiredPropertiesNode = mapper.createArrayNode(); + + // Initialize input schema with empty object. + inputSchemaNode.put("type", "object"); + inputSchemaNode.set(JSON_SCHEMA_PROPERTIES_ELEMENT, schemaPropertiesNode); + inputSchemaNode.set(JSON_SCHEMA_REQUIRED_ELEMENT, requiredPropertiesNode); + inputSchemaNode.put(JSON_SCHEMA_ADD_PROPERTIES_ELEMENT, false); + try { + if (sd == null) { + sd = GrpcUtil.findServiceDescriptor(resource.getContent(), service.getName()); + } + + Descriptors.MethodDescriptor md = sd.findMethodByName(operation.getName()); + if (md.getInputType() != null) { + // Visit the input type descriptor. + visitDescriptor(md.getInputType(), schemaPropertiesNode, requiredPropertiesNode); + } + } catch (Exception e) { + log.error("Exception while trying to get input schema", e); + } + return mapper.convertValue(inputSchemaNode, McpSchema.JsonSchema.class); + } + + @Override + public Response getCallResponse(Operation operation, McpSchema.CallToolRequest request, + Map> headers) { + // Build a mock invocation context needed for the invocation processor. + MockInvocationContext ic = new MockInvocationContext(service, operation, null); + + try { + // De-serialize remaining arguments as the request body. + String body = mapper.writeValueAsString(request.arguments()); + + // Execute the invocation processor. + GrpcResponseResult result = invocationProcessor.processInvocation(ic, System.currentTimeMillis(), body); + + // Build a Microcks Response from the result. + Response response = new Response(); + if (result.content() != null) { + response.setContent(result.content()); + } + if (result.isError()) { + response.setFault(true); + response.setStatus("500"); + response.setContent(result.errorDescription()); + } + return response; + } catch (Exception e) { + log.error("Exception while processing the MCP call invocation", e); + } + return null; + } + + /** Visit a protobuf message descriptor and extract its properties. */ + private void visitDescriptor(Descriptors.Descriptor inputType, ObjectNode propertiesNode, + ArrayNode requiredPropertiesNode) { + for (Descriptors.FieldDescriptor fd : inputType.getFields()) { + + // Get the field name and type. + String fieldName = fd.getName(); + + // Check if the field is required. + if (fd.isRequired()) { + requiredPropertiesNode.add(fieldName); + } + + // Check if the field is an array/a repeated field. + if (fd.isRepeated()) { + // We must convert to a type array. + ObjectNode arraySchemaNode = mapper.createObjectNode(); + + arraySchemaNode.put("type", "array"); + propertiesNode.set(fieldName, arraySchemaNode); + + if (isMessageType(fd.getType())) { + ObjectNode subschemaNode = mapper.createObjectNode(); + ObjectNode subitemsNode = mapper.createObjectNode(); + ArrayNode requiredSubitemsPropertiesNode = mapper.createArrayNode(); + + visitDescriptor(fd.getMessageType(), subitemsNode, requiredSubitemsPropertiesNode); + + // Add the required properties to the subschema. + subschemaNode.put("type", "object"); + subschemaNode.set(JSON_SCHEMA_PROPERTIES_ELEMENT, subitemsNode); + + // Add the items definition to the current property. + arraySchemaNode.set(JSON_SCHEMA_ITEMS_ELEMENT, subschemaNode); + propertiesNode.set(fieldName, arraySchemaNode); + } else if (isEnumType(fd.getType())) { + arraySchemaNode.set(JSON_SCHEMA_ITEMS_ELEMENT, buildEnumTypeNode(fd)); + } else if (isScalarType(fd.getType())) { + arraySchemaNode.set(JSON_SCHEMA_ITEMS_ELEMENT, buildScalarTypeNode(fd)); + } + } else { + // Check if the field is an object/message type. + if (isMessageType(fd.getType())) { + // Initialize a new subschema node we must visit to resolve message fields. + ObjectNode subschemaNode = mapper.createObjectNode(); + ObjectNode subpropertiesNode = mapper.createObjectNode(); + ArrayNode requiredSubpropertiesNode = mapper.createArrayNode(); + + subschemaNode.put("type", "object"); + subschemaNode.set(JSON_SCHEMA_PROPERTIES_ELEMENT, subpropertiesNode); + subschemaNode.set(JSON_SCHEMA_REQUIRED_ELEMENT, requiredSubpropertiesNode); + subschemaNode.put(JSON_SCHEMA_ADD_PROPERTIES_ELEMENT, false); + propertiesNode.set(fieldName, subschemaNode); + + visitDescriptor(fd.getMessageType(), subpropertiesNode, requiredSubpropertiesNode); + } else if (isEnumType(fd.getType())) { + propertiesNode.set(fieldName, buildEnumTypeNode(fd)); + } else if (isScalarType(fd.getType())) { + propertiesNode.set(fieldName, buildScalarTypeNode(fd)); + } + } + } + } + + /** Defines is a protobuf message field type is an message type. */ + private static boolean isMessageType(Descriptors.FieldDescriptor.Type fieldType) { + return fieldType == Descriptors.FieldDescriptor.Type.MESSAGE + || fieldType == Descriptors.FieldDescriptor.Type.GROUP; + } + + /** Defines is a protobuf message field type is an enum type. */ + private static boolean isEnumType(Descriptors.FieldDescriptor.Type fieldType) { + return fieldType == Descriptors.FieldDescriptor.Type.ENUM; + } + + /** Defines is a protobuf message field type is a scalar type. */ + private static boolean isScalarType(Descriptors.FieldDescriptor.Type fieldType) { + return fieldType != Descriptors.FieldDescriptor.Type.MESSAGE + && fieldType != Descriptors.FieldDescriptor.Type.GROUP + && fieldType != Descriptors.FieldDescriptor.Type.BYTES; + } + + /** Build a leaf node that is an enum type. */ + private ObjectNode buildEnumTypeNode(Descriptors.FieldDescriptor fd) { + ObjectNode subschemaNode = mapper.createObjectNode(); + ArrayNode enumsArrayNode = mapper.createArrayNode(); + + subschemaNode.put("type", toMcpJsonType(fd.getType())); + subschemaNode.set("enum", enumsArrayNode); + + // Put the enum values in enum array. + for (Descriptors.EnumValueDescriptor evd : fd.getEnumType().getValues()) { + enumsArrayNode.add(evd.getName()); + } + return subschemaNode; + } + + /** Build a leaf node that is a scalar type. */ + private ObjectNode buildScalarTypeNode(Descriptors.FieldDescriptor fd) { + ObjectNode subschemaNode = mapper.createObjectNode(); + subschemaNode.put("type", toMcpJsonType(fd.getType())); + return subschemaNode; + } + + /** Convert a scalar Field Protobuf type into a MCP Json compatible one. */ + private static String toMcpJsonType(Descriptors.FieldDescriptor.Type fieldType) { + switch (fieldType) { + case DOUBLE: + case FLOAT: + case INT64: + case UINT64: + case INT32: + case FIXED64: + case FIXED32: + return "number"; + case BOOL: + return "boolean"; + case STRING: + case BYTES: + default: + return "string"; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMetadataUtil.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMetadataUtil.java new file mode 100644 index 000000000..dbe122894 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMetadataUtil.java @@ -0,0 +1,75 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.github.microcks.util.script.HttpHeadersStringToStringsMap; +import io.github.microcks.util.script.StringToStringsMap; +import io.grpc.Context; +import io.grpc.Metadata; + +import java.io.UnsupportedEncodingException; +import java.util.Set; + +import com.nimbusds.jose.util.StandardCharset; + +/** + * Helper class containing utility to deal with GrpcMetadata. + */ +public class GrpcMetadataUtil { + + /* Key used to pass gRPC metadata from interceptor to server via context */ + public static final Context.Key METADATA_CTX_KEY = Context.key("grpc-metadata"); + + private GrpcMetadataUtil() { + // Private constructor to hide the implicit public one. + } + + /** + * Convert Metadata sent with a gRPC requests into a map of key-value pairs with metadata key to list of values. + * + * @param metadata The gRPC Metadata to convert + * @return A StringToStringsMap containing all Metadata key-value pairs + * @throws UnsupportedEncodingException If metadata contains invalid binary value which could not be decoded into a + * string. + */ + public static StringToStringsMap convertToMap(Metadata metadata) throws UnsupportedEncodingException { + StringToStringsMap result = new HttpHeadersStringToStringsMap(); + + Set keys = metadata.keys(); + for (String key : keys) { + // Depending on the suffix of the key we either extract binary values or + // ASCII string values. + if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + // binary metadata entry + Metadata.Key metadataKey = Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER); + Iterable values = metadata.getAll(metadataKey); + + for (byte[] value : values) { + String stringValue = new String(value, StandardCharset.UTF_8); + result.add(key, stringValue); + } + } else { + Metadata.Key metadataKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + Iterable values = metadata.getAll(metadataKey); + for (String value : values) { + result.add(key, value); + } + } + } + + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMockHandlerRegistry.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMockHandlerRegistry.java new file mode 100644 index 000000000..f06813f79 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcMockHandlerRegistry.java @@ -0,0 +1,98 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.web.GrpcServerCallHandler; + +import io.grpc.HandlerRegistry; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCallHandler; +import io.grpc.ServerMethodDefinition; +import io.grpc.protobuf.ProtoUtils; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.stub.ServerCalls; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.stereotype.Component; + +import javax.annotation.Nullable; + +/** + * A GRPC HandlerRegistry that delegates server calls handling to GrpcServerCallHandler. + * @author laurent + */ +@Component +public class GrpcMockHandlerRegistry extends HandlerRegistry { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(GrpcMockHandlerRegistry.class); + + private static final String SERVER_REFLECTION_V1_METHOD = "grpc.reflection.v1.ServerReflection/ServerReflectionInfo"; + + private final GrpcServerCallHandler serverCallHandler; + + private final ProtoReflectionService reflectionService; + + /** + * Build a new GrpcMockHandlerRegistry with a callback handler. + * @param serverCallHandler The server callback handler to use + * @param serviceRepository The repository used to get access to service definitions + * @param resourceRepository The repository used to get access to resource definitions + */ + public GrpcMockHandlerRegistry(GrpcServerCallHandler serverCallHandler, ServiceRepository serviceRepository, + ResourceRepository resourceRepository) { + this.serverCallHandler = serverCallHandler; + this.reflectionService = new ProtoReflectionService(serviceRepository, resourceRepository); + } + + public ProtoReflectionService getReflectionService() { + return reflectionService; + } + + @Nullable + @Override + public ServerMethodDefinition lookupMethod(String fullMethodName, @Nullable String authority) { + log.debug("lookupMethod() with fullMethodName: {}", fullMethodName); + if (SERVER_REFLECTION_V1_METHOD.equals(fullMethodName)) { + return ServerMethodDefinition.create(reflectionMethodDescriptor(), reflectionServerCallHandler()); + } + return ServerMethodDefinition.create(mockMethodDescriptor(fullMethodName), mockServerCallHandler(fullMethodName)); + } + + protected MethodDescriptor reflectionMethodDescriptor() { + return MethodDescriptor + .newBuilder(ProtoUtils.marshaller(ServerReflectionRequest.getDefaultInstance()), + ProtoUtils.marshaller(ServerReflectionResponse.getDefaultInstance())) + .setType(MethodDescriptor.MethodType.BIDI_STREAMING).setFullMethodName(SERVER_REFLECTION_V1_METHOD) + .setSampledToLocalTracing(true).build(); + } + + protected ServerCallHandler reflectionServerCallHandler() { + return ServerCalls.asyncBidiStreamingCall(this.reflectionService::serverReflectionInfo); + } + + protected MethodDescriptor mockMethodDescriptor(String fullMethodName) { + return GrpcUtil.buildGenericUnaryMethodDescriptor(fullMethodName); + } + + protected ServerCallHandler mockServerCallHandler(String fullMethodName) { + return serverCallHandler.getUnaryServerCallHandler(fullMethodName); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcServerStarter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcServerStarter.java new file mode 100644 index 000000000..02f4a5554 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcServerStarter.java @@ -0,0 +1,213 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.TlsServerCredentials; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This is starter component for building, starting and managing shutdown of a GRPC server handling mock calls. + * @author laurent + */ +@Component +public class GrpcServerStarter { + + class HeaderInterceptor implements ServerInterceptor { + + @Override + public Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + log.info("Found headers for operation {}: {}", call.getMethodDescriptor().getFullMethodName(), headers.keys()); + Context context = Context.current().withValue(GrpcMetadataUtil.METADATA_CTX_KEY, headers); + return Contexts.interceptCall(context, call, headers, next); + } + + } + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(GrpcServerStarter.class); + + private static final String BEGIN_RSA_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----"; + private static final String END_RSA_PRIVATE_KEY = "-----END RSA PRIVATE KEY-----"; + + private final GrpcMockHandlerRegistry mockHandlerRegistry; + + @Value("${grpc.server.port:9090}") + private Integer serverPort = 9090; + + @Value("${grpc.server.certChainFilePath:}") + private String certChainFilePath = null; + + @Value("${grpc.server.privateKeyFilePath:}") + private String privateKeyFilePath = null; + + private AtomicBoolean isRunning = new AtomicBoolean(false); + + private CountDownLatch latch; + + public GrpcServerStarter(GrpcMockHandlerRegistry mockHandlerRegistry) { + this.mockHandlerRegistry = mockHandlerRegistry; + } + + @PostConstruct + public void startGrpcServer() { + try { + latch = new CountDownLatch(1); + Server grpcServer = null; + + // If cert and private key is provided, build a TLS capable server. + if (certChainFilePath != null && !certChainFilePath.isEmpty() && privateKeyFilePath != null + && !privateKeyFilePath.isEmpty()) { + grpcServer = buildTLSServer(); + } else { + // Else build a "plain text" server. + grpcServer = ServerBuilder.forPort(serverPort).fallbackHandlerRegistry(mockHandlerRegistry) + .intercept(new HeaderInterceptor()).build(); + } + grpcServer.start(); + log.info("GRPC Server started on port {}", serverPort); + + Server finalGrpcServer = grpcServer; + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + if (finalGrpcServer != null) { + log.info("Shutting down GRPC server since JVM is shutting down"); + finalGrpcServer.shutdown().awaitTermination(2, TimeUnit.SECONDS); + } + } catch (InterruptedException e) { + log.error("GRPC Server shutdown interrupted", e); + } + } + }); + startDaemonAwaitThread(); + } catch (Exception e) { + log.error("GRPC Server cannot be started", e); + } + } + + private Server buildTLSServer() throws IOException { + Server grpcServer = null; + TlsServerCredentials.Builder tlsBuilder = TlsServerCredentials.newBuilder() + .keyManager(new File(certChainFilePath), new File(privateKeyFilePath)); + + try { + grpcServer = Grpc.newServerBuilderForPort(serverPort, tlsBuilder.build()) + .fallbackHandlerRegistry(mockHandlerRegistry).intercept(new HeaderInterceptor()).build(); + } catch (IllegalArgumentException iae) { + if (iae.getCause() instanceof NoSuchAlgorithmException || iae.getCause() instanceof InvalidKeySpecException) { + + // Private key may be not directly recognized as the RSA keys generated by Helm chart genSelfSignedCert. + // Underlying Netty only supports PKCS#8 formmatted private key and key is detected as a key pair instead. + log.warn("GRPC PrivateKey appears to be invalid. Trying to convert it."); + + final byte[] privateKeyBytes = extractPrivateKeyIfAny(privateKeyFilePath); + if (privateKeyBytes != null) { + log.info("Building a GRPC server with converted key"); + try (FileInputStream certChainStream = new FileInputStream(certChainFilePath)) { + tlsBuilder = TlsServerCredentials.newBuilder().keyManager(certChainStream, + new ByteArrayInputStream(privateKeyBytes)); + grpcServer = Grpc.newServerBuilderForPort(serverPort, tlsBuilder.build()) + .addService(mockHandlerRegistry.getReflectionService()) + .fallbackHandlerRegistry(mockHandlerRegistry).intercept(new HeaderInterceptor()).build(); + } + } + } + } + return grpcServer; + } + + private void startDaemonAwaitThread() { + Thread awaitThread = new Thread(() -> { + try { + isRunning.set(true); + latch.await(); + } catch (InterruptedException e) { + log.error("GRPC Server awaiter interrupted.", e); + } finally { + isRunning.set(false); + } + }); + awaitThread.setName("grpc-server-awaiter"); + awaitThread.setDaemon(false); + awaitThread.start(); + } + + private static byte[] extractPrivateKeyIfAny(String privateKeyFilePath) throws IOException { + String privateKey = Files.readString(Path.of(privateKeyFilePath)); + if (privateKey.startsWith(BEGIN_RSA_PRIVATE_KEY)) { + try (PEMParser pemParser = new PEMParser(new FileReader(privateKeyFilePath))) { + Object object = pemParser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + + PrivateKey privatekey = null; + + log.debug("Parsed PrivateKey: {}", object); + if (object instanceof PEMKeyPair pemKeyPair) { + privatekey = converter.getPrivateKey(pemKeyPair.getPrivateKeyInfo()); + } + if (object instanceof PrivateKeyInfo privateKeyInfo) { + privatekey = converter.getPrivateKey(privateKeyInfo); + } + if (privatekey != null) { + log.debug("Found PrivateKey Algorithm: {}", privatekey.getAlgorithm()); // ex. RSA + log.debug("Found PrivateKey Format: {}", privatekey.getFormat()); // ex. PKCS#8 + + String privateKeyPem = BEGIN_RSA_PRIVATE_KEY + "\n" + + Base64.getEncoder().encodeToString(privatekey.getEncoded()) + "\n" + END_RSA_PRIVATE_KEY + "\n"; + log.debug("New PrivateKey PEM is {}", privateKeyPem); + return privateKeyPem.getBytes(StandardCharsets.UTF_8); + } + } + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcTestRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcTestRunner.java new file mode 100644 index 000000000..b38d6f9bd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcTestRunner.java @@ -0,0 +1,310 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.util.test.AbstractTestRunner; +import io.github.microcks.util.test.TestRunnerCommons; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; +import io.grpc.Deadline; +import io.grpc.ForwardingClientCall; +import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.TlsChannelCredentials; +import io.grpc.Status.Code; +import io.grpc.stub.ClientCalls; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; + +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * An extension of AbstractTestRunner that deals with GRPC calls. Response is received as byte array and then parsed as + * DynamicMessage to check that it conforms with Service Protobuf description. + * @author laurent + */ +public class GrpcTestRunner extends AbstractTestRunner { + + /* Call Option used to pass gRPC Metadata from client invocation to header client interceptor */ + public static final String CUSTOM_CALL_OPTION_NAME = "request-metadata"; + public static final CallOptions.Key METADATA_CUSTOM_CALL_OPTION = CallOptions.Key + .createWithDefault(CUSTOM_CALL_OPTION_NAME, null); + + class HeaderInterceptor implements ClientInterceptor { + + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { + /** + * Extension of the AttachHeadersInterceptor by allowing custom headers on each request, passed through via + * custom CallOption. + */ + @Override + public void start(Listener responseListener, Metadata headers) { + // Extract custom headers from CallOptions + Metadata customHeaders = callOptions.getOption(METADATA_CUSTOM_CALL_OPTION); + if (customHeaders != null) { + log.debug("Adding headers to client request: {}", customHeaders.keys()); + headers.merge(customHeaders); + } + super.start(responseListener, headers); + } + }; + + } + + } + + /** A simple logger for diagnostic messages. */ + private final static Logger log = LoggerFactory.getLogger(GrpcTestRunner.class); + + private long timeout = 10000L; + + private Secret secret; + + private final ResourceRepository resourceRepository; + + /** + * Build a new GrpcTestRunner. + * @param resourceRepository Access to resources repository + */ + public GrpcTestRunner(ResourceRepository resourceRepository) { + this.resourceRepository = resourceRepository; + } + + /** + * Set the timeout to apply for each request tests. + * @param timeout Timeout value in milliseconds. + */ + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + /** + * Set the Secret used for securing the requests. + * @param secret The Secret used or securing the requests. + */ + public void setSecret(Secret secret) { + this.secret = secret; + } + + @Override + public List runTest(Service service, Operation operation, TestResult testResult, List requests, + String endpointUrl, HttpMethod method) throws URISyntaxException, IOException { + + log.debug("Launching test run on {} for {} request(s)", endpointUrl, requests.size()); + + if (requests.isEmpty()) { + return null; + } + + // Initialize results. + List results = new ArrayList<>(); + + // Rebuild the GRPC fullMethodName. + String fullMethodName = service.getName() + "/" + operation.getName(); + + // Build a new GRPC Channel from endpoint URL. + URL endpoint = new URL(endpointUrl); + + ManagedChannel originChannel; + if (endpointUrl.startsWith("https://") || endpoint.getPort() == 443) { + TlsChannelCredentials.Builder tlsBuilder = TlsChannelCredentials.newBuilder(); + if (secret != null && secret.getCaCertPem() != null) { + // Install a trust manager with custom CA certificate. + tlsBuilder.trustManager(new ByteArrayInputStream(secret.getCaCertPem().getBytes(StandardCharsets.UTF_8))); + } else { + // Install a trust manager that accepts everything and does not validate certificate chains. + tlsBuilder.trustManager(new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // Accept everything. + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // Accept everything. + } + }); + } + // Build a Channel using the TLS Builder. + originChannel = Grpc.newChannelBuilderForAddress(endpoint.getHost(), endpoint.getPort(), tlsBuilder.build()) + .build(); + } else { + // Build a simple Channel using no creds (now default to plain text so usePlainText() is no longer necessary). + originChannel = Grpc + .newChannelBuilderForAddress(endpoint.getHost(), endpoint.getPort(), InsecureChannelCredentials.create()) + .build(); + } + // Add a custom header interceptor which adds the request-specific headers to + // every operation + ClientInterceptor headerInterceptor = new HeaderInterceptor(); + Channel channel = ClientInterceptors.intercept(originChannel, headerInterceptor); + + // In order to produce outgoing byte array, we need the Protobuf binary descriptor that should + // have been processed while importing the .proto schema for the service. + List resources = resourceRepository.findByServiceIdAndType(service.getId(), + ResourceType.PROTOBUF_DESCRIPTOR); + if (resources == null || resources.size() != 1) { + log.error("Could not found any pre-processed Protobuf binary descriptor..."); + results.add(new TestReturn(TestReturn.FAILURE_CODE, 0, + "Could not found any pre-processed Protobuf binary descriptor...", null, null)); + return results; + } + Resource pbResource = resources.get(0); + + Descriptors.MethodDescriptor md = null; + try { + md = GrpcUtil.findMethodDescriptor(pbResource.getContent(), service.getName(), operation.getName()); + } catch (Exception e) { + log.error("Protobuf descriptor cannot be read or parsed: {}", e.getMessage()); + results.add(new TestReturn(TestReturn.FAILURE_CODE, 0, + "Protobuf descriptor cannot be read or parsed: " + e.getMessage(), null, null)); + return results; + } + + // Use a builder for out type with a Json parser to merge content and build outMsg. + DynamicMessage.Builder reqBuilder = DynamicMessage.newBuilder(md.getInputType()); + DynamicMessage.Builder resBuilder = DynamicMessage.newBuilder(md.getOutputType()); + JsonFormat.Parser parser = JsonFormat.parser(); + JsonFormat.Printer printer = JsonFormat.printer(); + + for (Request request : requests) { + // Reset status code, message and request each time. + int code = TestReturn.SUCCESS_CODE; + String message = null; + String contentResponse = null; + String statusCode = null; + + reqBuilder.clear(); + resBuilder.clear(); + + // Now produce the request message byte array. + parser.merge(request.getContent(), reqBuilder); + byte[] requestBytes = reqBuilder.build().toByteArray(); + + CallOptions callOptions = CallOptions.DEFAULT.withDeadline(Deadline.after(timeout, TimeUnit.MILLISECONDS)); + + if (secret != null && secret.getToken() != null) { + log.debug("Secret contains token and maybe token header, adding them as call credentials"); + callOptions = callOptions + .withCallCredentials(new TokenCallCredentials(secret.getToken(), secret.getTokenHeader())); + } + // Add all headers as customOptions to callOptions + Set
headers = TestRunnerCommons.collectHeaders(testResult, request, operation); + callOptions = callOptions.withOption(METADATA_CUSTOM_CALL_OPTION, convertHeadersToMetadata(headers)); + + // Actually execute request. + long startTime = System.currentTimeMillis(); + byte[] responseBytes = null; + try { + responseBytes = ClientCalls.blockingUnaryCall(channel, + GrpcUtil.buildGenericUnaryMethodDescriptor(fullMethodName), callOptions, requestBytes); + } catch (StatusRuntimeException sre) { + log.error("StatusRuntimeException while executing grpc request {} on {}", fullMethodName, endpointUrl, sre); + code = TestReturn.FAILURE_CODE; + Status status = sre.getStatus(); + statusCode = status.getCode().name(); + message = String.format("Request failed with %s and description %s", statusCode, status.getDescription()); + } + long duration = System.currentTimeMillis() - startTime; + + // If still in success, validate and parse response + if (code == TestReturn.SUCCESS_CODE) { + statusCode = Code.OK.name(); + contentResponse = new String(responseBytes, StandardCharsets.UTF_8); + + try { + // Validate incoming message parsing a DynamicMessage. + DynamicMessage respMsg = DynamicMessage.parseFrom(md.getOutputType(), responseBytes); + + // Now update response content with readable content. + contentResponse = printer.print(respMsg); + } catch (InvalidProtocolBufferException ipbe) { + log.error("Received bytes cannot be transformed in {}", md.getOutputType().getFullName()); + code = TestReturn.FAILURE_CODE; + message = "Received bytes cannot be transformed in " + md.getOutputType().getFullName(); + } + } + + // Create a Response object for returning. + Response response = new Response(); + response.setStatus(statusCode); + response.setMediaType("application/x-protobuf"); + response.setContent(contentResponse); + + results.add(new TestReturn(code, duration, message, request, response)); + } + return results; + } + + private static Metadata convertHeadersToMetadata(Set
headers) { + Metadata metadata = new Metadata(); + for (Header header : headers) { + for (String value : header.getValues()) { + metadata.put(Metadata.Key.of(header.getName(), Metadata.ASCII_STRING_MARSHALLER), value); + } + } + return metadata; + } + + /** + * Build the HttpMethod corresponding to string. + */ + @Override + public HttpMethod buildMethod(String method) { + return HttpMethod.POST; + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcUtil.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcUtil.java new file mode 100644 index 000000000..df98668f6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/GrpcUtil.java @@ -0,0 +1,166 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.TypeRegistry; +import io.grpc.MethodDescriptor; +import io.grpc.protobuf.services.BinaryLogProvider; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +/** + * Helper class containing utility methods related to Grpc/Protobuf descriptors. + * @author laurent + */ +public class GrpcUtil { + + private GrpcUtil() { + // Private constructor to hide the implicit public one. + } + + /** + * Find a Protobuf service descriptor using a base64 encoded representation of the proto descriptor + service name. + * @param base64ProtobufDescriptor The encoded representation of proto descriptor as produced by protoc. + * @param serviceName The name of the service to get method for. + * @return A Protobuf ServiceDescriptor + * @throws InvalidProtocolBufferException If representation is not understood as protobuf descriptor. + * @throws Descriptors.DescriptorValidationException If included FileDescriptor cannot be validated. + */ + public static Descriptors.ServiceDescriptor findServiceDescriptor(String base64ProtobufDescriptor, + String serviceName) throws InvalidProtocolBufferException, Descriptors.DescriptorValidationException { + // Now we may have serviceName as being the FQDN. We have to find short version to later findServiceByName(). + String shortServiceName = serviceName; + if (serviceName.contains(".")) { + shortServiceName = serviceName.substring(serviceName.lastIndexOf(".") + 1); + } + + // Find descriptor with this service name as symbol. + Descriptors.FileDescriptor fd = findFileDescriptorBySymbol(base64ProtobufDescriptor, shortServiceName); + return fd.findServiceByName(shortServiceName); + } + + /** + * Find a Protobuf method descriptor using a base64 encoded representation of the proto descriptor + service and + * method name. + * @param base64ProtobufDescriptor The encoded representation of proto descriptor as produced by protoc. + * @param serviceName The name of the service to get method for. + * @param methodName The name of the method. + * @return A Protobuf MethodDescriptor + * @throws InvalidProtocolBufferException If representation is not understood as protobuf descriptor. + * @throws Descriptors.DescriptorValidationException If included FileDescriptor cannot be validated. + */ + public static Descriptors.MethodDescriptor findMethodDescriptor(String base64ProtobufDescriptor, String serviceName, + String methodName) throws InvalidProtocolBufferException, Descriptors.DescriptorValidationException { + + // Retrieve service descriptor first. + Descriptors.ServiceDescriptor sd = findServiceDescriptor(base64ProtobufDescriptor, serviceName); + return sd.findMethodByName(methodName); + } + + /** + * Get the Protobuf FileDescriptorSet from a base64 encoded representation of the proto descriptor. + * @param base64ProtobufDescriptor The encoded representation of proto descriptor as produced by protoc. + * @return A Protobuf FileDescriptorSet + * @throws InvalidProtocolBufferException + */ + public static DescriptorProtos.FileDescriptorSet getFileDescriptorSet(String base64ProtobufDescriptor) + throws InvalidProtocolBufferException { + // Protobuf binary descriptor has been encoded in base64 to be stored as a string. + // Decode it and recreate DescriptorProtos objects. + byte[] decodedBinaryPB = Base64.getDecoder().decode(base64ProtobufDescriptor.getBytes(StandardCharsets.UTF_8)); + return DescriptorProtos.FileDescriptorSet.parseFrom(decodedBinaryPB); + } + + /** + * Find a Protobuf file descriptor using a base64 encoded representation of the proto descriptor + symbol name. + * @param base64ProtobufDescriptor The encoded representation of proto descriptor as produced by protoc. + * @param symbol The name of a symbol to get descriptor for (can be a service, a message type or an + * extension). + * @return A Protobuf FileDescriptor + * @throws InvalidProtocolBufferException If representation is not understood as protobuf descriptor. + * @throws Descriptors.DescriptorValidationException If included FileDescriptor cannot be validated. + */ + public static Descriptors.FileDescriptor findFileDescriptorBySymbol(String base64ProtobufDescriptor, String symbol) + throws InvalidProtocolBufferException, Descriptors.DescriptorValidationException { + + // Get Descriptor objects corresponding to the base64 encoded descriptor. + DescriptorProtos.FileDescriptorSet fds = getFileDescriptorSet(base64ProtobufDescriptor); + + if (fds.getFileCount() > 1) { + // Build dependencies. + List dependencies = new ArrayList<>(); + for (int i = 0; i < fds.getFileCount(); i++) { + // Build descriptor and add to dependencies. + Descriptors.FileDescriptor fd = Descriptors.FileDescriptor.buildFrom(fds.getFile(i), + dependencies.toArray(new Descriptors.FileDescriptor[dependencies.size()]), true); + dependencies.add(fd); + + // Search for symbol. + if (fd.findServiceByName(symbol) != null || fd.findMessageTypeByName(symbol) != null + || fd.findExtensionByName(symbol) != null) { + return fd; + } + } + } + return Descriptors.FileDescriptor.buildFrom(fds.getFile(0), new Descriptors.FileDescriptor[] {}, true); + } + + /** + * Build a TypeRegistry for JSON parsing/serialization. Use the base64 encoded representation of the proto descriptor + * to extract types information. + * @param base64ProtobufDescriptor The encoded representation of proto descriptor as produced by protoc. + * @return A TYpeRegistry instance. + * @throws InvalidProtocolBufferException If representation is not understood as protobuf descriptor. + * @throws Descriptors.DescriptorValidationException If included FileDescriptor cannot be validated. + */ + public static TypeRegistry buildTypeRegistry(String base64ProtobufDescriptor) + throws InvalidProtocolBufferException, Descriptors.DescriptorValidationException { + // Get Descriptor objects corresponding to the base64 encoded descriptor. + DescriptorProtos.FileDescriptorSet fds = getFileDescriptorSet(base64ProtobufDescriptor); + + // Initialize a new TypeRegistry builder. + TypeRegistry.Builder registryBuilder = TypeRegistry.newBuilder(); + + // Build dependencies. + List dependencies = new ArrayList<>(); + for (int i = 0; i < fds.getFileCount(); i++) { + // Build descriptor and add to dependencies. + Descriptors.FileDescriptor fd = Descriptors.FileDescriptor.buildFrom(fds.getFile(i), + dependencies.toArray(new Descriptors.FileDescriptor[dependencies.size()]), true); + dependencies.add(fd); + + registryBuilder.add(fd.getMessageTypes()); + } + + return registryBuilder.build(); + } + + /** + * Build a generic GRPC Unary Method descriptor (using byte[] as input and byte[] as output. + * @param fullMethodName The GRPC method full name (service fqdn / method) + * @return A new MethodDescriptor using a byte array marshaller. + */ + public static MethodDescriptor buildGenericUnaryMethodDescriptor(String fullMethodName) { + return MethodDescriptor.newBuilder(BinaryLogProvider.BYTEARRAY_MARSHALLER, BinaryLogProvider.BYTEARRAY_MARSHALLER) + .setType(MethodDescriptor.MethodType.UNARY).setFullMethodName(fullMethodName).build(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/ProtoReflectionService.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/ProtoReflectionService.java new file mode 100644 index 000000000..2423e12d2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/ProtoReflectionService.java @@ -0,0 +1,272 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; + +import com.google.protobuf.Descriptors; +import io.grpc.BindableService; +import io.grpc.Status; +import io.grpc.reflection.v1alpha.ErrorResponse; +import io.grpc.reflection.v1alpha.FileDescriptorResponse; +import io.grpc.reflection.v1alpha.ListServiceResponse; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.reflection.v1alpha.ServiceResponse; +import io.grpc.stub.ServerCallStreamObserver; +import io.grpc.stub.StreamObserver; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * Provides a reflection service for GRPC services (excluding the reflection service itself) registered into this + * Microcks instance. + * @author laurent + */ +public class ProtoReflectionService extends ServerReflectionGrpc.ServerReflectionImplBase { + + final ServiceRepository serviceRepository; + final ResourceRepository resourceRepository; + + /** + * Build a new ProtoReflectionService using application repositories. + * @param serviceRepository Repository to get access to the list of GRPC services. + * @param resourceRepository Repository to get access to the list of Protobuf resources for services. + */ + public ProtoReflectionService(ServiceRepository serviceRepository, ResourceRepository resourceRepository) { + this.serviceRepository = serviceRepository; + this.resourceRepository = resourceRepository; + } + + /** + * Build a new ProtoReflectionService using application repositories. + * @param serviceRepository Repository to get access to the list of GRPC services. + * @param resourceRepository Repository to get access to the list of Protobuf resources for services. + */ + public static BindableService newInstance(ServiceRepository serviceRepository, + ResourceRepository resourceRepository) { + return new ProtoReflectionService(serviceRepository, resourceRepository); + } + + @Override + public StreamObserver serverReflectionInfo( + StreamObserver responseObserver) { + final ServerCallStreamObserver serverCallStreamObserver = (ServerCallStreamObserver) responseObserver; + ProtoReflectionStreamObserver requestObserver = new ProtoReflectionStreamObserver(serviceRepository, + resourceRepository, serverCallStreamObserver); + serverCallStreamObserver.setOnReadyHandler(requestObserver); + serverCallStreamObserver.disableAutoRequest(); + serverCallStreamObserver.request(1); + return requestObserver; + } + + private static class ProtoReflectionStreamObserver implements Runnable, StreamObserver { + + private final ServiceRepository serviceRepository; + private final ResourceRepository resourceRepository; + private final ServerCallStreamObserver serverCallStreamObserver; + private boolean closeAfterSend = false; + private ServerReflectionRequest request; + + ProtoReflectionStreamObserver(ServiceRepository serviceRepository, ResourceRepository resourceRepository, + ServerCallStreamObserver serverCallStreamObserver) { + this.serviceRepository = serviceRepository; + this.resourceRepository = resourceRepository; + this.serverCallStreamObserver = checkNotNull(serverCallStreamObserver, "observer"); + } + + @Override + public void run() { + if (request != null) { + handleReflectionRequest(); + } + } + + @Override + public void onNext(ServerReflectionRequest request) { + checkState(this.request == null); + this.request = checkNotNull(request); + handleReflectionRequest(); + } + + @Override + public void onCompleted() { + if (request != null) { + closeAfterSend = true; + } else { + serverCallStreamObserver.onCompleted(); + } + } + + @Override + public void onError(Throwable cause) { + serverCallStreamObserver.onError(cause); + } + + private void handleReflectionRequest() { + if (serverCallStreamObserver.isReady()) { + switch (request.getMessageRequestCase()) { + case FILE_BY_FILENAME: + getFileByName(request); + break; + case FILE_CONTAINING_SYMBOL: + getFileContainingSymbol(request); + break; + case FILE_CONTAINING_EXTENSION: + getFileByExtension(request); + break; + case ALL_EXTENSION_NUMBERS_OF_TYPE: + getAllExtensions(request); + break; + case LIST_SERVICES: + listServices(request); + break; + default: + sendErrorResponse(request, Status.Code.UNIMPLEMENTED, + "not implemented " + request.getMessageRequestCase()); + } + request = null; + if (closeAfterSend) { + serverCallStreamObserver.onCompleted(); + } else { + serverCallStreamObserver.request(1); + } + } + } + + private void getFileByName(ServerReflectionRequest request) { + sendErrorResponse(request, Status.Code.UNIMPLEMENTED, "not implemented " + request.getMessageRequestCase()); + } + + private void getFileContainingSymbol(ServerReflectionRequest request) { + String serviceName = request.getFileContainingSymbol(); + String version = getVersionFromServiceName(serviceName); + + Service service = serviceRepository.findByNameAndVersion(serviceName, version); + if (service != null) { + sendFileDescriptorForService(service, request); + } else { + // This could be a 'describe' request on a method or inner type like: + // grpcurl -plaintext localhost:9090 describe io.github.microcks.grpc.hello.v1.HelloService.greeting + serviceName = serviceName.substring(0, serviceName.lastIndexOf(".")); + version = getVersionFromServiceName(serviceName); + + service = serviceRepository.findByNameAndVersion(serviceName, version); + if (service != null) { + sendFileDescriptorForService(service, request); + } else { + sendErrorResponse(request, Status.Code.NOT_FOUND, "Symbol not found"); + } + } + } + + private void sendFileDescriptorForService(Service service, ServerReflectionRequest request) { + List resources = resourceRepository.findByServiceIdAndType(service.getId(), + ResourceType.PROTOBUF_DESCRIPTOR); + if (!resources.isEmpty()) { + Resource resource = resources.get(0); + + // Now we may have serviceName as being the FQDN. We have to find short version to later findServiceByName(). + String shortServiceName = service.getName(); + if (service.getName().contains(".")) { + shortServiceName = service.getName().substring(service.getName().lastIndexOf(".") + 1); + } + + try { + Descriptors.FileDescriptor fd = GrpcUtil.findFileDescriptorBySymbol(resource.getContent(), + shortServiceName); + serverCallStreamObserver.onNext(createServerReflectionResponse(request, fd)); + } catch (Exception e) { + sendErrorResponse(request, Status.Code.INTERNAL, "Unreadable protobuf descriptor"); + } + } else { + sendErrorResponse(request, Status.Code.INTERNAL, "No protobuf descriptor found"); + } + } + + private void getFileByExtension(ServerReflectionRequest request) { + sendErrorResponse(request, Status.Code.UNIMPLEMENTED, "not implemented " + request.getMessageRequestCase()); + } + + private void getAllExtensions(ServerReflectionRequest request) { + sendErrorResponse(request, Status.Code.UNIMPLEMENTED, "not implemented " + request.getMessageRequestCase()); + } + + private void listServices(ServerReflectionRequest request) { + ListServiceResponse.Builder builder = ListServiceResponse.newBuilder(); + + List services = serviceRepository.findByType(ServiceType.GRPC); + for (Service service : services) { + builder.addService(ServiceResponse.newBuilder().setName(service.getName())); + } + + serverCallStreamObserver.onNext(ServerReflectionResponse.newBuilder().setValidHost(request.getHost()) + .setOriginalRequest(request).setListServicesResponse(builder).build()); + } + + private void sendErrorResponse(ServerReflectionRequest request, Status.Code code, String message) { + ServerReflectionResponse response = ServerReflectionResponse.newBuilder().setValidHost(request.getHost()) + .setOriginalRequest(request) + .setErrorResponse(ErrorResponse.newBuilder().setErrorCode(code.value()).setErrorMessage(message)) + .build(); + serverCallStreamObserver.onNext(response); + } + + private ServerReflectionResponse createServerReflectionResponse(ServerReflectionRequest request, + Descriptors.FileDescriptor fd) { + FileDescriptorResponse.Builder fdRBuilder = FileDescriptorResponse.newBuilder(); + + Set seenFiles = new HashSet<>(); + Queue frontier = new ArrayDeque<>(); + seenFiles.add(fd.getName()); + frontier.add(fd); + while (!frontier.isEmpty()) { + Descriptors.FileDescriptor nextFd = frontier.remove(); + fdRBuilder.addFileDescriptorProto(nextFd.toProto().toByteString()); + for (Descriptors.FileDescriptor dependencyFd : nextFd.getDependencies()) { + if (!seenFiles.contains(dependencyFd.getName())) { + seenFiles.add(dependencyFd.getName()); + frontier.add(dependencyFd); + } + } + } + return ServerReflectionResponse.newBuilder().setValidHost(request.getHost()).setOriginalRequest(request) + .setFileDescriptorResponse(fdRBuilder).build(); + } + + private String getVersionFromServiceName(String serviceName) { + // Retrieve version from package name. + // org.acme package => org.acme version + // org.acme.v1 package => v1 version + String packageName = serviceName.substring(0, serviceName.lastIndexOf('.')); + String[] parts = packageName.split("\\."); + return (parts.length > 2 ? parts[parts.length - 1] : packageName); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/ProtobufImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/ProtobufImporter.java new file mode 100644 index 000000000..fc62a4855 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/ProtobufImporter.java @@ -0,0 +1,312 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; +import io.github.microcks.util.ReferenceResolver; + +import com.github.os72.protocjar.Protoc; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +/** + * An implementation of MockRepositoryImporter that deals with Protobuf v3 specification documents. + * @author laurent + */ +public class ProtobufImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ProtobufImporter.class); + + private static final String BINARY_DESCRIPTOR_EXT = ".pbb"; + private static final String BUILTIN_LIBRARY_PREFIX = "google/protobuf"; + + private final String protoDirectory; + private final String protoFileName; + private final ReferenceResolver referenceResolver; + private String specContent; + private DescriptorProtos.FileDescriptorSet fds; + + + /** + * Build a new importer. + * @param protoFilePath The path to local proto spec file + * @param referenceResolver An optional resolver for references present into the Protobuf file + * @throws IOException if project file cannot be found or read. + */ + public ProtobufImporter(String protoFilePath, ReferenceResolver referenceResolver) throws IOException { + this.referenceResolver = referenceResolver; + // Prepare file, path and name for easier process. + File protoFile = new File(protoFilePath); + protoDirectory = protoFile.getParentFile().getAbsolutePath(); + protoFileName = protoFile.getName(); + + // Prepare protoc arguments. + String[] args = { "-v3.21.8", "--include_std_types", "--include_imports", "--proto_path=" + protoDirectory, + "--descriptor_set_out=" + protoDirectory + "/" + protoFileName + BINARY_DESCRIPTOR_EXT, protoFileName }; + + // Track resolved imports (must be cleanup after success of failure). + List resolvedImportsLocalFiles = null; + try { + // Read spec bytes. + byte[] bytes = Files.readAllBytes(Paths.get(protoFilePath)); + specContent = new String(bytes, StandardCharsets.UTF_8); + + // Resolve and retrieve imports if any. + if (referenceResolver != null) { + resolvedImportsLocalFiles = new ArrayList<>(); + resolveAndPrepareRemoteImports(Paths.get(protoFilePath), resolvedImportsLocalFiles); + } + + // Run Protoc. + int result = Protoc.runProtoc(args); + + File protoFileB = new File(protoDirectory, protoFileName + BINARY_DESCRIPTOR_EXT); + fds = DescriptorProtos.FileDescriptorSet.parseFrom(new FileInputStream(protoFileB)); + } catch (InterruptedException ie) { + log.error("Protobuf schema compilation has been interrupted on {}", protoFilePath); + Thread.currentThread().interrupt(); + } catch (Exception e) { + throw new IOException("Protobuf schema file parsing error on " + protoFilePath + ": " + e.getMessage()); + } finally { + // Cleanup locally downloaded dependencies needed by protoc. + if (resolvedImportsLocalFiles != null) { + resolvedImportsLocalFiles.forEach(File::delete); + } + } + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List results = new ArrayList<>(); + + // Prepare dependencies. + List dependencies = new ArrayList<>(); + for (DescriptorProtos.FileDescriptorProto fdp : fds.getFileList()) { + // Retrieve version from package name. + // org.acme package => org.acme version + // org.acme.v1 package => v1 version + String packageName = fdp.getPackage(); + String[] parts = packageName.split("\\."); + String version = (parts.length > 2 ? parts[parts.length - 1] : packageName); + + Descriptors.FileDescriptor fd = null; + try { + fd = Descriptors.FileDescriptor.buildFrom(fdp, + dependencies.toArray(new Descriptors.FileDescriptor[dependencies.size()]), true); + dependencies.add(fd); + } catch (Descriptors.DescriptorValidationException e) { + throw new MockRepositoryImportException( + "Exception while building Protobuf descriptor, probably a missing dependency issue: " + + e.getMessage()); + } + + for (Descriptors.ServiceDescriptor sd : fd.getServices()) { + // Build a new service. + Service service = new Service(); + service.setName(sd.getFullName()); + service.setVersion(version); + service.setType(ServiceType.GRPC); + service.setXmlNS(fd.getPackage()); + + // Then build its operations. + service.setOperations(extractOperations(sd)); + + results.add(service); + } + } + return results; + } + + @Override + public List getResourceDefinitions(Service service) throws MockRepositoryImportException { + List results = new ArrayList<>(); + + // Build 2 resources: one with plain text, another with base64 encoded binary descriptor. + Resource textResource = new Resource(); + textResource.setName(service.getName() + "-" + service.getVersion() + ".proto"); + textResource.setType(ResourceType.PROTOBUF_SCHEMA); + textResource.setContent(specContent); + results.add(textResource); + + try { + byte[] binaryPB = Files.readAllBytes(Path.of(protoDirectory, protoFileName + BINARY_DESCRIPTOR_EXT)); + String base64PB = new String(Base64.getEncoder().encode(binaryPB), StandardCharsets.UTF_8); + + Resource descResource = new Resource(); + descResource.setName(service.getName() + "-" + service.getVersion() + BINARY_DESCRIPTOR_EXT); + descResource.setType(ResourceType.PROTOBUF_DESCRIPTOR); + descResource.setContent(base64PB); + results.add(descResource); + } catch (Exception e) { + log.error("Exception while encoding Protobuf binary descriptor into base64", e); + throw new MockRepositoryImportException("Exception while encoding Protobuf binary descriptor into base64"); + } + + // Now build resources for dependencies if any. + if (referenceResolver != null) { + referenceResolver.getRelativeResolvedReferences().forEach((p, f) -> { + Resource protoResource = new Resource(); + protoResource.setName(service.getName() + "-" + service.getVersion() + "-" + p.replace("/", "~1")); + protoResource.setType(ResourceType.PROTOBUF_SCHEMA); + protoResource.setPath(p); + try { + protoResource.setContent(Files.readString(f.toPath(), StandardCharsets.UTF_8)); + } catch (IOException ioe) { + log.warn("Exception while setting content of {} Protobuf resource", protoResource.getName(), ioe); + log.warn("Pursuing on next resource as it was for information purpose only"); + } + results.add(protoResource); + }); + referenceResolver.cleanResolvedReferences(); + } + return results; + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + return new ArrayList<>(); + } + + /** + * Analyse a protofile imports, resolve and retrieve them from remote to allow protoc to run later. + */ + private void resolveAndPrepareRemoteImports(Path protoFilePath, List resolvedImportsLocalFiles) { + String line = null; + try (BufferedReader reader = Files.newBufferedReader(protoFilePath, StandardCharsets.UTF_8)) { + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.startsWith("import ")) { + String importStr = line.substring("import ".length() + 1); + // Remove semicolon and quotes/double-quotes. + if (importStr.endsWith(";")) { + importStr = importStr.substring(0, importStr.length() - 1); + } + if (importStr.endsWith("\"") || importStr.endsWith("'")) { + importStr = importStr.substring(0, importStr.length() - 1); + } + if (importStr.startsWith("\"") || importStr.startsWith("'")) { + importStr = importStr.substring(1); + } + log.debug("Found an import to resolve in protobuf: {}", importStr); + + // Check that this lib is not in built-in ones. + if (!importStr.startsWith(BUILTIN_LIBRARY_PREFIX)) { + // Check if import path is locally there. + Path importPath = protoFilePath.getParent().resolve(importStr); + if (!Files.exists(importPath)) { + // Not there, so resolve it remotely and write to local file for protoc. + String importContent = referenceResolver.getReferenceContent(importStr, StandardCharsets.UTF_8); + try { + Files.createDirectories(importPath.getParent()); + Files.createFile(importPath); + } catch (FileAlreadyExistsException faee) { + log.warn("Exception while writing protobuf dependency", faee); + } + Files.write(importPath, importContent.getBytes(StandardCharsets.UTF_8)); + resolvedImportsLocalFiles.add(importPath.toFile()); + + // Now go down the resource content and resolve its own imports. + resolveAndPrepareRemoteImports(importPath, resolvedImportsLocalFiles); + } + } + } + } + } catch (Exception e) { + log.error("Exception while retrieving protobuf dependency", e); + } + } + + /** + * Extract the operations from GRPC service methods. + */ + private List extractOperations(Descriptors.ServiceDescriptor service) { + List results = new ArrayList<>(); + + for (Descriptors.MethodDescriptor method : service.getMethods()) { + Operation operation = new Operation(); + operation.setName(method.getName()); + if (method.getInputType() != null) { + operation.setInputName("." + method.getInputType().getFullName()); + + boolean hasOnlyPrimitiveArgs = hasOnlyPrimitiveArgs(method); + if (hasOnlyPrimitiveArgs && !method.getInputType().getFields().isEmpty()) { + operation.setDispatcher(DispatchStyles.QUERY_ARGS); + operation.setDispatcherRules(extractOperationParams(method.getInputType().getFields())); + } + } + if (method.getOutputType() != null) { + operation.setOutputName("." + method.getOutputType().getFullName()); + } + + results.add(operation); + } + return results; + } + + /** Check if a method has only primitive arguments. */ + private static boolean hasOnlyPrimitiveArgs(Descriptors.MethodDescriptor method) { + for (Descriptors.FieldDescriptor field : method.getInputType().getFields()) { + Descriptors.FieldDescriptor.Type fieldType = field.getType(); + if (!isScalarType(fieldType)) { + return false; + } + } + return true; + } + + /** Defines is a protobuf message field type is a scalar type. s */ + private static boolean isScalarType(Descriptors.FieldDescriptor.Type fieldType) { + return fieldType != Descriptors.FieldDescriptor.Type.MESSAGE + && fieldType != Descriptors.FieldDescriptor.Type.GROUP + && fieldType != Descriptors.FieldDescriptor.Type.BYTES; + } + + /** Build a string representing operation parameters as used in dispatcher rules (arg1 && arg2). */ + private static String extractOperationParams(List inputFields) { + StringBuilder builder = new StringBuilder(); + + for (Descriptors.FieldDescriptor inputField : inputFields) { + builder.append(inputField.getName()).append(" && "); + } + return builder.substring(0, builder.length() - 4); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/TokenCallCredentials.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/TokenCallCredentials.java new file mode 100644 index 000000000..2128f8ae0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/grpc/TokenCallCredentials.java @@ -0,0 +1,62 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.grpc.CallCredentials; +import io.grpc.Metadata; +import io.grpc.Status; + +import java.util.concurrent.Executor; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; + +/** + * A GRPC CallCredentials implementation adding token (Bearer or custom type) as call header. + * @author laurent + */ +public class TokenCallCredentials extends CallCredentials { + + public static final Metadata.Key AUTHORIZATION_METADATA_KEY = Metadata.Key.of("Authorization", + ASCII_STRING_MARSHALLER); + + public static final String BEARER_TYPE = "Bearer "; + + private final String token; + private final String tokenHeader; + + public TokenCallCredentials(String token, String tokenHeader) { + this.token = token; + this.tokenHeader = tokenHeader; + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, MetadataApplier metadataApplier) { + executor.execute(() -> { + try { + Metadata headers = new Metadata(); + // If no token header provided, assume it's an Authorization with Bearer type. + if (tokenHeader == null) { + headers.put(AUTHORIZATION_METADATA_KEY, BEARER_TYPE + token); + } else { + headers.put(Metadata.Key.of(tokenHeader, ASCII_STRING_MARSHALLER), token); + } + metadataApplier.apply(headers); + } catch (Throwable e) { + metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e)); + } + }); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/har/HARImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/har/HARImporter.java new file mode 100644 index 000000000..f3146ef9e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/har/HARImporter.java @@ -0,0 +1,576 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.har; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.OperationDefinition; +import graphql.parser.Parser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * An implementation of MockRepositoryImporter that deals with HAR or HTTP Archive 1.2 files. See + * https://w3c.github.io/web-performance/specs/HAR/Overview.html and http://www.softwareishard.com/blog/har-12-spec/ for + * explanations + * @author laurent + */ +public class HARImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(HARImporter.class); + + /** The starter marker for the comment referencing microcks service and version identifiers. */ + public static final String MICROCKS_ID_STARTER = "microcksId:"; + + /** The started marker for the comment referencing the prefix to identify API and remove from URLs. */ + public static final String API_PREFIX_STARTER = "apiPrefix:"; + + protected static final String REQUEST_NODE = "request"; + protected static final String RESPONSE_NODE = "response"; + + protected static final String GRAPHQL_VARIABLES_NODE = "variables"; + protected static final String GRAPHQL_QUERY_NODE = "query"; + + private ObjectMapper jsonMapper; + + private JsonNode spec; + + private String apiPrefix; + + private Map> operationToEntriesMap = new HashMap<>(); + + private static final List VALID_VERSIONS = List.of("1.1", "1.2"); + + private static final List INVALID_ENTRY_EXTENSIONS = List.of(".ico", ".html", ".css", ".js", ".png", ".jpg", + ".ttf", ".woff2"); + + private static final List UNWANTED_HEADERS = List.of("host", "sec-fetch-dest", "sec-fetch-mode", + "sec-fetch-user", "sec-fetch-site", "sec-gpc", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", + "upgrade-insecure-requests", "user-agent", "date", "vary"); + + + /** + * Build a new importer. + * @param specificationFilePath The path to local HAR archive file + * @throws IOException if project file cannot be found or read. + */ + public HARImporter(String specificationFilePath) throws IOException { + File specificationFile = new File(specificationFilePath); + jsonMapper = new ObjectMapper(); + spec = jsonMapper.readTree(specificationFile); + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List results = new ArrayList<>(); + + // Start checking version and comment. + String version = spec.path("log").path("version").asText(); + if (!VALID_VERSIONS.contains(version)) { + throw new MockRepositoryImportException( + "HAR version is not supported. Currently supporting: " + VALID_VERSIONS); + } + String comment = spec.path("log").path("comment").asText(); + if (comment == null || comment.length() == 0) { + throw new MockRepositoryImportException( + "Expecting a comment in HAR log to specify Microcks service identifier"); + } + + // We can start building something. + Service service = new Service(); + service.setType(ServiceType.REST); + + // 1st thing: look for comments to get service and version identifiers. + String[] commentLines = comment.split("\\r?\\n|\\r"); + for (String commentLine : commentLines) { + if (commentLine.trim().startsWith(MICROCKS_ID_STARTER)) { + String identifiers = commentLine.trim().substring(MICROCKS_ID_STARTER.length()); + + if (identifiers.indexOf(":") != -1) { + String[] serviceAndVersion = identifiers.split(":"); + service.setName(serviceAndVersion[0].trim()); + service.setVersion(serviceAndVersion[1].trim()); + } else { + log.error("microcksId comment is malformed. Expecting \'microcksId: :\'"); + throw new MockRepositoryImportException( + "microcksId comment is malformed. Expecting \'microcksId: :\'"); + } + } else if (commentLine.trim().startsWith(API_PREFIX_STARTER)) { + apiPrefix = commentLine.trim().substring(API_PREFIX_STARTER.length()).trim(); + log.info("Found an API prefix to use for shortening URLs: {}", apiPrefix); + } + } + if (service.getName() == null || service.getVersion() == null) { + log.error("No microcksId: comment found into GraphQL schema to get API name and version"); + throw new MockRepositoryImportException( + "No microcksId: comment found into GraphQL schema to get API name and version"); + } + + // Inspect requests content to determine the most probable service type. + Map requestsCounters = countRequestsByServiceType( + spec.path("log").path("entries").elements()); + if ((requestsCounters.get(ServiceType.GRAPHQL) > requestsCounters.get(ServiceType.REST)) + && (requestsCounters.get(ServiceType.GRAPHQL) > requestsCounters.get(ServiceType.SOAP_HTTP))) { + service.setType(ServiceType.GRAPHQL); + } else if ((requestsCounters.get(ServiceType.SOAP_HTTP) > requestsCounters.get(ServiceType.REST)) + && (requestsCounters.get(ServiceType.SOAP_HTTP) > requestsCounters.get(ServiceType.GRAPHQL))) { + service.setType(ServiceType.SOAP_HTTP); + } + + // Extract service operations. + service.setOperations(extractOperations(service.getType(), spec.path("log").path("entries").elements())); + + results.add(service); + return results; + } + + @Override + public List getResourceDefinitions(Service service) throws MockRepositoryImportException { + return new ArrayList<>(); + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + Map result = new HashMap<>(); + + Optional opOperation = operationToEntriesMap.keySet().stream() + .filter(op -> op.getName().equals(operation.getName())) + .filter(op -> op.getMethod().equals(operation.getMethod())).findFirst(); + + if (opOperation.isPresent()) { + // If we previously override the dispatcher with a Fallback, we must be sure to get wrapped elements. + DispatchCriteriaHelper.DispatcherDetails details = DispatchCriteriaHelper + .extractDispatcherWithRules(operation); + String rootDispatcher = details.rootDispatcher(); + String rootDispatcherRules = details.rootDispatcherRules(); + + List operationEntries = operationToEntriesMap.get(opOperation.get()); + for (JsonNode entry : operationEntries) { + JsonNode requestNode = entry.path(REQUEST_NODE); + JsonNode responseNode = entry.path(RESPONSE_NODE); + String requestUrl = shortenURL(requestNode.path("url").asText()); + log.debug("Extracting message definitions for entry url {}", requestUrl); + + // startedDateTIme is the only "unique" identifier for an entry... + String name = entry.path("startedDateTime").asText(); + Request request = buildRequest(requestNode, name); + + // Build dispatchCriteria before building response. + String dispatchCriteria = null; + + if (DispatchStyles.URI_PARAMS.equals(rootDispatcher)) { + dispatchCriteria = DispatchCriteriaHelper.extractFromURIParams(rootDispatcherRules, requestUrl); + } else if (DispatchStyles.URI_PARTS.equals(rootDispatcher)) { + // We may have null dispatcher rules here if just one request + // (as it prevents detecting patterns in URL and deducing parts) + if (rootDispatcherRules != null) { + dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(rootDispatcherRules, + removeVerbFromURL(operation.getName()), requestUrl); + } + } else if (DispatchStyles.URI_ELEMENTS.equals(rootDispatcher)) { + // We may have null dispatcher rules here if just one request + // (as it prevents detecting patterns in URL and deducing parts) + if (rootDispatcherRules != null) { + dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(rootDispatcherRules, + removeVerbFromURL(operation.getName()), requestUrl); + } + dispatchCriteria += DispatchCriteriaHelper.extractFromURIParams(rootDispatcherRules, requestUrl); + } else if (DispatchStyles.QUERY_ARGS.equals(rootDispatcher)) { + // This dispatcher is used for GraphQL, we have to extract variables from request body. + dispatchCriteria = extractGraphQLCriteria(rootDispatcherRules, request.getContent()); + } else { + // If dispatcher has been overriden (to SCRIPT for example), we should still put a generic resourcePath + // (maybe containing : parts) to later force operation matching at the mock controller level. Only do that + // when request url is not empty (means not the root url like POST /order). + if (requestUrl != null && requestUrl.length() > 0) { + operation.addResourcePath(requestUrl); + log.debug("Added operation generic resource path: {}", operation.getResourcePaths()); + } + } + + if (service.getType() == ServiceType.GRAPHQL) { + // We also have to shorten GraphQL body as we typically just display the query in Microcks. + //adaptGraphQLRequestContent(request); + } + + // Finalize with response and store. + Response response = buildResponse(responseNode, name, dispatchCriteria); + result.put(request, response); + } + } + + // Adapt map to list of Exchanges. + return result.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + + /** Try to guess and count the number of requests of each type (REST, GRAPHQL, SOAP). */ + private Map countRequestsByServiceType(Iterator entries) { + Map requestsCounters = new EnumMap<>(ServiceType.class); + requestsCounters.put(ServiceType.REST, 0); + requestsCounters.put(ServiceType.GRAPHQL, 0); + requestsCounters.put(ServiceType.SOAP_HTTP, 0); + + List candidateEntries = filterValidEntries(entries); + for (JsonNode entry : candidateEntries) { + + JsonNode responseContent = entry.path(RESPONSE_NODE).path("content"); + if (responseContent != null) { + String mimeType = responseContent.path("mimeType").asText(); + String responseText = getContentText(responseContent); + + ServiceType responseType = ServiceType.REST; + if ("application/json".equals(mimeType)) { + // Try to guess between REST and GRAPHQL... + String requestText = entry.path(REQUEST_NODE).path("postData").path("text").asText(); + + if (responseText.contains("\"data\":") && (requestText.contains(GRAPHQL_QUERY_NODE) + || requestText.contains("mutation") || requestText.contains("fragment"))) { + responseType = ServiceType.GRAPHQL; + } + } else if ("text/xml".equals(mimeType)) { + // Try to guess between REST and SOAP... + if (responseText.contains(":Envelope") && responseText.contains(":Body/>")) { + responseType = ServiceType.SOAP_HTTP; + } + } + requestsCounters.put(responseType, requestsCounters.get(responseType) + 1); + } + } + return requestsCounters; + } + + /** Extract Service Operations from entries. */ + private List extractOperations(ServiceType serviceType, Iterator entries) { + Map discoveredOperations = new HashMap<>(); + + List candidateEntries = filterValidEntries(entries); + + for (JsonNode entry : candidateEntries) { + String requestMethod = entry.path(REQUEST_NODE).path("method").asText(); + String requestUrl = entry.path(REQUEST_NODE).path("url").asText(); + String requestText = entry.path(REQUEST_NODE).path("postData").path("text").asText(); + + String baseRequestUrl = shortenURL(requestUrl); + String dispatcher = DispatchStyles.URI_PARTS; + if (baseRequestUrl.contains("?")) { + baseRequestUrl = baseRequestUrl.substring(0, baseRequestUrl.indexOf("?")); + dispatcher = DispatchStyles.URI_PARAMS; + } + + // This is OK for REST only. Has to be changed for SOAP and GRAPHQL. + String operationName = requestMethod + " " + baseRequestUrl; + if (serviceType == ServiceType.GRAPHQL) { + Parser requestParser = new Parser(); + try { + Document graphqlRequest = requestParser.parseDocument(extractGraphQLQuery(requestText)); + OperationDefinition graphqlOperation = (OperationDefinition) graphqlRequest.getDefinitions().get(0); + requestMethod = graphqlOperation.getOperation().toString(); + operationName = ((Field) graphqlOperation.getSelectionSet().getSelections().get(0)).getName(); + if ("QUERY".equals(requestMethod)) { + dispatcher = DispatchStyles.QUERY_ARGS; + } + } catch (Exception e) { + log.warn("Error parsing GraphQL request: {}", e.getMessage()); + } + } + + Operation existingOperation = discoveredOperations.get(operationName); + if (existingOperation == null) { + // Do we have other operation on different paths? + String[] newCandidatePaths = operationName.split("/"); + final String searchedMethod = requestMethod; + List similarOperations = discoveredOperations.values().stream() + .filter(op -> searchedMethod.equals(op.getMethod())) + .filter(op -> newCandidatePaths.length == op.getName().split("/").length).toList(); + + Operation mostSimilarOperation = findMostSimilarOperation(operationName, similarOperations); + if (mostSimilarOperation != null) { + List uris = List.of(removeVerbFromURL(mostSimilarOperation.getName()), + removeVerbFromURL(operationName)); + + String urlPart = DispatchCriteriaHelper.buildTemplateURLWithPartsFromURIs(uris); + mostSimilarOperation.setName(requestMethod + " " + urlPart); + + if (DispatchStyles.URI_PARAMS.equals(mostSimilarOperation.getDispatcher())) { + mostSimilarOperation.setDispatcher(DispatchStyles.URI_ELEMENTS); + } else { + mostSimilarOperation.setDispatcher(DispatchStyles.URI_PARTS); + mostSimilarOperation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIs(uris)); + mostSimilarOperation.addResourcePath(uris.get(0)); + mostSimilarOperation.addResourcePath(uris.get(1)); + } + // Add current entry to similar operation. + operationToEntriesMap.get(mostSimilarOperation).add(entry); + } else { + // If finally we get here, it's time to build a new operation ;-) + Operation operation = new Operation(); + operation.setName(operationName); + operation.setMethod(requestMethod); + operation.setDispatcher(dispatcher); + + if (DispatchStyles.URI_PARAMS.equals(dispatcher)) { + operation.setDispatcherRules(DispatchCriteriaHelper.extractParamsFromURI(requestUrl)); + } else if (DispatchStyles.URI_PARTS.equals(dispatcher)) { + operation.addResourcePath(baseRequestUrl); + } + + // Store this operation and initialize its entries. + discoveredOperations.put(operationName, operation); + List operationEntries = new ArrayList<>(); + operationEntries.add(entry); + operationToEntriesMap.put(operation, operationEntries); + } + } else { + // Add current entry to existing operation. + operationToEntriesMap.get(existingOperation).add(entry); + } + } + + return discoveredOperations.values().stream().toList(); + } + + /** Filter valid entries in HAR, removing static resources and calls not having the correct prefix if specified. */ + private List filterValidEntries(Iterator entries) { + return Stream.generate(() -> null).takeWhile(x -> entries.hasNext()).map(next -> entries.next()).filter(entry -> { + String url = entry.path(REQUEST_NODE).path("url").asText(); + String extension = url.substring(url.lastIndexOf(".")); + return !INVALID_ENTRY_EXTENSIONS.contains(extension); + }).filter(entry -> { + if (apiPrefix != null) { + // Filter by api prefix if provided. + String url = entry.path(REQUEST_NODE).path("url").asText(); + return url.startsWith(apiPrefix) || removeProtocolAndHostPort(url).startsWith(apiPrefix); + } + return true; + }).toList(); + } + + /** Find the most similar operation among candidates. */ + private Operation findMostSimilarOperation(String operationName, List similarOperations) { + Operation mostSimilarOperation = null; + if (!similarOperations.isEmpty()) { + int maxScore = 0; + for (Operation similarOperation : similarOperations) { + int score = getSimilarityScore(similarOperation.getName(), operationName); + if (score > 70 && score > maxScore) { + maxScore = score; + mostSimilarOperation = similarOperation; + } + } + } + return mostSimilarOperation; + } + + /** Compute a similarity score between a base URL and a candidate one. The greater, the better. */ + private int getSimilarityScore(String base, String candidate) { + int similarityScore = 0; + int commonPrefixSize = 0; + String[] basePaths = base.split("/"); + String[] candidatePaths = candidate.split("/"); + int stepScore = 100 / basePaths.length; + boolean stillCommon = true; + + for (int i = 0; i < basePaths.length; i++) { + String basePath = basePaths[i]; + String candidatePath = candidatePaths[i]; + if (basePath.equals(candidatePath)) { + if (stillCommon) { + commonPrefixSize++; + } + similarityScore += stepScore; + } else { + stillCommon = false; + } + } + return similarityScore + (basePaths.length * commonPrefixSize); + } + + /** Build Microcks request from HAR request. */ + private Request buildRequest(JsonNode requestNode, String name) { + Request request = new Request(); + request.setName(name); + request.setHeaders(buildHeaders(requestNode.path("headers"))); + request.setContent(requestNode.path("postData").path("text").asText()); + + JsonNode queryStringNode = requestNode.path("queryString"); + if (!queryStringNode.isMissingNode() && queryStringNode.isArray()) { + Iterator queryStrings = queryStringNode.elements(); + while (queryStrings.hasNext()) { + JsonNode queryString = queryStrings.next(); + Parameter param = new Parameter(); + param.setName(queryString.path("name").asText()); + param.setValue(queryString.path("value").asText()); + request.addQueryParameter(param); + } + } + return request; + } + + /** Build Microcks response from HAR response. */ + private Response buildResponse(JsonNode responseNode, String name, String dispatchCriteria) { + Response response = new Response(); + response.setName(name); + response.setStatus(responseNode.path("status").asText("200")); + response.setHeaders(buildHeaders(responseNode.path("headers"))); + response.setContent(getContentText(responseNode.path("content"))); + response.setDispatchCriteria(dispatchCriteria); + + if (response.getHeaders() != null) { + for (Header header : response.getHeaders()) { + if (header.getName().equalsIgnoreCase("Content-Type")) { + response.setMediaType(header.getValues().toArray(new String[] {})[0]); + } + } + } + + return response; + } + + /** Build Microcks headers from HAR headers. */ + private Set
buildHeaders(JsonNode headerNode) { + Map headers = new HashMap<>(); + Iterator items = headerNode.elements(); + while (items.hasNext()) { + JsonNode item = items.next(); + // Filter unwanted headers that are typically sent by browsers and + // meaningless in the context of API/Service exchanges. + String name = item.path("name").asText(); + if (!UNWANTED_HEADERS.contains(name.toLowerCase())) { + Header header = headers.computeIfAbsent(name, k -> { + Header h = new Header(); + h.setName(k); + h.setValues(new HashSet<>()); + return h; + }); + header.getValues().add(item.path("value").asText()); + } + } + return headers.values().stream().collect(Collectors.toSet()); + } + + /** Extract response content as string from HAR response content. */ + private String getContentText(JsonNode contentNode) { + String text = contentNode.path("text").asText(); + if ("base64".equals(contentNode.path("encoding").asText())) { + text = new String(Base64.getDecoder().decode(text)); + } + return text; + } + + /** Shorten an entry URL, removing protocol and host and API prefix if provided. */ + private String shortenURL(String url) { + if (apiPrefix != null) { + if (apiPrefix.startsWith("http://") || apiPrefix.startsWith("https://")) { + // Prefix includes protocol and port, using it as is. + return url.substring(apiPrefix.length() + 1); + } else { + // Start removing protocol and host and then the prefix. + url = removeProtocolAndHostPort(url); + return removeApiPrefix(url, apiPrefix); + } + } + return removeProtocolAndHostPort(url); + } + + private String extractGraphQLQuery(String requestContent) { + try { + JsonNode requestNode = jsonMapper.readTree(requestContent); + return requestNode.path(GRAPHQL_QUERY_NODE).asText(); + } catch (Exception e) { + log.error("Exception while extracting dispatch criteria from GraphQL variables: {}", e.getMessage(), e); + } + return ""; + } + + private String extractGraphQLCriteria(String dispatcherRules, String requestContent) { + String dispatchCriteria = ""; + try { + JsonNode requestNode = jsonMapper.readTree(requestContent); + if (requestNode.has(GRAPHQL_VARIABLES_NODE)) { + JsonNode variablesNode = requestNode.path(GRAPHQL_VARIABLES_NODE); + Map paramsMap = jsonMapper.convertValue(variablesNode, + TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class)); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, paramsMap); + } + } catch (Exception e) { + log.error("Exception while extracting dispatch criteria from GraphQL variables: {}", e.getMessage(), e); + } + return dispatchCriteria; + } + + private static String removeApiPrefix(String url, String apiPrefix) { + if (url.startsWith(apiPrefix)) { + return url.substring(apiPrefix.length()); + } + return url; + } + + private static String removeVerbFromURL(String operationName) { + return operationName.substring(operationName.indexOf(" ") + 1); + } + + private static String removeProtocolAndHostPort(String url) { + if (url.startsWith("https://")) { + url = url.substring(8); + } + if (url.startsWith("http://")) { + url = url.substring(7); + } + // Remove host and port specification if any. + if (url.indexOf('/') != -1) { + url = url.substring(url.indexOf('/')); + } + return url; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/http/HttpHeadersUtil.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/http/HttpHeadersUtil.java new file mode 100644 index 000000000..21ef2008e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/http/HttpHeadersUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.http; + +import java.util.Collections; + +import io.github.microcks.util.script.HttpHeadersStringToStringsMap; +import io.github.microcks.util.script.StringToStringsMap; +import jakarta.servlet.http.HttpServletRequest; + +/** + * Helper class containing utility to deal with Http Headers + */ +public class HttpHeadersUtil { + + private HttpHeadersUtil() { + // Private constructor to hide the implicit public one. + } + + /** + * Extract headers from HttpServletRequest into a map of key-value pairs with header name to list of values. + * + * @param request The original HttpServletRequest from which the headers should be extracted + * @return A StringToStringsMap containing all headers + */ + public static StringToStringsMap extractFromHttpServletRequest(HttpServletRequest request) { + StringToStringsMap headers = new HttpHeadersStringToStringsMap(); + if (request != null) { + for (String headerName : Collections.list(request.getHeaderNames())) { + headers.put(headerName, Collections.list(request.getHeaders(headerName))); + } + } + return headers; + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/ExamplesExporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/ExamplesExporter.java new file mode 100644 index 000000000..979691e7d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/ExamplesExporter.java @@ -0,0 +1,178 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.metadata; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Message; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.MockRepositoryExportException; +import io.github.microcks.util.MockRepositoryExporter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Mock repository exporter that exports Microcks domain objects definitions into the APIExamples YAML format. + * @author laurent + */ +public class ExamplesExporter implements MockRepositoryExporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ExamplesExporter.class); + + private final ObjectMapper mapper; + private Service service; + private final Map> operationsExchanges = new HashMap<>(); + + /** Create a new ExamplesExporter. */ + public ExamplesExporter() { + mapper = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)); + } + + @Override + public void addServiceDefinition(Service service) throws MockRepositoryExportException { + if (this.service != null) { + log.error("ExamplesExporter only allows one service definition"); + throw new MockRepositoryExportException("Service definition already set"); + } + this.service = service; + } + + @Override + public void addMessageDefinitions(Service service, Operation operation, List exchanges) + throws MockRepositoryExportException { + if (this.service != null && !this.service.getId().equals(service.getId())) { + log.error("ExamplesExporter only allows one service definition, this one is different"); + throw new MockRepositoryExportException("Service definition doesn't match with the one already set"); + } + if (this.service == null) { + this.service = service; + } + this.operationsExchanges.put(operation, exchanges); + } + + @Override + public String exportAsString() throws MockRepositoryExportException { + ObjectNode rootNode = mapper.createObjectNode(); + + // Initialize header and metadata. + rootNode.put("apiVersion", "mocks.microcks.io/v1alpha1"); + rootNode.put("kind", "APIExamples"); + rootNode.set("metadata", + mapper.createObjectNode().put("name", service.getName()).put("version", service.getVersion())); + + + ObjectNode operationsNode = rootNode.putObject("operations"); + for (Operation operation : operationsExchanges.keySet()) { + // Initialize a node for the operation. + ObjectNode operationNode = operationsNode.putObject(operation.getName()); + List exchanges = operationsExchanges.get(operation); + + for (Exchange exchange : exchanges) { + exportExchange(operationNode, exchange); + } + } + + // Output the root node. + String yamlResult = null; + try { + yamlResult = mapper.writeValueAsString(rootNode); + } catch (Exception e) { + log.error("Exception while writing YAML export", e); + throw new MockRepositoryExportException("Exception while writing YAML export", e); + } + return yamlResult; + } + + private void exportExchange(ObjectNode operationNode, Exchange exchange) { + if (ServiceType.EVENT.equals(service.getType())) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + exportUnidirectionalEvent(operationNode, event); + } else { + RequestResponsePair pair = (RequestResponsePair) exchange; + exportRequestResponsePair(operationNode, pair); + } + } + + private void exportUnidirectionalEvent(ObjectNode operationNode, UnidirectionalEvent event) { + EventMessage eventMessage = event.getEventMessage(); + ObjectNode exchangeNode = operationNode.putObject(eventMessage.getName()); + ObjectNode messageNode = exchangeNode.putObject("eventMessage"); + + exportHeaders(messageNode, eventMessage); + messageNode.put("payload", eventMessage.getContent()); + } + + private void exportRequestResponsePair(ObjectNode operationNode, RequestResponsePair pair) { + Request request = pair.getRequest(); + Response response = pair.getResponse(); + + ObjectNode exchangeNode = operationNode.putObject(request.getName()); + ObjectNode requestNode = exchangeNode.putObject("request"); + ObjectNode responseNode = exchangeNode.putObject("response"); + + // Export the request into the request node. + exportHeaders(requestNode, request); + if (request.getQueryParameters() != null && !request.getQueryParameters().isEmpty()) { + ObjectNode parametersNode = requestNode.putObject("parameters"); + for (Parameter parameter : request.getQueryParameters()) { + parametersNode.put(parameter.getName(), parameter.getValue()); + } + } + if (request.getContent() != null) { + requestNode.put("body", request.getContent()); + } + + // Export the response into the response node. + exportHeaders(responseNode, response); + if (response.getStatus() != null) { + responseNode.put("status", response.getStatus()); + } + if (response.getMediaType() != null) { + responseNode.put("mediaType", response.getMediaType()); + } + if (response.getContent() != null) { + responseNode.put("body", response.getContent()); + } + } + + private void exportHeaders(ObjectNode messageNode, Message message) { + if (message.getHeaders() != null && !message.getHeaders().isEmpty()) { + ObjectNode headersNode = messageNode.putObject("headers"); + for (Header header : message.getHeaders()) { + headersNode.put(header.getName(), header.getValues().stream().findFirst().get()); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/ExamplesImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/ExamplesImporter.java new file mode 100644 index 000000000..313d8be56 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/ExamplesImporter.java @@ -0,0 +1,413 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.metadata; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Message; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; +import io.github.microcks.util.URIBuilder; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * Mock repository importer that uses a {@code ApiExamples} YAML descriptor as a source artifact. + * @author laurent + */ +public class ExamplesImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ExamplesImporter.class); + + private static final String REQUEST_NODE = "request"; + private static final String RESPONSE_NODE = "response"; + private static final String PARAMETERS_NODE = "parameters"; + private static final String HEADERS_NODE = "headers"; + private static final String BODY_NODE = "body"; + private static final String PAYLOAD_NODE = "payload"; + + private final ObjectMapper mapper; + private final JsonNode spec; + + /** + * Build a new importer. + * @param specificationFilePath The path to local APIExamples spec file + * @throws IOException if project file cannot be found or read. + */ + public ExamplesImporter(String specificationFilePath) throws IOException { + try { + // Read spec bytes. + byte[] bytes = Files.readAllBytes(Paths.get(specificationFilePath)); + String specContent = new String(bytes, StandardCharsets.UTF_8); + + // Convert them to Node using Jackson object mapper. + mapper = new ObjectMapper(new YAMLFactory()); + spec = mapper.readTree(specContent.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + log.error("Exception while parsing APIMetadata specification file " + specificationFilePath, e); + throw new IOException("APIMetadata spec file parsing error"); + } + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List result = new ArrayList<>(); + + // Build a new service. + Service service = new Service(); + + JsonNode metadataNode = spec.get("metadata"); + if (metadataNode == null) { + log.error("Missing mandatory metadata in {}", spec.asText()); + throw new MockRepositoryImportException("Mandatory metadata property is missing in APIMetadata"); + } + service.setName(metadataNode.path("name").asText()); + service.setVersion(metadataNode.path("version").asText()); + + // Then build its operations. + service.setOperations(extractOperations()); + + result.add(service); + + return result; + } + + @Override + public List getResourceDefinitions(Service service) throws MockRepositoryImportException { + return Collections.emptyList(); + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + List result = new ArrayList<>(); + + // Iterate on specification "operations" nodes. + Iterator> operations = spec.path("operations").fields(); + while (operations.hasNext()) { + Map.Entry operationEntry = operations.next(); + + // Select the matching operation. + if (operation.getName().equals(operationEntry.getKey())) { + // Browse examples and extract exchanges. + Iterator> examples = operationEntry.getValue().fields(); + while (examples.hasNext()) { + Map.Entry exampleEntry = examples.next(); + + // Extract and add this exchange. + result.add(extractExchange(service, operation, exampleEntry.getKey(), exampleEntry.getValue())); + } + } + } + return result; + } + + /** Extract the list of operations from Specification. */ + private List extractOperations() { + List results = new ArrayList<>(); + + // Iterate on specification "operations" nodes. + Iterator> operations = spec.path("operations").fields(); + while (operations.hasNext()) { + Map.Entry operation = operations.next(); + + // Build a new operation. + Operation op = new Operation(); + op.setName(operation.getKey()); + + results.add(op); + } + return results; + } + + /** Extract exchange information from an example node. */ + private Exchange extractExchange(Service service, Operation operation, String exampleName, JsonNode exampleNode) { + Exchange exchange; + if (ServiceType.EVENT.equals(service.getType())) { + exchange = extractUnidirectionalEvent(exampleName, exampleNode); + } else { + exchange = extractRequestResponsePair(service, operation, exampleName, exampleNode); + } + return exchange; + } + + /** Extract unidirectional event messages from an example node. */ + private UnidirectionalEvent extractUnidirectionalEvent(String exampleName, JsonNode exampleNode) { + if (exampleNode.has("eventMessage")) { + JsonNode eventNode = exampleNode.get("eventMessage"); + + EventMessage event = new EventMessage(); + event.setName(exampleName); + + if (eventNode.has(HEADERS_NODE)) { + completeWithHeaders(event, eventNode.get(HEADERS_NODE)); + } + event.setContent(getSerializedValue(eventNode.get(PAYLOAD_NODE))); + + return new UnidirectionalEvent(event); + } + return null; + } + + /** Extract request/response pair from an example node. */ + private RequestResponsePair extractRequestResponsePair(Service service, Operation operation, String exampleName, + JsonNode exampleNode) { + // Build and return something only if request and response. + if (exampleNode.has(REQUEST_NODE) && exampleNode.has(RESPONSE_NODE)) { + JsonNode requestNode = exampleNode.get(REQUEST_NODE); + JsonNode responseNode = exampleNode.get(RESPONSE_NODE); + + // Initialize and complete the request. + Request request = new Request(); + request.setName(exampleName); + request.setContent(getSerializedValue(requestNode.get(BODY_NODE))); + + Multimap parameters = null; + if (requestNode.has(PARAMETERS_NODE)) { + parameters = completeWithParameters(request, requestNode.get(PARAMETERS_NODE)); + } + if (requestNode.has(HEADERS_NODE)) { + completeWithHeaders(request, requestNode.get(HEADERS_NODE)); + } + + // Initialize and complete the response. + Response response = new Response(); + response.setName(exampleName); + // Default response status + response.setStatus("200"); + + if (responseNode.has(HEADERS_NODE)) { + completeWithHeaders(response, responseNode.get(HEADERS_NODE)); + } + if (responseNode.has("mediaType")) { + String mediaType = responseNode.get("mediaType").asText(); + response.setMediaType(mediaType); + } + if (responseNode.has("status")) { + String status = responseNode.get("status").asText(); + response.setStatus(status); + if (!status.startsWith("2") || !status.startsWith("3")) { + response.setFault(true); + } + } + response.setContent(getSerializedValue(responseNode.get(BODY_NODE))); + + // Finally, take care about dispatchCriteria and complete operation resourcePaths. + // If we previously override the dispatcher with a Fallback, we must be sure to get wrapped elements. + DispatchCriteriaHelper.DispatcherDetails details = DispatchCriteriaHelper + .extractDispatcherWithRules(operation); + + // Finally, take care about dispatchCriteria and complete operation resourcePaths. + completeDispatchCriteriaAndResourcePaths(service, operation, details.rootDispatcher(), + details.rootDispatcherRules(), parameters, requestNode, responseNode, request, response); + + return new RequestResponsePair(request, response); + } + return null; + } + + /** Complete a request by extracting parameters. */ + private Multimap completeWithParameters(Request request, JsonNode parametersNode) { + Multimap result = ArrayListMultimap.create(); + Iterator> parameters = parametersNode.fields(); + while (parameters.hasNext()) { + Map.Entry parameterNode = parameters.next(); + + Parameter parameter = new Parameter(); + parameter.setName(parameterNode.getKey()); + parameter.setValue(getSerializedValue(parameterNode.getValue())); + request.addQueryParameter(parameter); + + // Depending on node type, extract different representations to MultiMap result. + if (parameterNode.getValue().isArray()) { + for (JsonNode current : parameterNode.getValue()) { + result.put(parameterNode.getKey(), getSerializedValue(current)); + } + } else if (parameterNode.getValue().isObject()) { + final var fieldsIterator = parameterNode.getValue().fields(); + while (fieldsIterator.hasNext()) { + var current = fieldsIterator.next(); + result.put(current.getKey(), getSerializedValue(current.getValue())); + } + } else { + result.put(parameterNode.getKey(), getSerializedValue(parameterNode.getValue())); + } + } + return result; + } + + /** Complete a message by extracting headers. */ + private void completeWithHeaders(Message message, JsonNode headersNode) { + Iterator> headers = headersNode.fields(); + while (headers.hasNext()) { + Map.Entry headerNode = headers.next(); + + Header header = new Header(); + header.setName(headerNode.getKey()); + // Header value may be multiple CSV. + Set values = Arrays.stream(getSerializedValue(headerNode.getValue()).split(",")).map(String::trim) + .collect(Collectors.toSet()); + header.setValues(values); + + message.addHeader(header); + } + } + + /** */ + private void completeDispatchCriteriaAndResourcePaths(Service service, Operation operation, String rootDispatcher, + String rootDispatcherRules, Multimap parameters, JsonNode requestNode, JsonNode responseNode, + Request request, Response response) { + String dispatchCriteria = null; + String resourcePathPattern = operation.getName().contains(" ") ? operation.getName().split(" ")[1] + : operation.getName(); + + if (DispatchStyles.URI_PARAMS.equals(rootDispatcher)) { + dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, parameters); + // We only need the pattern here. + operation.addResourcePath(resourcePathPattern); + } else if (DispatchStyles.URI_PARTS.equals(rootDispatcher)) { + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parameters); + // We should complete resourcePath here. + String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parameters); + operation.addResourcePath(resourcePath); + } else if (DispatchStyles.URI_ELEMENTS.equals(rootDispatcher)) { + // Split parameters between path and query. + Multimap pathParameters = Multimaps.filterEntries(parameters, + entry -> operation.getName().contains("/{" + entry.getKey() + "}") + || operation.getName().contains("/:" + entry.getKey())); + Multimap queryParameters = Multimaps.filterEntries(parameters, + entry -> !pathParameters.containsKey(entry.getKey())); + + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, pathParameters); + dispatchCriteria += DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, queryParameters); + // We should complete resourcePath here. + String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parameters); + operation.addResourcePath(resourcePath); + } else if (DispatchStyles.QUERY_HEADER.equals(rootDispatcher)) { + if (requestNode.has(HEADERS_NODE)) { + JsonNode headersNode = requestNode.get(HEADERS_NODE); + Map headersMap = extractHeaders(headersNode); + log.debug("Headers map for dispatchCriteria computation: {}", headersMap); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(rootDispatcherRules, headersMap); + } else { + log.debug("Missing headers node in request node"); + } + } else if (DispatchStyles.QUERY_ARGS.equals(rootDispatcher)) { + if (ServiceType.GRAPHQL.equals(service.getType()) && requestNode.has(BODY_NODE)) { + JsonNode variables = requestNode.get(BODY_NODE).path("variables"); + dispatchCriteria = extractQueryArgsCriteria(rootDispatcherRules, variables); + } else if (ServiceType.GRPC.equals(service.getType())) { + dispatchCriteria = extractQueryArgsCriteria(rootDispatcherRules, request.getContent()); + } + } else if (responseNode.has("dispatchCriteria")) { + dispatchCriteria = responseNode.get("dispatchCriteria").asText(); + } + + // In any case (dispatcher forced via Metadata or set to SCRIPT, we should still put a generic resourcePath + // (maybe containing {} parts) to later force operation matching at the mock controller level. + operation.addResourcePath(resourcePathPattern); + + response.setDispatchCriteria(dispatchCriteria); + } + + /** Extract the QUERY_ARGS Dispatch criteria from the variable provided as JSON string representation. */ + private String extractQueryArgsCriteria(String dispatcherRules, JsonNode variables) { + String dispatchCriteria = ""; + try { + Map paramsMap = mapper.convertValue(variables, + TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class)); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, paramsMap); + } catch (Exception e) { + log.error("Exception while converting dispatch criteria from JSON body: {}", e.getMessage()); + } + return dispatchCriteria; + } + + /** Extract the QUERY_ARGS Dispatch criteria from the variable provided as JSON string representation. */ + private String extractQueryArgsCriteria(String dispatcherRules, String variables) { + String dispatchCriteria = ""; + try { + Map paramsMap = mapper.readValue(variables, + TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class)); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, paramsMap); + } catch (Exception e) { + log.error("Exception while extracting dispatch criteria from JSON body: {}", e.getMessage()); + } + return dispatchCriteria; + } + + /** Get the serialized value of a content node, or null if node is null ;-) */ + private String getSerializedValue(JsonNode contentNode) { + if (contentNode != null) { + // Get string representation if array or object. + if (contentNode.getNodeType() == JsonNodeType.ARRAY || contentNode.getNodeType() == JsonNodeType.OBJECT) { + return contentNode.toString(); + } + // Else get raw representation. + return contentNode.asText(); + } + return null; + } + + /** Extract headers from a node to be processed for dispatch criteria computing. */ + private Map extractHeaders(JsonNode headersNode) { + Map result = new HashMap<>(); + Iterator> headers = headersNode.fields(); + while (headers.hasNext()) { + Map.Entry headerNode = headers.next(); + result.put(headerNode.getKey(), getSerializedValue(headerNode.getValue())); + } + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtensions.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtensions.java new file mode 100644 index 000000000..678b2fbcc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtensions.java @@ -0,0 +1,34 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.metadata; + +/** + * Come constants helping for parsing metadata extensions.s + * @author laurent + */ +public class MetadataExtensions { + + /** Name of OpenAPI / AsyncAPI Microcks extension attribute. */ + public static final String MICROCKS_EXTENSION = "x-microcks"; + + /** Name of OpenAPI / AsyncAPI operation Microcks extension attribute. */ + public static final String MICROCKS_OPERATION_EXTENSION = "x-microcks-operation"; + + /** Private constructor to hide the implicit public one and prevent instantiation. */ + private MetadataExtensions() { + // Hidden constructor + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtractor.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtractor.java new file mode 100644 index 000000000..b270acbf5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataExtractor.java @@ -0,0 +1,90 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.metadata; + +import io.github.microcks.domain.Metadata; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.ParameterLocation; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Util methods for extracting Metadata or operation properties from JsonNode. + * @author laurent + */ +public class MetadataExtractor { + + /** Private constructor to hide the implicit public one and prevent instantiation. */ + private MetadataExtractor() { + // Hidden constructor + } + + /** + * Complete a Metadata object with extracted metadata from JsonNode. + * @param metadata The object to complete + * @param node Node representing a metadata node + */ + public static void completeMetadata(Metadata metadata, JsonNode node) { + JsonNode annotationsNode = node.get("annotations"); + if (annotationsNode != null) { + annotationsNode.fields() + .forEachRemaining(entry -> metadata.setAnnotation(entry.getKey(), entry.getValue().asText())); + } + JsonNode labelsNode = node.get("labels"); + if (labelsNode != null) { + labelsNode.fields().forEachRemaining(entry -> metadata.setLabel(entry.getKey(), entry.getValue().asText())); + } + } + + /** + * Complete an Operation object with extracted properties from JsonNode. + * @param operation The object to complete + * @param node Node representing an operation node + */ + public static void completeOperationProperties(Operation operation, JsonNode node) { + if (node.has("delay")) { + operation.setDefaultDelay(node.path("delay").asLong(0)); + } + if (node.has("frequency")) { + operation.setDefaultDelay(node.path("frequency").asLong()); + } + if (node.has("dispatcher")) { + operation.setDispatcher(node.path("dispatcher").asText()); + } + if (node.has("dispatcherRules")) { + operation.setDispatcherRules(node.path("dispatcherRules").asText()); + } + if (node.has("parameterConstraints")) { + node.get("parameterConstraints").elements().forEachRemaining(paramNode -> { + ParameterConstraint constraint = extractParameterConstraint(paramNode); + operation.addParameterConstraint(constraint); + }); + } + } + + private static ParameterConstraint extractParameterConstraint(JsonNode node) { + ParameterConstraint constraint = new ParameterConstraint(); + constraint.setName(node.get("name").asText()); + constraint.setIn(ParameterLocation.valueOf(node.get("in").asText())); + constraint.setRequired(node.path("required").asBoolean(false)); + constraint.setRecopy(node.path("recopy").asBoolean(false)); + if (node.has("mustMatchRegexp")) { + constraint.setMustMatchRegexp(node.get("mustMatchRegexp").asText()); + } + return constraint; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataImporter.java new file mode 100644 index 000000000..79fe2e20f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/metadata/MetadataImporter.java @@ -0,0 +1,131 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.metadata; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Metadata; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Service; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Mock repository importer that uses a {@code APIMetadata} YAML descriptor as a source artifact. + * @author laurent + */ +public class MetadataImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(MetadataImporter.class); + + private final JsonNode spec; + + /** + * Build a new importer. + * @param specificationFilePath The path to local APIMetadata spec file + * @throws IOException if project file cannot be found or read. + */ + public MetadataImporter(String specificationFilePath) throws IOException { + try { + // Read spec bytes. + byte[] bytes = Files.readAllBytes(Paths.get(specificationFilePath)); + String specContent = new String(bytes, StandardCharsets.UTF_8); + + // Convert them to Node using Jackson object mapper. + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + spec = mapper.readTree(specContent.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + log.error("Exception while parsing APIMetadata specification file " + specificationFilePath, e); + throw new IOException("APIMetadata spec file parsing error"); + } + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List result = new ArrayList<>(); + + // Build a new service. + Service service = new Service(); + + JsonNode metadataNode = spec.get("metadata"); + if (metadataNode == null) { + log.error("Missing mandatory metadata in {}", spec.asText()); + throw new MockRepositoryImportException("Mandatory metadata property is missing in APIMetadata"); + } + service.setName(metadataNode.path("name").asText()); + service.setVersion(metadataNode.path("version").asText()); + + Metadata metadata = new Metadata(); + MetadataExtractor.completeMetadata(metadata, metadataNode); + service.setMetadata(metadata); + + // Then build its operations. + service.setOperations(extractOperations()); + + result.add(service); + return result; + } + + @Override + public List getResourceDefinitions(Service service) throws MockRepositoryImportException { + return new ArrayList<>(); + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + return new ArrayList<>(); + } + + /** + * Extract the list of operations from Specification. + */ + private List extractOperations() { + List results = new ArrayList<>(); + + // Iterate on specification "operations" nodes. + Iterator> operations = spec.path("operations").fields(); + while (operations.hasNext()) { + Map.Entry operation = operations.next(); + + // Build a new operation. + Operation op = new Operation(); + op.setName(operation.getKey()); + + JsonNode operationValue = operation.getValue(); + MetadataExtractor.completeOperationProperties(op, operationValue); + + results.add(op); + } + return results; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIImporter.java new file mode 100644 index 000000000..80f1dd4a7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIImporter.java @@ -0,0 +1,728 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Metadata; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.ParameterLocation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.AbstractJsonRepositoryImporter; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; +import io.github.microcks.util.ReferenceResolver; +import io.github.microcks.util.URIBuilder; +import io.github.microcks.util.metadata.MetadataExtensions; +import io.github.microcks.util.metadata.MetadataExtractor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * An implementation of MockRepositoryImporter that deals with OpenAPI v3.x.x specification file ; whether encoding into + * JSON or YAML documents. + * @author laurent + */ +public class OpenAPIImporter extends AbstractJsonRepositoryImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(OpenAPIImporter.class); + + private static final List VALID_VERBS = Arrays.asList("get", "put", "post", "delete", "options", "head", + "patch", "trace"); + + private static final String PARAMETERS_NODE = "parameters"; + private static final String PARAMETERS_QUERY_VALUE = "query"; + private static final String CONTENT_NODE = "content"; + private static final String HEADERS_NODE = "headers"; + private static final String EXAMPLES_NODE = "examples"; + private static final String EXAMPLE_VALUE_NODE = "value"; + + /** + * Build a new importer. + * @param specificationFilePath The path to local OpenAPI spec file + * @param referenceResolver An optional resolver for references present into the OpenAPI file + * @throws IOException if project file cannot be found or read. + */ + public OpenAPIImporter(String specificationFilePath, ReferenceResolver referenceResolver) throws IOException { + super(specificationFilePath, referenceResolver); + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List result = new ArrayList<>(); + + // Build a new service. + Service service = new Service(); + service.setName(rootSpecification.path("info").path("title").asText()); + service.setVersion(rootSpecification.path("info").path("version").asText()); + service.setType(ServiceType.REST); + + // Complete metadata if specified via extension. + if (rootSpecification.path("info").has(MetadataExtensions.MICROCKS_EXTENSION)) { + Metadata metadata = new Metadata(); + MetadataExtractor.completeMetadata(metadata, + rootSpecification.path("info").path(MetadataExtensions.MICROCKS_EXTENSION)); + service.setMetadata(metadata); + } + + // Before extraction operations, we need to get and build external reference if we have a resolver. + initializeReferencedResources(service); + + // Then build its operations. + service.setOperations(extractOperations()); + + result.add(service); + return result; + } + + @Override + public List getResourceDefinitions(Service service) { + List results = new ArrayList<>(); + + // Build a suitable name. + String name = service.getName() + "-" + service.getVersion(); + if (Boolean.TRUE.equals(isYaml)) { + name += ".yaml"; + } else { + name += ".json"; + } + + // Build a brand-new resource just with spec content. + Resource resource = new Resource(); + resource.setName(name); + resource.setType(ResourceType.OPEN_API_SPEC); + results.add(resource); + // Set the content of main OpenAPI that may have been updated with normalized dependencies with initializeReferencedResources(). + resource.setContent(rootSpecificationContent); + + // Add the external resources that were imported during service discovery. + results.addAll(externalResources); + + return results; + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + Map result = new HashMap<>(); + + // Iterate on specification "paths" nodes. + Iterator> paths = rootSpecification.path("paths").fields(); + while (paths.hasNext()) { + Entry path = paths.next(); + String pathName = path.getKey(); + JsonNode pathValue = followRefIfAny(path.getValue()); + + // Find examples fragments defined at the path level. + Map> pathPathParametersByExample = extractParametersByExample(pathValue, + "path"); + + // Iterate on specification path, "verbs" nodes. + Iterator> verbs = pathValue.fields(); + while (verbs.hasNext()) { + Entry verb = verbs.next(); + String verbName = verb.getKey(); + + // Find the correct operation. + if (operation.getName().equals(verbName.toUpperCase() + " " + pathName.trim())) { + // Find examples fragments defined at the verb level. + Map> pathParametersByExample = extractParametersByExample( + verb.getValue(), "path"); + pathParametersByExample.putAll(pathPathParametersByExample); + Map> queryParametersByExample = extractParametersByExample( + verb.getValue(), PARAMETERS_QUERY_VALUE); + Map> headerParametersByExample = extractParametersByExample( + verb.getValue(), "header"); + Map requestBodiesByExample = extractRequestBodies(verb.getValue()); + + // No need to go further if no examples. + if (verb.getValue().has("responses")) { + + // If we previously override the dispatcher with a Fallback, we must be sure to get wrapped elements. + DispatchCriteriaHelper.DispatcherDetails details = DispatchCriteriaHelper + .extractDispatcherWithRules(operation); + String rootDispatcher = details.rootDispatcher(); + String rootDispatcherRules = details.rootDispatcherRules(); + + Iterator> responseCodes = verb.getValue().path("responses").fields(); + while (responseCodes.hasNext()) { + Entry responseCode = responseCodes.next(); + Iterator> contents = getResponseContent(responseCode.getValue()).fields(); + + if (!contents.hasNext() && responseCode.getValue().has("x-microcks-refs")) { + result.putAll(getNoContentRequestResponsePair(operation, rootDispatcher, rootDispatcherRules, + requestBodiesByExample, pathParametersByExample, queryParametersByExample, + headerParametersByExample, responseCode)); + } + while (contents.hasNext()) { + Entry content = contents.next(); + result.putAll(getContentRequestResponsePairs(operation, rootDispatcher, rootDispatcherRules, + requestBodiesByExample, pathParametersByExample, queryParametersByExample, + headerParametersByExample, responseCode, content)); + } + } + } + } + } + } + + // Adapt map to list of Exchanges. + return result.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + /** + * Extract the list of operations from Specification. + */ + private List extractOperations() { + List results = new ArrayList<>(); + + // Iterate on specification "paths" nodes. + Iterator> paths = rootSpecification.path("paths").fields(); + while (paths.hasNext()) { + Entry path = paths.next(); + String pathName = path.getKey(); + JsonNode pathValue = followRefIfAny(path.getValue()); + + // Iterate on specification path, "verbs" nodes. + Iterator> verbs = pathValue.fields(); + while (verbs.hasNext()) { + Entry verb = verbs.next(); + String verbName = verb.getKey(); + + // Only deal with real verbs for now. + if (VALID_VERBS.contains(verbName)) { + String operationName = verbName.toUpperCase() + " " + pathName.trim(); + + Operation operation = new Operation(); + operation.setName(operationName); + operation.setMethod(verbName.toUpperCase()); + + // Complete operation properties if any. + if (verb.getValue().has(MetadataExtensions.MICROCKS_OPERATION_EXTENSION)) { + MetadataExtractor.completeOperationProperties(operation, + verb.getValue().path(MetadataExtensions.MICROCKS_OPERATION_EXTENSION)); + } + + // Deal with dispatcher stuffs if needed. + completeOperationWithDispatcher(operation, verb, pathName); + + // Deal with parameter constraints if required parameters. + completeOperationWithParameterConstraints(operation, verb.getValue()); + + results.add(operation); + } + } + } + return results; + } + + /** Complete operation with dispatcher rules if not already set. */ + private void completeOperationWithDispatcher(Operation operation, Entry verb, String pathName) { + if (operation.getDispatcher() == null) { + if (operationHasParameters(verb.getValue(), PARAMETERS_QUERY_VALUE) && urlHasParts(pathName)) { + operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(pathName) + " ?? " + + extractOperationParams(verb.getValue())); + operation.setDispatcher(DispatchStyles.URI_ELEMENTS); + } else if (operationHasParameters(verb.getValue(), PARAMETERS_QUERY_VALUE)) { + operation.setDispatcherRules(extractOperationParams(verb.getValue())); + operation.setDispatcher(DispatchStyles.URI_PARAMS); + } else if (urlHasParts(pathName)) { + operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(pathName)); + operation.setDispatcher(DispatchStyles.URI_PARTS); + } else { + operation.addResourcePath(pathName); + } + } else { + // If dispatcher has been forced via Metadata, we should still put a generic resourcePath + // (maybe containing {} parts) to later force operation matching at the mock controller level. + operation.addResourcePath(pathName); + } + } + + /** Add ParameterConstraints to operation if required parameters are found. */ + private void completeOperationWithParameterConstraints(Operation operation, JsonNode operationNode) { + Iterator parameters = operationNode.path(PARAMETERS_NODE).elements(); + while (parameters.hasNext()) { + JsonNode parameter = followRefIfAny(parameters.next()); + boolean required = parameter.path("required").asBoolean(false); + String location = parameter.path("in").asText(); + + if (required && !"path".equals(location)) { + ParameterConstraint constraint = new ParameterConstraint(); + constraint.setRequired(true); + constraint.setName(parameter.path("name").asText()); + constraint.setIn(ParameterLocation.valueOf(parameter.path("in").asText())); + operation.addParameterConstraint(constraint); + } + } + } + + /** + * Extract parameters within a specification node and organize them by example. Parameter can be of type 'path', + * 'query', 'header' or 'cookie'. Allow to filter them using parameterType. Key of returned map is example name. Key + * of value map is param name. Value of value map is param value ;-) + */ + private Map> extractParametersByExample(JsonNode node, String parameterType) { + Map> results = new HashMap<>(); + + Iterator parameters = node.path(PARAMETERS_NODE).elements(); + while (parameters.hasNext()) { + JsonNode parameter = followRefIfAny(parameters.next()); + String parameterName = parameter.path("name").asText(); + + if (parameter.has("in") && parameter.path("in").asText().equals(parameterType) + && parameter.has(EXAMPLES_NODE)) { + Iterator exampleNames = parameter.path(EXAMPLES_NODE).fieldNames(); + while (exampleNames.hasNext()) { + String exampleName = exampleNames.next(); + JsonNode example = parameter.path(EXAMPLES_NODE).path(exampleName); + JsonNode exampleValue = getExampleValue(example); + + if (exampleValue == null) { + log.warn("Couldn't find example value for example node: name: {}, data: {}", exampleName, example); + continue; + } + + Multimap exampleParams = results.computeIfAbsent(exampleName, + k -> ArrayListMultimap.create()); + + if (PARAMETERS_QUERY_VALUE.equals(parameterType) && exampleValue.isArray()) { + // Array of query params. + for (JsonNode current : (ArrayNode) exampleValue) { + exampleParams.put(parameterName, getValueString(current)); + } + } else if (PARAMETERS_QUERY_VALUE.equals(parameterType) && exampleValue.isObject()) { + final var fieldsIterator = ((ObjectNode) exampleValue).fields(); + while (fieldsIterator.hasNext()) { + var current = fieldsIterator.next(); + exampleParams.put(current.getKey(), getValueString(current.getValue())); + } + + } else { + exampleParams.put(parameterName, getValueString(exampleValue)); + } + } + } + } + return results; + } + + /** + * Extract request bodies within verb specification and organize them by example. Key of returned map is example + * name. Value is basic Microcks Request object (no query params, no headers) + */ + private Map extractRequestBodies(JsonNode verbNode) { + Map results = new HashMap<>(); + + JsonNode requestBody = verbNode.path("requestBody"); + Iterator contentTypeNames = requestBody.path(CONTENT_NODE).fieldNames(); + while (contentTypeNames.hasNext()) { + String contentTypeName = contentTypeNames.next(); + JsonNode contentType = requestBody.path(CONTENT_NODE).path(contentTypeName); + + if (contentType.has(EXAMPLES_NODE)) { + Iterator exampleNames = contentType.path(EXAMPLES_NODE).fieldNames(); + while (exampleNames.hasNext()) { + String exampleName = exampleNames.next(); + JsonNode example = contentType.path(EXAMPLES_NODE).path(exampleName); + String exampleValue = getSerializedExampleValue(example); + + // Build and store a request object. + Request request = new Request(); + request.setName(exampleName); + request.setContent(exampleValue); + + // We should add a Content-type header here for request body. + Header header = new Header(); + header.setName("Content-Type"); + HashSet values = new HashSet<>(); + values.add(contentTypeName); + header.setValues(values); + request.addHeader(header); + + results.put(exampleName, request); + } + } + } + return results; + } + + /** + * Extract headers within a header specification node and organize them by example. Key of returned map is example + * name. Value is a list of Microcks Header objects. + */ + private Map> extractHeadersByExample(JsonNode responseNode) { + Map> results = new HashMap<>(); + + responseNode = followRefIfAny(responseNode); + if (responseNode.has(HEADERS_NODE)) { + JsonNode headersNode = responseNode.path(HEADERS_NODE); + Iterator headerNames = headersNode.fieldNames(); + + while (headerNames.hasNext()) { + String headerName = headerNames.next(); + JsonNode headerNode = headersNode.path(headerName); + + if (headerNode.has(EXAMPLES_NODE)) { + Iterator exampleNames = headerNode.path(EXAMPLES_NODE).fieldNames(); + while (exampleNames.hasNext()) { + String exampleName = exampleNames.next(); + JsonNode example = headerNode.path(EXAMPLES_NODE).path(exampleName); + String exampleValue = getSerializedExampleValue(example); + + List
headersForExample = results.computeIfAbsent(exampleName, k -> new ArrayList<>()); + + // Example may be multiple CSV. + Set values = Arrays.stream(exampleValue.split(",")).map(String::trim) + .collect(Collectors.toSet()); + + Header header = new Header(); + header.setName(headerName); + header.setValues(values); + + headersForExample.add(header); + results.put(exampleName, headersForExample); + } + } + } + } + return results; + } + + /** + * Get the request/response pairs for a response content. + */ + private Map getContentRequestResponsePairs(Operation operation, String rootDispatcher, + String rootDispatcherRules, Map requestBodiesByExample, + Map> pathParametersByExample, + Map> queryParametersByExample, + Map> headerParametersByExample, Entry responseCode, + Entry content) { + + Map results = new HashMap<>(); + + String contentValue = content.getKey(); + // Find here potential headers for output of this operation examples. + Map> headersByExample = extractHeadersByExample(responseCode.getValue()); + + JsonNode examplesNode = followRefIfAny(content.getValue().path(EXAMPLES_NODE)); + + Iterator exampleNames = examplesNode.fieldNames(); + while (exampleNames.hasNext()) { + String exampleName = exampleNames.next(); + JsonNode example = examplesNode.path(exampleName); + + // We should have everything at hand to build response here. + Response response = new Response(); + response.setName(exampleName); + response.setMediaType(contentValue); + response.setStatus(responseCode.getKey()); + response.setContent(getSerializedExampleValue(example)); + if (!responseCode.getKey().startsWith("2")) { + response.setFault(true); + } + List
responseHeaders = headersByExample.get(exampleName); + if (responseHeaders != null) { + responseHeaders.stream().forEach(response::addHeader); + } + + // Do we have a request for this example? + Request request = requestBodiesByExample.get(exampleName); + if (request == null) { + request = new Request(); + request.setName(exampleName); + } + + // Complete request accept-type with response content-type. + Header header = new Header(); + header.setName("Accept"); + HashSet values = new HashSet<>(); + values.add(contentValue); + header.setValues(values); + request.addHeader(header); + + // Do we have to complete request with path parameters? + Multimap pathParameters = pathParametersByExample.get(exampleName); + if (pathParameters != null) { + completeRequestWithPathParameters(request, pathParameters); + } else if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher()) + || DispatchStyles.URI_ELEMENTS.equals(operation.getDispatcher())) { + // We must have at least one path parameters but none! + // Do not register this request / response pair. + break; + } + + // Complete request with query parameters if any. + completeRequestWithQueryParameters(request, queryParametersByExample.get(exampleName)); + // Complete request with header parameters if any. + completeRequestWithHeaderParameters(request, headerParametersByExample.get(exampleName)); + + // Finally, take care about dispatchCriteria and complete operation resourcePaths. + completeDispatchCriteriaAndResourcePaths(operation, rootDispatcher, rootDispatcherRules, + pathParametersByExample, queryParametersByExample, headerParametersByExample, exampleName, response); + + results.put(request, response); + } + + return results; + } + + /** + * Get the request/response pairs for a response without content. A response without content has a x-microcks-refs + * property to get bounds to requests. + */ + private Map getNoContentRequestResponsePair(Operation operation, String rootDispatcher, + String rootDispatcherRules, Map requestBodiesByExample, + Map> pathParametersByExample, + Map> queryParametersByExample, + Map> headerParametersByExample, Entry responseCode) { + + Map results = new HashMap<>(); + JsonNode requestRefs = responseCode.getValue().path("x-microcks-refs"); + + if (requestRefs.isArray()) { + // Find here potential headers for output of this operation examples. + Map> headersByExample = extractHeadersByExample(responseCode.getValue()); + + Iterator requestRefsIterator = requestRefs.elements(); + while (requestRefsIterator.hasNext()) { + String exampleName = requestRefsIterator.next().textValue(); + + // Do we have a request or path or query or header parameters? + Request request = requestBodiesByExample.get(exampleName); + Multimap pathParameters = pathParametersByExample.get(exampleName); + Multimap queryParameters = queryParametersByExample.get(exampleName); + Multimap headerParameters = headerParametersByExample.get(exampleName); + + if (request != null || pathParameters != null || queryParameters != null || headerParameters != null) { + if (request == null) { + request = new Request(); + request.setName(exampleName); + } + + if (pathParameters != null) { + completeRequestWithPathParameters(request, pathParameters); + } else if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher()) + || DispatchStyles.URI_ELEMENTS.equals(operation.getDispatcher())) { + // We must have at least one path parameters but none! + // Do not register this request / response pair. + break; + } + + // We should have everything at hand to build response here. + Response response = new Response(); + response.setName(exampleName); + response.setStatus(responseCode.getKey()); + if (!responseCode.getKey().startsWith("2")) { + response.setFault(true); + } + List
responseHeaders = headersByExample.get(exampleName); + if (responseHeaders != null) { + responseHeaders.stream().forEach(response::addHeader); + } + + // Complete request with query parameters if any. + completeRequestWithQueryParameters(request, queryParametersByExample.get(exampleName)); + // Complete request with header parameters if any. + completeRequestWithHeaderParameters(request, headerParametersByExample.get(exampleName)); + + // Finally, take care about dispatchCriteria and complete operation resourcePaths. + completeDispatchCriteriaAndResourcePaths(operation, rootDispatcher, rootDispatcherRules, + pathParametersByExample, queryParametersByExample, headerParametersByExample, exampleName, + response); + + results.put(request, response); + } + } + } + return results; + } + + private void completeRequestWithPathParameters(Request request, Multimap pathParameters) { + for (Entry paramEntry : pathParameters.entries()) { + Parameter param = new Parameter(); + param.setName(paramEntry.getKey()); + param.setValue(paramEntry.getValue()); + request.addQueryParameter(param); + } + } + + private void completeRequestWithQueryParameters(Request request, Multimap queryParameters) { + if (queryParameters != null) { + for (Entry paramEntry : queryParameters.entries()) { + Parameter param = new Parameter(); + param.setName(paramEntry.getKey()); + param.setValue(paramEntry.getValue()); + request.addQueryParameter(param); + } + } + } + + private void completeRequestWithHeaderParameters(Request request, Multimap headerParameters) { + if (headerParameters != null) { + for (Entry headerEntry : headerParameters.entries()) { + Header header = new Header(); + header.setName(headerEntry.getKey()); + // Values may be multiple and CSV. + Set headerValues = Arrays.stream(headerEntry.getValue().split(",")).map(String::trim) + .collect(Collectors.toSet()); + header.setValues(headerValues); + request.addHeader(header); + } + } + } + + private void completeDispatchCriteriaAndResourcePaths(Operation operation, String rootDispatcher, + String rootDispatcherRules, Map> pathParametersByExample, + Map> queryParametersByExample, + Map> headerParametersByExample, String exampleName, Response response) { + String dispatchCriteria = null; + String resourcePathPattern = operation.getName().split(" ")[1]; + + if (DispatchStyles.URI_PARAMS.equals(rootDispatcher)) { + Multimap queryParams = queryParametersByExample.get(exampleName); + dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, queryParams); + // We only need the pattern here. + operation.addResourcePath(resourcePathPattern); + } else if (DispatchStyles.URI_PARTS.equals(rootDispatcher)) { + Multimap parts = pathParametersByExample.get(exampleName); + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parts); + // We should complete resourcePath here. + String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parts); + operation.addResourcePath(resourcePath); + } else if (DispatchStyles.URI_ELEMENTS.equals(rootDispatcher)) { + Multimap parts = pathParametersByExample.get(exampleName); + Multimap queryParams = queryParametersByExample.get(exampleName); + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parts); + dispatchCriteria += DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, queryParams); + // We should complete resourcePath here. + String resourcePath = URIBuilder.buildURIFromPattern(resourcePathPattern, parts); + operation.addResourcePath(resourcePath); + } else if (DispatchStyles.QUERY_HEADER.equals(rootDispatcher)) { + Multimap headerParams = headerParametersByExample.get(exampleName); + dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap(rootDispatcherRules, headerParams); + // We only need the pattern here. + operation.addResourcePath(resourcePathPattern); + } + response.setDispatchCriteria(dispatchCriteria); + } + + /** Get the value of an example. This can be direct value field or those of followed $ref */ + private JsonNode getExampleValue(JsonNode example) { + if (example.has(EXAMPLE_VALUE_NODE)) { + return followRefIfAny(example.path(EXAMPLE_VALUE_NODE)); + } + if (example.has("$ref")) { + JsonNode component = followRefIfAny(example); + return getExampleValue(component); + } + return null; + } + + /** Get the serialized value of an example. This can be direct value field or those of followed $ref */ + private String getSerializedExampleValue(JsonNode example) { + JsonNode exampleValue = getExampleValue(example); + return exampleValue != null ? getValueString(exampleValue) : null; + } + + /** Get the content of a response. This can be direct content field or those of followed $ref */ + private JsonNode getResponseContent(JsonNode response) { + if (response.has("$ref")) { + JsonNode component = followRefIfAny(response); + return getResponseContent(component); + } + return response.path(CONTENT_NODE); + } + + /** Build a string representing operation parameters as used in dispatcher rules (param1 && param2) */ + private String extractOperationParams(JsonNode operation) { + StringBuilder params = new StringBuilder(); + Iterator parameters = operation.path(PARAMETERS_NODE).elements(); + while (parameters.hasNext()) { + JsonNode parameter = followRefIfAny(parameters.next()); + String parameterIn = parameter.path("in").asText(); + String parameterType = followRefIfAny(parameter.path("schema")).path("type").asText(); + if (!"path".equals(parameterIn)) { + if (params.length() > 0) { + params.append(" && "); + } + if (parameterType.equals("object")) { + params.append(StreamSupport.stream(Spliterators.spliteratorUnknownSize( + followRefIfAny(parameter.path("schema")).path("properties").fieldNames(), Spliterator.ORDERED), + false).collect(Collectors.joining(" && "))); + } else { + params.append(parameter.path("name").asText()); + } + } + } + return params.toString(); + } + + /** Check parameters presence into given operation node. */ + private boolean operationHasParameters(JsonNode operation, String parameterType) { + if (!operation.has(PARAMETERS_NODE)) { + return false; + } + Iterator parameters = operation.path(PARAMETERS_NODE).elements(); + while (parameters.hasNext()) { + JsonNode parameter = followRefIfAny(parameters.next()); + + String parameterIn = parameter.path("in").asText(); + if (parameterIn.equals(parameterType)) { + return true; + } + } + return false; + } + + /** Check variables parts presence into given url. */ + private static boolean urlHasParts(String url) { + return (url.indexOf("/:") != -1 || url.indexOf("/{") != -1); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIMcpToolConverter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIMcpToolConverter.java new file mode 100644 index 000000000..724b27d44 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIMcpToolConverter.java @@ -0,0 +1,352 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.util.URIBuilder; +import io.github.microcks.util.ai.McpSchema; +import io.github.microcks.util.ai.McpToolConverter; +import io.github.microcks.web.BasicHttpServletRequest; +import io.github.microcks.web.MockControllerCommons; +import io.github.microcks.web.MockInvocationContext; +import io.github.microcks.web.ResponseResult; +import io.github.microcks.web.RestInvocationProcessor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_ADD_PROPERTIES_ELEMENT; +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_PROPERTIES_ELEMENT; +import static io.github.microcks.util.JsonSchemaValidator.JSON_SCHEMA_REQUIRED_ELEMENT; + +/** + * Implementation of McpToolConverter for OpenAPI services. + * @author laurent + */ +public class OpenAPIMcpToolConverter extends McpToolConverter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(OpenAPIMcpToolConverter.class); + + private static final String OPEN_API_PATHS_ELEMENT = "/paths/"; + + private final RestInvocationProcessor invocationProcessor; + private final ObjectMapper mapper; + + private JsonNode schemaNode; + + /** + * Build a new instance of OpenAPIMcpToolConverter. + * @param service The service to which this converter is attached + * @param resource The resource used for OpenAPI service conversion + * @param invocationProcessor The invocation processor to use for processing the call + * @param mapper The ObjectMapper to use for JSON serialization + */ + public OpenAPIMcpToolConverter(Service service, Resource resource, RestInvocationProcessor invocationProcessor, + ObjectMapper mapper) { + super(service, resource); + this.invocationProcessor = invocationProcessor; + this.mapper = mapper; + } + + @Override + public String getToolName(Operation operation) { + // Anthropic Claude tools must respect ^[a-zA-Z0-9_-]{1,64}$ regular expression that doesn't match with our URI. + return operation.getName().replace(" ", "_").replace("/", "_").replace("{", "").replace("}", "") + .replace("__", "_").toLowerCase(); + } + + @Override + public String getToolDescription(Operation operation) { + try { + if (schemaNode == null) { + schemaNode = OpenAPISchemaValidator.getJsonNodeForSchema(resource.getContent()); + } + + // Extract JsonNode corresponding to operation. + String verb = operation.getName().split(" ")[0].toLowerCase(); + String path = operation.getName().split(" ")[1].trim().replace("/", "~1"); + + String operationPointer = OPEN_API_PATHS_ELEMENT + path + "/" + verb; + JsonNode operationNode = schemaNode.at(operationPointer); + if (operationNode.has("description")) { + return operationNode.path("description").asText(); + } + if (operationNode.has("summary")) { + return operationNode.path("summary").asText(); + } + } catch (Exception e) { + log.error("Exception while trying to get tool description", e); + } + return null; + } + + @Override + public McpSchema.JsonSchema getInputSchema(Operation operation) { + ObjectNode inputSchemaNode = mapper.createObjectNode(); + ObjectNode schemaPropertiesNode = mapper.createObjectNode(); + ArrayNode requiredPropertiesNode = mapper.createArrayNode(); + + // Initialize input schema with empty object. + inputSchemaNode.put("type", "object"); + inputSchemaNode.set(JSON_SCHEMA_PROPERTIES_ELEMENT, schemaPropertiesNode); + inputSchemaNode.set(JSON_SCHEMA_REQUIRED_ELEMENT, requiredPropertiesNode); + inputSchemaNode.put(JSON_SCHEMA_ADD_PROPERTIES_ELEMENT, false); + try { + if (schemaNode == null) { + schemaNode = OpenAPISchemaValidator.getJsonNodeForSchema(resource.getContent()); + } + + // Extract JsonNode corresponding to request body. + String verb = operation.getName().split(" ")[0].toLowerCase(); + String path = operation.getName().split(" ")[1].trim().replace("/", "~1"); + + String schemaPointer = OPEN_API_PATHS_ELEMENT + path + "/" + verb + + "/requestBody/content/application~1json/schema"; + JsonNode requestSchemaNode = schemaNode.at(schemaPointer); + if (!requestSchemaNode.isMissingNode()) { + requestSchemaNode = followRefIfAny(requestSchemaNode); + + // May be null if not resolved. + if (requestSchemaNode != null) { + visitNodeWithProperties(requestSchemaNode, schemaPropertiesNode, requiredPropertiesNode); + } + } + + // Add parameters to input schema. + String paramsPointer = OPEN_API_PATHS_ELEMENT + path + "/" + verb + "/parameters"; + JsonNode paramsNode = schemaNode.at(paramsPointer); + + Iterator parameters = paramsNode.elements(); + while (parameters.hasNext()) { + JsonNode parameter = followRefIfAny(parameters.next()); + String paramName = parameter.path("name").asText(); + + // Create a property node for the parameter. + ObjectNode propertyNode = mapper.createObjectNode(); + propertyNode.put("type", "string"); + schemaPropertiesNode.set(paramName, propertyNode); + if (parameter.path(JSON_SCHEMA_REQUIRED_ELEMENT).asBoolean(false)) { + requiredPropertiesNode.add(paramName); + } + } + } catch (Exception e) { + log.error("Exception while trying to get input schema", e); + } + return mapper.convertValue(inputSchemaNode, McpSchema.JsonSchema.class); + } + + @Override + public Response getCallResponse(Operation operation, McpSchema.CallToolRequest request, + Map> headers) { + String queryString = ""; + String verb = operation.getName().split(" ")[0]; + String resourcePath = operation.getName().split(" ")[1].trim(); + Map pathParams = new HashMap<>(); + Map queryParams = new HashMap<>(); + + // Unwrap the request parameters and headers and remove them from request. + try { + if (schemaNode == null) { + schemaNode = OpenAPISchemaValidator.getJsonNodeForSchema(resource.getContent()); + } + + // Extract JsonNode corresponding to request parameters. + String path = resourcePath.replace("/", "~1"); + String paramsPointer = OPEN_API_PATHS_ELEMENT + path + "/" + verb.toLowerCase() + "/parameters"; + JsonNode paramsNode = schemaNode.at(paramsPointer); + + Iterator parameters = paramsNode.elements(); + while (parameters.hasNext()) { + JsonNode parameter = followRefIfAny(parameters.next()); + String paramName = parameter.path("name").asText(); + String paramIn = parameter.path("in").asText(); // Check if it's "path" or "query" + + if (request.arguments().containsKey(paramName)) { + String paramValue = request.arguments().remove(paramName).toString(); + if ("path".equals(paramIn)) { + pathParams.put(paramName, paramValue); + } else if ("query".equals(paramIn)) { + queryParams.put(paramName, paramValue); + } else if ("header".equals(paramIn)) { + headers.put(paramName, List.of(paramValue)); + } + } + } + } catch (Exception e) { + log.error("Exception while extracting URI parameters from arguments", e); + } + + // Re-build the resource path with parameters if needed. + if (operation.getName().contains("{")) { + resourcePath = URIBuilder.buildURIFromPattern(resourcePath, pathParams); + } + // Re-build the query string with parameters if needed. + if (!queryParams.isEmpty()) { + queryString = URIBuilder.buildURIFromPattern("", queryParams); + if (queryString.startsWith("?")) { + queryString = queryString.substring(1); + } + } + + // Create a mock request to pass to the invocation processor. + MockInvocationContext ic = new MockInvocationContext(service, operation, resourcePath); + + try { + // Serialize remaining arguments as the request body. + String body = mapper.writeValueAsString(request.arguments()); + + // Execute the invocation processor after having cleaned the headers to propagate. + headers = sanitizeHttpHeaders(headers); + ResponseResult result = invocationProcessor.processInvocation(ic, System.currentTimeMillis(), null, body, + headers, + new BasicHttpServletRequest( + "http://localhost:8080/rest/" + + MockControllerCommons.composeServiceAndVersion(service.getName(), service.getVersion()), + verb, resourcePath, queryString, queryParams, headers)); + + // Build a Microcks Response from the result. + Response response = new Response(); + response.setStatus(result.status().toString()); + response.setHeaders(null); + response.setContent(extractResponseContent(result)); + + if (result.status().isError()) { + response.setFault(true); + } + return response; + } catch (Exception e) { + log.error("Exception while processing the MCP call invocation", e); + } + return null; + } + + /** Follow the $ref if we have one. Otherwise, return given node. */ + protected JsonNode followRefIfAny(JsonNode referencableNode) { + if (referencableNode.has("$ref")) { + String ref = referencableNode.path("$ref").asText(); + return getNodeForRef(ref); + } + return referencableNode; + } + + /** Get the JsonNode for reference within the specification. */ + private JsonNode getNodeForRef(String reference) { + if (reference.startsWith("#/")) { + return schemaNode.at(reference.substring(1)); + } + // TODO: handle external references reusing imported resources? + return null; + } + + /** Visit a node and extract its properties. */ + private void visitNodeWithProperties(JsonNode node, ObjectNode propertiesNode, ArrayNode requiredPropertiesNode) { + JsonNode requiredNode = node.path(JSON_SCHEMA_REQUIRED_ELEMENT); + Iterator> propertiesList = node.path(JSON_SCHEMA_PROPERTIES_ELEMENT).fields(); + + while (propertiesList.hasNext()) { + Map.Entry property = propertiesList.next(); + String propertyName = property.getKey(); + JsonNode propertyValue = followRefIfAny(property.getValue()); + + if (propertyValue.has(JSON_SCHEMA_PROPERTIES_ELEMENT)) { + // Initialize a new subschema node we must visit to resolve all possible references. + ObjectNode subschemaNode = mapper.createObjectNode(); + ObjectNode subpropertiesNode = mapper.createObjectNode(); + ArrayNode requiredSubpropertiesNode = mapper.createArrayNode(); + + subschemaNode.put("type", "object"); + subschemaNode.set(JSON_SCHEMA_PROPERTIES_ELEMENT, subpropertiesNode); + subschemaNode.set(JSON_SCHEMA_REQUIRED_ELEMENT, requiredSubpropertiesNode); + subschemaNode.put(JSON_SCHEMA_ADD_PROPERTIES_ELEMENT, false); + propertiesNode.set(propertyName, subschemaNode); + + visitNodeWithProperties(propertyValue, subpropertiesNode, requiredSubpropertiesNode); + } else { + propertiesNode.set(propertyName, dereferencedNode(propertyValue)); + if (!requiredNode.isMissingNode() && requiredNode.isArray() + && arrayNodeContains((ArrayNode) requiredNode, property.getKey())) { + requiredPropertiesNode.add(property.getKey()); + } + } + } + } + + private JsonNode dereferencedNode(JsonNode node) { + if (node.isObject()) { + Iterator fieldNames = node.fieldNames(); + while (fieldNames.hasNext()) { + String fieldName = fieldNames.next(); + JsonNode fieldValue = node.get(fieldName); + if (fieldValue.has("$ref")) { + JsonNode target = followRefIfAny(fieldValue); + if (target != null) { + // Replace the field value with the dereferenced node. + ((ObjectNode) node).replace(fieldName, dereferencedNode(target)); + } else { + // If the target is null, remove the field. + ((ObjectNode) node).remove(fieldName); + } + } else if (fieldValue.isObject() || fieldValue.isArray()) { + // Recursively process nested objects or arrays. + dereferencedNode(fieldValue); + } + } + } else if (node.isArray()) { + for (int i = 0; i < node.size(); i++) { + JsonNode arrayElement = node.get(i); + if (arrayElement.has("$ref")) { + JsonNode target = followRefIfAny(arrayElement); + if (target != null) { + JsonNode dereferencedTarget = dereferencedNode(target); + // Replace the array element with the dereferenced node. + ((ArrayNode) node).set(i, dereferencedTarget); + } else { + // If the target is null, remove the array element. + ((ArrayNode) node).remove(i); + } + } else if (arrayElement.isObject() || arrayElement.isArray()) { + // Recursively process nested objects or arrays. + dereferencedNode(arrayElement); + } + } + } + return node; + } + + /** Check if the arrayNode contains the given value. */ + private boolean arrayNodeContains(ArrayNode arrayNode, String value) { + // Iterate over arrayNode elements and check if any element matches the value. + for (JsonNode element : arrayNode) { + if (element.asText().equals(value)) { + return true; + } + } + return false; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIOverlayExporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIOverlayExporter.java new file mode 100644 index 000000000..4740e9e7f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPIOverlayExporter.java @@ -0,0 +1,168 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.util.MockRepositoryExportException; +import io.github.microcks.util.MockRepositoryExporter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Mock repository exporter that exports Microcks domain objects definitions into the Open API Overlay YAML format. + * @author laurent + */ +public class OpenAPIOverlayExporter implements MockRepositoryExporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(OpenAPIOverlayExporter.class); + + private final ObjectMapper mapper; + private Service service; + private final Map> operationsExchanges = new HashMap<>(); + + /** Create a new OpenAPIOverlayExporter. */ + public OpenAPIOverlayExporter() { + mapper = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)); + } + + @Override + public void addServiceDefinition(Service service) throws MockRepositoryExportException { + if (this.service != null) { + log.error("OpenAPIOverlayExporter only allows one service definition"); + throw new MockRepositoryExportException("Service definition already set"); + } + this.service = service; + } + + @Override + public void addMessageDefinitions(Service service, Operation operation, List exchanges) + throws MockRepositoryExportException { + if (this.service != null && !this.service.getId().equals(service.getId())) { + log.error("ExamplesExporter only allows one service definition, this one is different"); + throw new MockRepositoryExportException("Service definition doesn't match with the one already set"); + } + if (this.service == null) { + this.service = service; + } + this.operationsExchanges.put(operation, exchanges); + } + + @Override + public String exportAsString() throws MockRepositoryExportException { + ObjectNode rootNode = mapper.createObjectNode(); + + // Initialize header and metadata. + rootNode.put("overlay", "1.0.0"); + rootNode.set("info", mapper.createObjectNode().put("title", service.getName() + " Overlay for examples") + .put("version", service.getVersion())); + + ArrayNode actionsNode = rootNode.putArray("actions"); + for (Operation operation : operationsExchanges.keySet()) { + // Each exchange is translated into an update action. + List exchanges = operationsExchanges.get(operation); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair pair) { + exportRequest(actionsNode, operation, pair.getRequest()); + exportResponse(actionsNode, operation, pair.getResponse()); + } + } + } + + // Output the root node. + String yamlResult = null; + try { + yamlResult = mapper.writeValueAsString(rootNode); + } catch (Exception e) { + log.error("Exception while writing YAML export", e); + throw new MockRepositoryExportException("Exception while writing YAML export", e); + } + return yamlResult; + } + + private void exportRequest(ArrayNode actions, Operation operation, Request request) { + // Export the parameters part of the exchange if present. + if (request.getQueryParameters() != null && !request.getQueryParameters().isEmpty()) { + for (Parameter parameter : request.getQueryParameters()) { + // Export the query parameter part of the exchange. + ObjectNode parameterNode = actions.addObject(); + + String[] operationParts = operation.getName().split(" "); + + parameterNode.put("target", "$.paths['" + operationParts[1] + "']." + operationParts[0].toLowerCase() + + ".parameters[?@.name=='" + parameter.getName() + "'].examples"); + ObjectNode updateNode = parameterNode.putObject("update"); + ObjectNode exampleNode = updateNode.putObject(request.getName()); + exampleNode.put("value", parameter.getValue()); + } + } + + // Export the requestBody part of the exchange if present. + if (request.getContent() != null) { + ObjectNode requestNode = actions.addObject(); + String[] operationParts = operation.getName().split(" "); + + requestNode.put("target", "$.paths['" + operationParts[1] + "']." + operationParts[0].toLowerCase() + + ".requestBody.content['" + getContentType(request) + "'].examples"); + ObjectNode updateNode = requestNode.putObject("update"); + ObjectNode exampleNode = updateNode.putObject(request.getName()); + exampleNode.put("value", request.getContent()); + } + } + + private String getContentType(Request request) { + if (request.getHeaders() != null) { + for (Header header : request.getHeaders()) { + if ("Content-Type".equalsIgnoreCase(header.getName())) { + return header.getValues().iterator().next(); + } + } + } + return "application/json"; + } + + private void exportResponse(ArrayNode actions, Operation operation, Response response) { + // Export the response part of the exchange. + ObjectNode responseNode = actions.addObject(); + + String[] operationParts = operation.getName().split(" "); + + responseNode.put("target", "$.paths['" + operationParts[1] + "']." + operationParts[0].toLowerCase() + + ".responses." + response.getStatus() + ".content['" + response.getMediaType() + "'].examples"); + ObjectNode updateNode = responseNode.putObject("update"); + ObjectNode exampleNode = updateNode.putObject(response.getName()); + exampleNode.put("value", response.getContent()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPITestRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPITestRunner.java new file mode 100644 index 000000000..6a0fd5d08 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/OpenAPITestRunner.java @@ -0,0 +1,264 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.util.test.HttpTestRunner; +import com.fasterxml.jackson.databind.JsonNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * This is an implementation of HttpTestRunner that deals with OpenAPI schema validation. This implementation now + * manages the 2 flavors of OpenAPI: OpenAPI v3.x and Swagger v2.x. + * @author laurent + */ +public class OpenAPITestRunner extends HttpTestRunner { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(OpenAPITestRunner.class); + + /** Content-types for JSON that are valid response types validated using JSON Schemas. */ + public static final Pattern APPLICATION_JSON_TYPES_PATTERN = Pattern.compile("application/.*json"); + + private final ResourceRepository resourceRepository; + private final ResponseRepository responseRepository; + + private final boolean validateResponseCode; + + + /** The URL of resources used for validation. */ + private String resourceUrl = null; + + private List lastValidationErrors = null; + + /** + * Build a new OpenAPITestRunner. + * @param resourceRepository Access to resources repository + * @param responseRepository Access to response repository + * @param validateResponseCode whether to validate response code + */ + public OpenAPITestRunner(ResourceRepository resourceRepository, ResponseRepository responseRepository, + boolean validateResponseCode) { + this.resourceRepository = resourceRepository; + this.responseRepository = responseRepository; + this.validateResponseCode = validateResponseCode; + } + + /** + * The URL of resources used for validation. + * @return The URL of resources used for validation + */ + public String getResourceUrl() { + return resourceUrl; + } + + /** + * The URL of resources used for validation. + * @param resourceUrl The URL of resources used for validation. + */ + public void setResourceUrl(String resourceUrl) { + this.resourceUrl = resourceUrl; + } + + /** + * Build the HttpMethod corresponding to string. + */ + @Override + public HttpMethod buildMethod(String method) { + return HttpMethod.valueOf(method.toUpperCase()); + } + + @Override + protected int extractTestReturnCode(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse, String responseContent) { + int code = TestReturn.SUCCESS_CODE; + + int responseCode = 0; + try { + responseCode = httpResponse.getStatusCode().value(); + log.debug("Response status code: {}", responseCode); + } catch (IOException ioe) { + log.debug("IOException while getting raw status code in response", ioe); + return TestReturn.FAILURE_CODE; + } + + // Extract response content-type in any case. + String contentType = null; + if (httpResponse.getHeaders().getContentType() != null) { + contentType = httpResponse.getHeaders().getContentType().toString(); + log.debug("Response media-type is {}", httpResponse.getHeaders().getContentType()); + } + + // If required, compare response code and content-type to expected ones. + if (validateResponseCode) { + Response expectedResponse = responseRepository.findById(request.getResponseId()).orElse(null); + if (expectedResponse != null) { + log.debug("Response expected status code: {}", expectedResponse.getStatus()); + if (!String.valueOf(responseCode).equals(expectedResponse.getStatus())) { + log.debug("Response HttpStatus does not match expected one, returning failure"); + lastValidationErrors = List + .of(String.format("Response HttpStatus does not match expected one. Expecting %s but got %d", + expectedResponse.getStatus(), responseCode)); + return TestReturn.FAILURE_CODE; + } + + if (expectedResponse.getMediaType() != null + && !contentTypesAreEquivalent(expectedResponse.getMediaType(), contentType)) { + log.debug("Response Content-Type does not match expected one, returning failure"); + lastValidationErrors = List + .of(String.format("Response Content-Type does not match expected one. Expecting %s but got %s", + expectedResponse.getMediaType(), contentType)); + return TestReturn.FAILURE_CODE; + } + } + } + + // Do not try to validate response content if no content provided ;-) + // Also do not try to schema validate something that is not application/.*json for now... + // Alternatives schemes are on their way for OpenAPI but not yet ready (see https://github.com/OAI/OpenAPI-Specification/pull/1736) + String shortContentType = getShortContentType(contentType); + if (responseCode != 204 && shortContentType != null + && APPLICATION_JSON_TYPES_PATTERN.matcher(shortContentType).matches()) { + + boolean isOpenAPIv3 = true; + + // Retrieve the resource corresponding to OpenAPI specification if any. + Resource openapiSpecResource = findResourceCandidate(service); + if (openapiSpecResource == null) { + log.debug("Found no OpenAPI specification resource for service {} - {}, so failing validating", + service.getId(), service.getName()); + return TestReturn.FAILURE_CODE; + } + + // Check the type so guess the kind of validation. + if (ResourceType.SWAGGER.equals(openapiSpecResource.getType())) { + isOpenAPIv3 = false; + } + + JsonNode openApiSpec = null; + try { + openApiSpec = OpenAPISchemaValidator.getJsonNodeForSchema(openapiSpecResource.getContent()); + } catch (IOException ioe) { + log.debug("OpenAPI specification cannot be transformed into valid JsonNode schema, so failing"); + return TestReturn.FAILURE_CODE; + } + + // Extract JsonNode corresponding to response. + String verb = operation.getName().split(" ")[0].toLowerCase(); + String path = operation.getName().split(" ")[1].trim(); + + // Get body content as a string. + JsonNode contentNode = null; + try { + contentNode = OpenAPISchemaValidator.getJsonNode(responseContent); + } catch (IOException ioe) { + log.debug("Response body cannot be accessed or transformed as Json, returning failure"); + return TestReturn.FAILURE_CODE; + } + String jsonPointer = "/paths/" + path.replace("/", "~1") + "/" + verb + "/responses/" + responseCode; + + if (isOpenAPIv3) { + lastValidationErrors = OpenAPISchemaValidator.validateJsonMessage(openApiSpec, contentNode, jsonPointer, + contentType, resourceUrl); + } else { + lastValidationErrors = SwaggerSchemaValidator.validateJsonMessage(openApiSpec, contentNode, jsonPointer, + resourceUrl); + } + + if (!lastValidationErrors.isEmpty()) { + log.debug("OpenAPI schema validation errors found {}, marking test as failed.", + lastValidationErrors.size()); + return TestReturn.FAILURE_CODE; + } + log.debug("OpenAPI schema validation of response is successful !"); + } + return code; + } + + @Override + protected String extractTestReturnMessage(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse) { + StringBuilder builder = new StringBuilder(); + if (lastValidationErrors != null && !lastValidationErrors.isEmpty()) { + for (String error : lastValidationErrors) { + builder.append(error).append('\n'); + } + } + // Reset just after consumption so avoid side-effects. + lastValidationErrors = null; + return builder.toString(); + } + + private boolean contentTypesAreEquivalent(String expectedContentType, String responseContentType) { + if (responseContentType != null) { + // If charset is specified, compare ignore case ignoring spaces. + if (expectedContentType.contains("charset=")) { + return expectedContentType.replace(" ", "").equalsIgnoreCase(responseContentType.replace(" ", "")); + } + // Sanitize charset information from media-type. + String shortResponseContentType = getShortContentType(responseContentType); + return expectedContentType.equalsIgnoreCase(shortResponseContentType); + } + return false; + } + + private String getShortContentType(String contentType) { + // Sanitize charset information from media-type. + if (contentType != null && contentType.contains("charset=") && contentType.indexOf(";") > 0) { + return contentType.substring(0, contentType.indexOf(";")); + } + return contentType; + } + + private Resource findResourceCandidate(Service service) { + Optional candidate = Optional.empty(); + // Try resources marked within mainArtifact first. + List resources = resourceRepository.findMainByServiceId(service.getId()); + if (!resources.isEmpty()) { + candidate = getResourceCandidate(resources); + } + // Else try all the services resources... + if (candidate.isEmpty()) { + resources = resourceRepository.findByServiceId(service.getId()); + if (!resources.isEmpty()) { + candidate = getResourceCandidate(resources); + } + } + return candidate.orElse(null); + } + + private Optional getResourceCandidate(List resources) { + return resources.stream() + .filter(r -> ResourceType.OPEN_API_SPEC.equals(r.getType()) || ResourceType.SWAGGER.equals(r.getType())) + .findFirst(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/SwaggerImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/SwaggerImporter.java new file mode 100644 index 000000000..f4afc81b2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/openapi/SwaggerImporter.java @@ -0,0 +1,61 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.ReferenceResolver; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * An implementation of MockRepositoryImporter that deals with Swagger v2.x.x specification file ; whether encoding into + * JSON or YAML documents. + * @author laurent + */ +public class SwaggerImporter extends OpenAPIImporter { + + /** + * Build a new importer. + * @param specificationFilePath The path to local OpenAPI spec file + * @param referenceResolver An optional resolver for references present into the OpenAPI file + * @throws IOException if project file cannot be found or read. + */ + public SwaggerImporter(String specificationFilePath, ReferenceResolver referenceResolver) throws IOException { + super(specificationFilePath, referenceResolver); + } + + @Override + public List getResourceDefinitions(Service service) { + List results = super.getResourceDefinitions(service); + if (!results.isEmpty()) { + results.get(0).setType(ResourceType.SWAGGER); + } + return results; + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + return Collections.emptyList(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanCollectionImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanCollectionImporter.java new file mode 100644 index 000000000..a729480a2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanCollectionImporter.java @@ -0,0 +1,618 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.postman; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; +import io.github.microcks.util.URIBuilder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * Implement of MockRepositoryImporter that uses a Postman collection for building domain objects. Only v2 collection + * format is supported. + * @author laurent + */ +public class PostmanCollectionImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(PostmanCollectionImporter.class); + + /** Postman collection property that references service version property. */ + public static final String SERVICE_VERSION_PROPERTY = "version"; + /** We only support Postman Collection v2. Here's the error message otherwise. */ + public static final String COLLECTION_VERSION_ERROR_MESSAGE = "Only Postman v2 Collection are supported."; + + private static final String INFO_NODE = "info"; + private static final String REQUEST_NODE = "request"; + private static final String RESPONSE_NODE = "response"; + private static final String QUERY_NODE = "query"; + private static final String VARIABLE_NODE = "variable"; + private static final String GRAPHQL_NODE = "graphql"; + private static final String VARIABLES_NODE = "variables"; + private static final String VALUE_NODE = "value"; + + private ObjectMapper mapper; + private JsonNode collection; + private String collectionContent; + // Flag telling if V2 format is used. + private boolean isV2Collection = false; + + /** Default constructor for package protected extensions. */ + protected PostmanCollectionImporter() { + } + + /** + * Build a new importer. + * @param collectionFilePath The path to local Postman collection file + * @throws IOException if project file cannot be found or read. + */ + public PostmanCollectionImporter(String collectionFilePath) throws IOException { + try { + // Read Json bytes. + byte[] jsonBytes = Files.readAllBytes(Paths.get(collectionFilePath)); + collectionContent = new String(jsonBytes, StandardCharsets.UTF_8); + // Convert them to Node using Jackson object mapper. + mapper = new ObjectMapper(); + collection = mapper.readTree(jsonBytes); + } catch (Exception e) { + log.error("Exception while parsing Postman collection file " + collectionFilePath, e); + throw new IOException("Postman collection file parsing error"); + } + } + + protected void setCollection(JsonNode collection) { + this.collection = collection; + } + + protected void setCollectionContent(String collectionContent) { + this.collectionContent = collectionContent; + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List result = new ArrayList<>(); + + // Build a new service. + Service service = new Service(); + + // Collection V2 as an info node. + if (collection.has(INFO_NODE)) { + isV2Collection = true; + fillServiceDefinition(service); + } else { + throw new MockRepositoryImportException(COLLECTION_VERSION_ERROR_MESSAGE); + } + + // Then build its operations. + try { + service.setOperations(extractOperations()); + } catch (Throwable t) { + log.error("Runtime exception while extracting Operations for {}", service.getName()); + throw new MockRepositoryImportException("Runtime exception for " + service.getName() + ": " + t.getMessage()); + } + + result.add(service); + return result; + } + + private void fillServiceDefinition(Service service) throws MockRepositoryImportException { + JsonNode infoNode = collection.path(INFO_NODE); + service.setName(infoNode.path("name").asText()); + service.setType(ServiceType.REST); + + String version = null; + + // On v2.1 collection format, we may have a version attribute under info. + // See https://schema.getpostman.com/json/collection/v2.1.0/docs/index.html + if (infoNode.has(SERVICE_VERSION_PROPERTY)) { + version = extractStructuredVersion(infoNode.path(SERVICE_VERSION_PROPERTY)); + } else { + String description = infoNode.path("description").asText(); + if (description != null && description.contains(SERVICE_VERSION_PROPERTY + "=")) { + description = description.substring(description.indexOf(SERVICE_VERSION_PROPERTY + "=")); + if (description.indexOf(' ') > -1) { + description = description.substring(0, description.indexOf(' ')); + } + if (description.split("=").length > 1) { + version = description.split("=")[1]; + } + } + } + + if (version == null) { + log.error( + "Version property is missing in Collection. Use either 'version' for v2.1 Collection or 'version=x.y - something' description syntax."); + throw new MockRepositoryImportException("Version property is missing in Collection description"); + } + service.setVersion(version); + } + + private String extractStructuredVersion(JsonNode versionNode) { + String version = null; + if (versionNode.has("identifier")) { + version = versionNode.path("identifier").asText(); + } else if (versionNode.has("major") && versionNode.has("minor") && versionNode.has("patch")) { + version = versionNode.path("major").asText(); + version += "." + versionNode.path("minor").asText(); + version += "." + versionNode.path("patch").asText(); + } else { + version = versionNode.asText(); + } + return version; + } + + @Override + public List getResourceDefinitions(Service service) { + List results = new ArrayList<>(); + + // Build a suitable name. + String name = service.getName() + "-" + service.getVersion() + ".json"; + + Resource resource = new Resource(); + resource.setName(name); + resource.setType(ResourceType.POSTMAN_COLLECTION); + resource.setContent(collectionContent); + results.add(resource); + + return results; + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + + if (isV2Collection) { + return getMessageDefinitionsV2(service, operation); + } else { + throw new MockRepositoryImportException(COLLECTION_VERSION_ERROR_MESSAGE); + } + } + + private List getMessageDefinitionsV2(Service service, Operation operation) { + Map result = new HashMap<>(); + + Iterator items = collection.path("item").elements(); + while (items.hasNext()) { + JsonNode item = items.next(); + result.putAll(getMessageDefinitionsV2("", item, operation)); + } + + // Adapt map to list of Exchanges. + return result.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private Map getMessageDefinitionsV2(String folderName, JsonNode itemNode, Operation operation) { + log.debug("Extracting message definitions in folder {}", folderName); + Map result = new HashMap<>(); + String itemNodeName = itemNode.path("name").asText(); + + if (!itemNode.has(REQUEST_NODE)) { + // Item is simply a folder that may contain some other folders recursively. + Iterator items = itemNode.path("item").elements(); + while (items.hasNext()) { + JsonNode item = items.next(); + result.putAll(getMessageDefinitionsV2(folderName + "/" + itemNodeName, item, operation)); + } + } else { + // Item is here an operation description. + String operationName = buildOperationName(itemNode, folderName); + + // Select item based onto operation name. + if (PostmanUtil.areOperationsEquivalent(operation.getName(), operationName)) { + // If we previously override the dispatcher with a Fallback, we must be sure to get wrapped elements. + DispatchCriteriaHelper.DispatcherDetails details = DispatchCriteriaHelper + .extractDispatcherWithRules(operation); + String rootDispatcher = details.rootDispatcher(); + String rootDispatcherRules = details.rootDispatcherRules(); + + Iterator responses = itemNode.path(RESPONSE_NODE).elements(); + while (responses.hasNext()) { + JsonNode responseNode = responses.next(); + JsonNode requestNode = responseNode.path("originalRequest"); + + // Build dispatchCriteria and complete operation resourcePaths. + String dispatchCriteria = null; + String requestUrl = requestNode.path("url").path("raw").asText(); + + if (DispatchStyles.URI_PARAMS.equals(rootDispatcher)) { + dispatchCriteria = DispatchCriteriaHelper.extractFromURIParams(operation.getDispatcherRules(), + requestUrl); + // We only need the pattern here. + operation.addResourcePath(extractResourcePath(requestUrl, null)); + } else if (DispatchStyles.URI_PARTS.equals(rootDispatcher)) { + Map parts = buildRequestParts(requestNode); + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parts); + // We should complete resourcePath here. + String resourcePath = extractResourcePath(requestUrl, null); + operation.addResourcePath(URIBuilder.buildURIFromPattern(resourcePath, parts)); + } else if (DispatchStyles.URI_ELEMENTS.equals(rootDispatcher)) { + Map parts = buildRequestParts(requestNode); + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(rootDispatcherRules, parts); + dispatchCriteria += DispatchCriteriaHelper.extractFromURIParams(rootDispatcherRules, requestUrl); + // We should complete resourcePath here. + String resourcePath = extractResourcePath(requestUrl, null); + operation.addResourcePath(URIBuilder.buildURIFromPattern(resourcePath, parts)); + } else if (DispatchStyles.QUERY_HEADER.equals(rootDispatcher)) { + Map paramsMap = buildRequestHeaders(requestNode); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(rootDispatcherRules, paramsMap); + // We only need the pattern here. + operation.addResourcePath(extractResourcePath(requestUrl, null)); + } else if (DispatchStyles.QUERY_ARGS.equals(rootDispatcher)) { + // This dispatcher is used for GraphQL + if (requestNode.path("body").has(GRAPHQL_NODE)) { + // We must transform JSON representing variables into a Map + // before building a dispatchCriteria matching the rules. + String variables = requestNode.path("body").path(GRAPHQL_NODE).path(VARIABLES_NODE).asText(); + dispatchCriteria = extractQueryArgsCriteria(rootDispatcherRules, variables); + } + // or for gRPC. + if (requestNode.path("body").has("raw")) { + String variables = requestNode.path("body").path("raw").asText(); + dispatchCriteria = extractQueryArgsCriteria(rootDispatcherRules, variables); + } + + } else { + // If dispatcher has been overriden (to SCRIPT for example), we should still put a generic resourcePath + // (maybe containing : parts) to later force operation matching at the mock controller level. Only do that + // when request url is not empty (means not the root url like POST /order). + if (requestUrl != null && !requestUrl.isEmpty()) { + operation.addResourcePath(extractResourcePath(requestUrl, null)); + log.debug("Added operation generic resource path: {}", operation.getResourcePaths()); + } + } + + Request request = buildRequest(requestNode, responseNode.path("name").asText()); + Response response = buildResponse(responseNode, dispatchCriteria); + result.put(request, response); + } + } + } + return result; + } + + private Request buildRequest(JsonNode requestNode, String name) { + Request request = new Request(); + request.setName(name); + + if (isV2Collection) { + request.setHeaders(buildHeaders(requestNode.path("header"))); + if (requestNode.has("body") && requestNode.path("body").has("raw")) { + request.setContent(requestNode.path("body").path("raw").asText()); + } else if (requestNode.has("body") && requestNode.path("body").has(GRAPHQL_NODE)) { + // We got to rebuild a specific HTTP request for GraphQL. + String query = requestNode.path("body").path(GRAPHQL_NODE).path(QUERY_NODE).asText(); + String variables = requestNode.path("body").path(GRAPHQL_NODE).path(VARIABLES_NODE).asText(); + // Initialize with query field. + StringBuilder requestContent = new StringBuilder("{\"query\": \"").append(query.replace("\n", "\\n")) + .append("\""); + + try { + // See if we have to add variables. + JsonNode variablesNode = mapper.readTree(variables); + if (variablesNode.isObject() && !variablesNode.isEmpty()) { + requestContent.append(", \"variables\": ").append(variables); + } + } catch (JsonProcessingException jpe) { + log.warn("Json processing excpetion while parsing GraphQL variables, ignoring them: {}", + jpe.getMessage()); + } + requestContent.append("}"); + request.setContent(requestContent.toString()); + } + if (requestNode.path("url").has(VARIABLE_NODE)) { + JsonNode variablesNode = requestNode.path("url").path(VARIABLE_NODE); + for (JsonNode variableNode : variablesNode) { + Parameter param = new Parameter(); + param.setName(variableNode.path("key").asText()); + param.setValue(variableNode.path(VALUE_NODE).asText()); + request.addQueryParameter(param); + } + } + if (requestNode.path("url").has(QUERY_NODE)) { + JsonNode queryNode = requestNode.path("url").path(QUERY_NODE); + for (JsonNode variableNode : queryNode) { + Parameter param = new Parameter(); + param.setName(variableNode.path("key").asText()); + param.setValue(variableNode.path(VALUE_NODE).asText()); + request.addQueryParameter(param); + } + } + } else { + request.setHeaders(buildHeaders(requestNode.path("headers"))); + } + return request; + } + + private Map buildRequestParts(JsonNode requestNode) { + Map parts = new HashMap<>(); + if (requestNode.has("url") && requestNode.path("url").has(VARIABLE_NODE)) { + Iterator variables = requestNode.path("url").path(VARIABLE_NODE).elements(); + while (variables.hasNext()) { + JsonNode variable = variables.next(); + parts.put(variable.path("key").asText(), variable.path(VALUE_NODE).asText()); + } + } + return parts; + } + + private Map buildRequestHeaders(JsonNode requestNode) { + Map headers = new HashMap<>(); + if (requestNode.has("header")) { + Iterator headerNodes = requestNode.path("header").elements(); + while (headerNodes.hasNext()) { + JsonNode headerNode = headerNodes.next(); + headers.put(headerNode.path("key").asText(), headerNode.path(VALUE_NODE).asText()); + } + } + return headers; + } + + private String extractQueryArgsCriteria(String dispatcherRules, String variables) { + String dispatchCriteria = ""; + try { + Map paramsMap = mapper.readValue(variables, + TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class)); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, paramsMap); + } catch (Exception e) { + log.error("Exception while extracting dispatch criteria from JSON body: {}", e.getMessage()); + } + return dispatchCriteria; + } + + private Response buildResponse(JsonNode responseNode, String dispatchCriteria) { + Response response = new Response(); + response.setName(responseNode.path("name").asText()); + + if (isV2Collection) { + response.setStatus(responseNode.path("code").asText("200")); + response.setHeaders(buildHeaders(responseNode.path("header"))); + response.setContent(responseNode.path("body").asText("")); + } else { + response.setStatus(responseNode.path("responseCode").path("code").asText()); + response.setHeaders(buildHeaders(responseNode.path("headers"))); + response.setContent(responseNode.path("text").asText()); + } + if (response.getHeaders() != null) { + for (Header header : response.getHeaders()) { + if (header.getName().equalsIgnoreCase("Content-Type")) { + response.setMediaType(header.getValues().toArray(new String[] {})[0]); + } + } + } + // For V1 Collection, if no Content-Type header but response expressed as a language, + // assume it is its content-type. + if (!isV2Collection && response.getMediaType() == null) { + if ("json".equals(responseNode.path("language").asText())) { + response.setMediaType("application/json"); + } + } + // For V2 Collection, if no Content-Type header but response expressed as a language, + // assume it is its content-type. + if (isV2Collection && response.getMediaType() == null) { + if ("json".equals(responseNode.path("_postman_previewlanguage").asText())) { + response.setMediaType("application/json"); + } else if ("xml".equals(responseNode.path("_postman_previewlanguage").asText())) { + response.setMediaType("text/xml"); + } + } + response.setDispatchCriteria(dispatchCriteria); + return response; + } + + private Set
buildHeaders(JsonNode headerNode) { + if (headerNode == null || headerNode.isEmpty()) { + return null; + } + + // Prepare and map the set of headers. + Set
headers = new HashSet<>(); + Iterator items = headerNode.elements(); + while (items.hasNext()) { + JsonNode item = items.next(); + Header header = new Header(); + header.setName(item.path("key").asText()); + Set values = new HashSet<>(); + values.add(item.path(VALUE_NODE).asText()); + header.setValues(values); + headers.add(header); + } + return headers; + } + + /** Extract the list of operations from Collection. */ + private List extractOperations() throws MockRepositoryImportException { + if (isV2Collection) { + return extractOperationsV2(); + } + throw new MockRepositoryImportException(COLLECTION_VERSION_ERROR_MESSAGE); + } + + private List extractOperationsV2() { + // Items corresponding to same operations may be defined multiple times in Postman + // with different names and resource path. We have to track them to complete them in second step. + Map collectedOperations = new HashMap<>(); + + Iterator items = collection.path("item").elements(); + while (items.hasNext()) { + JsonNode item = items.next(); + extractOperationV2("", item, collectedOperations); + } + + return new ArrayList<>(collectedOperations.values()); + } + + private void extractOperationV2(String folderName, JsonNode itemNode, Map collectedOperations) { + log.debug("Extracting operation in folder {}", folderName); + String itemNodeName = itemNode.path("name").asText(); + + // Item may be a folder or an operation description. + if (!itemNode.has(REQUEST_NODE)) { + // Item is simply a folder that may contain some other folders recursively. + Iterator items = itemNode.path("item").elements(); + while (items.hasNext()) { + JsonNode item = items.next(); + extractOperationV2(folderName + "/" + itemNodeName, item, collectedOperations); + } + } else { + // Item is here an operation description. + String operationName = buildOperationName(itemNode, folderName); + Operation operation = collectedOperations.get(operationName); + String url = itemNode.path(REQUEST_NODE).path("url").asText(""); + if ("".equals(url)) { + url = itemNode.path(REQUEST_NODE).path("url").path("raw").asText(""); + } + + // Collection may have been used for testing so it may contain a valid URL with prefix that will bother us. + // Ex: http://localhost:8080/prefix1/prefix2/order/123456 or http://petstore.swagger.io/v2/pet/1. Trim it. + if (url.contains(folderName + "/")) { + url = removeProtocolAndHostPort(url); + } + + if (operation == null) { + // Build a new operation. + operation = new Operation(); + operation.setName(operationName); + + // Complete with REST specific fields. + operation.setMethod(itemNode.path(REQUEST_NODE).path("method").asText()); + + // Deal with dispatcher stuffs. + if (urlHasParameters(url) && urlHasParts(url)) { + operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(url) + " ?? " + + DispatchCriteriaHelper.extractParamsFromURI(url)); + operation.setDispatcher(DispatchStyles.URI_ELEMENTS); + } else if (urlHasParameters(url)) { + operation.setDispatcherRules(DispatchCriteriaHelper.extractParamsFromURI(url)); + operation.setDispatcher(DispatchStyles.URI_PARAMS); + operation.addResourcePath(extractResourcePath(url, null)); + } else if (urlHasParts(url)) { + operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(url)); + operation.setDispatcher(DispatchStyles.URI_PARTS); + } else { + operation.addResourcePath(extractResourcePath(url, null)); + log.debug("Added operation generic resource path: {}", operation.getResourcePaths()); + } + } + + // Do not deal with resource path now as it will be done when extracting messages. + collectedOperations.put(operationName, operation); + } + } + + /** + * Build a coherent operation name from the JsonNode of collection representing operation (ie. having a request item) + * and an operationNameRadix (ie. a subcontext or nested subcontext folder where operation is stored). + * @param operationNode JSON node for operation + * @param folderName String representing radix of operation name + * @return Operation name + */ + public static String buildOperationName(JsonNode operationNode, String folderName) { + String url = operationNode.path(REQUEST_NODE).path("url").asText(""); + if ("".equals(url)) { + url = operationNode.path(REQUEST_NODE).path("url").path("raw").asText(); + } + + // New way of computing operation name. + if (url.indexOf('?') != -1) { + // Remove query parameters. + url = url.substring(0, url.indexOf('?')); + } + // Remove protocol pragma and host/port stuffs. + url = removeProtocolAndHostPort(url); + return operationNode.path(REQUEST_NODE).path("method").asText() + " " + url; + } + + /** + * Extract a resource path from a complete url and an optional operationName radix (context or subcontext). + * https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=998bac0775b1d5f588e0a6ca7c11b852&status=available + * => /v2/pet/findByStatus if no operationNameRadix => /pet/findByStatus if operationNameRadix=/pet + */ + private String extractResourcePath(String url, String operationNameRadix) { + // Remove protocol, host and port specification. + String result = removeProtocolAndHostPort(url); + // Remove trailing parameters. + if (result.indexOf('?') != -1) { + result = result.substring(0, result.indexOf('?')); + } + // Remove prefix of radix if specified. + if (operationNameRadix != null && result.contains(operationNameRadix)) { + result = result.substring(result.indexOf(operationNameRadix)); + } + // Remove trailing / if present. + if (result.endsWith("/")) { + result = result.substring(0, result.length() - 1); + } + return result; + } + + /** Check parameters presence into given url. */ + private static boolean urlHasParameters(String url) { + return url.indexOf('?') != -1 && url.indexOf('=') != -1; + } + + /** Check variables parts presence into given url. */ + private static boolean urlHasParts(String url) { + return url.indexOf("/:") != -1; + } + + private static String removeProtocolAndHostPort(String url) { + if (url.startsWith("https://")) { + url = url.substring("https://".length()); + } + if (url.startsWith("http://")) { + url = url.substring("http://".length()); + } + // Remove host and port specification if any. + if (url.indexOf('/') != -1) { + url = url.substring(url.indexOf('/')); + } + return url; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanTestStepsRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanTestStepsRunner.java new file mode 100644 index 000000000..0fb180bf0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanTestStepsRunner.java @@ -0,0 +1,267 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.postman; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.util.URIBuilder; +import io.github.microcks.util.test.AbstractTestRunner; +import io.github.microcks.util.test.TestRunnerCommons; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * An implementation of HttpTestRunner that deals with tests embedded into a Postman Collection. It delegates the actual + * testing to the microcks-postman-runner component, triggering it through an API call. + * @author laurent + */ +public class PostmanTestStepsRunner extends AbstractTestRunner { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(PostmanTestStepsRunner.class); + + private ObjectMapper mapper = new ObjectMapper(); + + private JsonNode collection; + + private ResourceRepository resourceRepository; + private ClientHttpRequestFactory clientHttpRequestFactory; + + private String testsCallbackUrl = null; + + private String postmanRunnerUrl = null; + + /** + * Build a new PostmanTestStepsRunner for a collection. + * @param resourceRepository The repository that contains Postman Collection to test + */ + public PostmanTestStepsRunner(ResourceRepository resourceRepository) { + this.resourceRepository = resourceRepository; + } + + /** + * Set the ClientHttpRequestFactory used for reaching endpoint. + * @param clientHttpRequestFactory The ClientHttpRequestFactory used for reaching endpoint + */ + public void setClientHttpRequestFactory(ClientHttpRequestFactory clientHttpRequestFactory) { + this.clientHttpRequestFactory = clientHttpRequestFactory; + } + + public void setTestsCallbackUrl(String testsCallbackUrl) { + this.testsCallbackUrl = testsCallbackUrl; + } + + public void setPostmanRunnerUrl(String postmanRunnerUrl) { + this.postmanRunnerUrl = postmanRunnerUrl; + } + + @Override + public List runTest(Service service, Operation operation, TestResult testResult, List requests, + String endpointUrl, HttpMethod method) throws URISyntaxException, IOException { + if (log.isDebugEnabled()) { + log.debug("Launching test run on {} for {} request(s)", endpointUrl, requests.size()); + } + + // Retrieve the resource corresponding to OpenAPI specification if any. + Resource collectionResource = null; + List resources = resourceRepository.findByServiceId(service.getId()); + for (Resource resource : resources) { + if (ResourceType.POSTMAN_COLLECTION.equals(resource.getType())) { + collectionResource = resource; + break; + } + } + + // Convert them to Node using Jackson object mapper. + try { + collection = mapper.readTree(collectionResource.getContent()); + } catch (Exception e) { + throw new IOException("Postman collection file cannot be found or parsed", e); + } + + // Sanitize endpoint url. + if (endpointUrl.endsWith("/")) { + endpointUrl = endpointUrl.substring(0, endpointUrl.length() - 1); + } + + // Microcks-postman-runner interface object building. + ObjectNode jsonArg = mapper.createObjectNode(); + jsonArg.put("operation", operation.getName()); + jsonArg.put("callbackUrl", testsCallbackUrl + "/api/tests/" + testResult.getId() + "/testCaseResult"); + + // First we have to retrieved and add the test script for this operation from within Postman collection. + JsonNode testScript = extractOperationTestScript(operation); + if (testScript != null) { + log.debug("Found a testScript for this operation !"); + jsonArg.set("testScript", testScript); + } else { + // We have nothing to test and Postman runner will reject the request without testScript. + log.info("No testScript found for operation '{}', marking it as failed", operation.getName()); + return requests.stream().map(request -> new TestReturn(TestReturn.FAILURE_CODE, 0, + "Not executed cause no Test Script found for this operation", request, new Response())).toList(); + } + + // Then we have to add the corresponding 'requests' objects. + ArrayNode jsonRequests = mapper.createArrayNode(); + for (Request request : requests) { + ObjectNode jsonRequest = mapper.createObjectNode(); + + String operationName = operation.getName().substring(operation.getName().indexOf(" ") + 1); + String customizedEndpointUrl = endpointUrl + + URIBuilder.buildURIFromPattern(operationName, request.getQueryParameters()); + log.debug("Using customized endpoint url: {}", customizedEndpointUrl); + + jsonRequest.put("endpointUrl", customizedEndpointUrl); + jsonRequest.put("method", operation.getMethod()); + jsonRequest.put("name", request.getName()); + + if (request.getContent() != null && !request.getContent().isEmpty()) { + jsonRequest.put("body", request.getContent()); + } + if (request.getQueryParameters() != null && !request.getQueryParameters().isEmpty()) { + ArrayNode jsonParams = buildQueryParams(request.getQueryParameters()); + jsonRequest.set("queryParams", jsonParams); + } + // Set headers to request if any. Start with those coming from request itself. + // Add or override existing headers with test specific ones for operation and globals. + Set
headers = TestRunnerCommons.collectHeaders(testResult, request, operation); + if (!headers.isEmpty()) { + ArrayNode jsonHeaders = buildHeaders(headers); + jsonRequest.set("headers", jsonHeaders); + } + + jsonRequests.add(jsonRequest); + } + jsonArg.set("requests", jsonRequests); + + URI postmanRunnerURI = new URI(postmanRunnerUrl + "/tests/" + testResult.getId()); + ClientHttpRequest httpRequest = clientHttpRequestFactory.createRequest(postmanRunnerURI, HttpMethod.POST); + httpRequest.getBody().write(mapper.writeValueAsBytes(jsonArg)); + httpRequest.getHeaders().add("Content-Type", "application/json"); + + // Actually execute request. + ClientHttpResponse httpResponse = null; + try { + httpResponse = httpRequest.execute(); + } catch (IOException ioe) { + log.error("IOException while executing request ", ioe); + } finally { + if (httpResponse != null) { + httpResponse.close(); + } + } + + return new ArrayList<>(); + } + + @Override + public HttpMethod buildMethod(String method) { + return null; + } + + private JsonNode extractOperationTestScript(Operation operation) { + List collectedScripts = new ArrayList<>(); + + Iterator items = collection.path("item").elements(); + while (items.hasNext()) { + JsonNode item = items.next(); + extractTestScript("", item, operation, collectedScripts); + } + if (!collectedScripts.isEmpty()) { + return collectedScripts.get(0); + } + return null; + } + + private void extractTestScript(String operationNameRadix, JsonNode itemNode, Operation operation, + List collectedScripts) { + String itemNodeName = itemNode.path("name").asText(); + + // Item may be a folder or an operation description. + if (!itemNode.has("request")) { + // Item is simply a folder that may contain some other folders recursively. + Iterator items = itemNode.path("item").elements(); + while (items.hasNext()) { + JsonNode item = items.next(); + extractTestScript(operationNameRadix + "/" + itemNodeName, item, operation, collectedScripts); + } + } else { + // Item is here an operation description. + String operationName = PostmanCollectionImporter.buildOperationName(itemNode, operationNameRadix); + log.debug("Found operation '{}', comparing with '{}'", operationName, operation.getName()); + if (PostmanUtil.areOperationsEquivalent(operation.getName(), operationName)) { + // We've got the correct operation. + JsonNode events = itemNode.path("event"); + for (JsonNode event : events) { + if ("test".equals(event.path("listen").asText())) { + log.debug("Found a matching event where listen=test"); + collectedScripts.add(event); + } + } + } + } + } + + private ArrayNode buildQueryParams(List queryParameters) { + ArrayNode jsonQPS = mapper.createArrayNode(); + for (Parameter parameter : queryParameters) { + ObjectNode jsonQP = mapper.createObjectNode(); + jsonQP.put("key", parameter.getName()); + jsonQP.put("value", parameter.getValue()); + jsonQPS.add(jsonQP); + } + return jsonQPS; + } + + private ArrayNode buildHeaders(Set
headers) { + ArrayNode jsonHS = mapper.createArrayNode(); + for (Header header : headers) { + ObjectNode jsonH = mapper.createObjectNode(); + jsonH.put("key", header.getName()); + jsonH.put("value", buildValue(header.getValues())); + jsonHS.add(jsonH); + } + return jsonHS; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanUtil.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanUtil.java new file mode 100644 index 000000000..b9b98d55a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanUtil.java @@ -0,0 +1,67 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.postman; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.regex.PatternSyntaxException; + +/** + * Some utility functions/methods for dealing with Postman specific formatting. + * @author laurent + */ +public class PostmanUtil { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(PostmanUtil.class); + + /** Regular expression used to evaluate operation name matching. */ + private static final String OPERATION_NAME_EXPRESSION_PREFIX = "(GET|POST|PUT|PATCH|DELETE|OPTION)?( *)(/)?"; + + /** Private constructor to hide the default one. */ + private PostmanUtil() { + } + + /** + * Tells if 2 operations may be equivalent giving their names. Useful when comparing OpenAPI operations (containing + * {param} in path) and Postman operations (containing :param in path). + * @param operationNameRef Reference operation name (typically the one coming from OpenAPI) + * @param operationNameCandidate Candidate operatoin name (typically the one coming from Postman Collection) + * @return True if both are equivalent, false otherwise. + */ + public static boolean areOperationsEquivalent(String operationNameRef, String operationNameCandidate) { + // First check equals ignoring case. + if (operationNameRef.equalsIgnoreCase(operationNameCandidate)) { + return true; + } + // Then we may have an OpenAPI template we should convert to Postman and check again. + if (operationNameRef.contains("/{")) { + String transformedName = operationNameRef.replaceAll("/\\{", "/:").replace("}", ""); + if (transformedName.equalsIgnoreCase(operationNameCandidate)) { + return true; + } + } + + try { + // Finally check again adding a verb as prefix. + return operationNameCandidate.matches(OPERATION_NAME_EXPRESSION_PREFIX + operationNameRef); + } catch (PatternSyntaxException pse) { + log.warn("{}{} throws a PatternSyntaxException", OPERATION_NAME_EXPRESSION_PREFIX, operationNameRef); + } + return false; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanWorkspaceCollectionImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanWorkspaceCollectionImporter.java new file mode 100644 index 000000000..202a6a8ed --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/postman/PostmanWorkspaceCollectionImporter.java @@ -0,0 +1,61 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.postman; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Implement of MockRepositoryImporter that uses a Postman collection coming from a Postman Workspace (see + * https://www.postman.com/postman/workspace/postman-public-workspace/request/12959542-a6a282df-907e-438b-8fe6-e5efaa60b8bf) + * for building domain objects. Only v2 collection format is supported. + * @author laurent + */ +public class PostmanWorkspaceCollectionImporter extends PostmanCollectionImporter { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(PostmanWorkspaceCollectionImporter.class); + + private static final String COLLECTION_WRAPPER_PROPERTY = "collection"; + + /** + * Build a new importer. + * @param collectionFilePath The path to local Postman collection file + * @throws IOException if project file cannot be found or read. + */ + public PostmanWorkspaceCollectionImporter(String collectionFilePath) throws IOException { + super(); + try { + // Read Json bytes. + byte[] jsonBytes = Files.readAllBytes(Paths.get(collectionFilePath)); + setCollectionContent(new String(jsonBytes, StandardCharsets.UTF_8)); + // Convert them to Node using Jackson object mapper. + ObjectMapper mapper = new ObjectMapper(); + JsonNode collectionWrapper = mapper.readTree(jsonBytes); + setCollection(collectionWrapper.path(COLLECTION_WRAPPER_PROPERTY)); + } catch (Exception e) { + log.error("Exception while parsing Postman workspace collection file " + collectionFilePath, e); + throw new IOException("Postman workspace collection file parsing error"); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/FakeScriptMockRequest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/FakeScriptMockRequest.java new file mode 100644 index 000000000..5acff1c2d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/FakeScriptMockRequest.java @@ -0,0 +1,82 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.script; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Map; + +/** + * This is a fake, lightweight implementation of mockRequest objects that are typically available within SoapUI script + * context (see http://www.soapui.org/Service-Mocking/creating-dynamic-mockservices.html for explanations on how they + * are used). Such object should follow contract exposed by + * http://www.soapui.org/apidocs/com/eviware/soapui/impl/wsdl/mock/WsdlMockRequest.html, and because it's Groovy dynamic + * stuff we do not have to implement a particular interface. Groovy !! isn't it ?? + * @author laurent + */ +public class FakeScriptMockRequest { + + /** The HttpServletRequest wrapped object. */ + private HttpServletRequest request; + /** The content of mock Request (request body indeed) */ + private String requestContent; + /** The headers of mock Request (http headers most of the time !) */ + private StringToStringsMap requestHeaders; + /** The URI parameters of mock Request */ + private Map uriParameters; + + /** + * Create a new fake request from content and headers. + * @param requestContent The request content + * @param requestHeaders The request headers + */ + public FakeScriptMockRequest(String requestContent, StringToStringsMap requestHeaders) { + this.requestContent = requestContent; + this.requestHeaders = requestHeaders; + } + + public HttpServletRequest getRequest() { + return request; + } + + public void setRequest(HttpServletRequest request) { + this.request = request; + } + + public String getRequestContent() { + return requestContent; + } + + public void setRequestContent(String requestContent) { + this.requestContent = requestContent; + } + + public StringToStringsMap getRequestHeaders() { + return requestHeaders; + } + + public void setRequestHeaders(StringToStringsMap requestHeaders) { + this.requestHeaders = requestHeaders; + } + + public void setURIParameters(Map uriParameters) { + this.uriParameters = uriParameters; + } + + public Map getURIParameters() { + return uriParameters; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/HttpHeadersStringToStringsMap.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/HttpHeadersStringToStringsMap.java new file mode 100644 index 000000000..887f4e152 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/HttpHeadersStringToStringsMap.java @@ -0,0 +1,81 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.script; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * An implementation of com.eviware.soapui.support.types.StringToStringsMap that implements the RFC 7230 regarding + * header name case-insensitiveness and to ensure a compatibility layer withe SoapUI scripting. + * @author laurent + */ +public class HttpHeadersStringToStringsMap extends StringToStringsMap { + + public HttpHeadersStringToStringsMap() { + } + + /** + * Override of HashMap get() to implement case-insensitive search of key. + * @param key The Http header name (case-insensitive) + * @return The value as List of String if nay, or null + */ + @Override + public List get(Object key) { + Iterator>> var3 = this.entrySet().iterator(); + + Map.Entry> stringListEntry; + do { + if (!var3.hasNext()) { + return null; + } + + stringListEntry = var3.next(); + } while (!key.toString().equalsIgnoreCase(stringListEntry.getKey()) || (stringListEntry.getValue()).isEmpty()); + + return stringListEntry.getValue(); + } + + /** + * Override of HashMap getOrDefault() to implement case-insensitive search of key. + * @param key The Http header name (case-insensitive) + * @param defaultValue The default mapping of the key + * @return The value as List of String if nay, or null + */ + @Override + public List getOrDefault(Object key, List defaultValue) { + List value; + return (value = this.get(key)) == null ? defaultValue : value; + } + + + public boolean hasValues(String key) { + return this.containsKeyIgnoreCase(key) && ((List) this.get(key)).size() > 0; + } + + public void add(String key, String string) { + List updatedValue = this.getOrDefault(key, new ArrayList()); + updatedValue.add(string); + this.put(key, updatedValue); + } + + public String get(String key, String defaultValue) { + return getCaseInsensitive(key, defaultValue); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/ScriptEngineBinder.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/ScriptEngineBinder.java new file mode 100644 index 000000000..ca4935ba7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/ScriptEngineBinder.java @@ -0,0 +1,136 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.script; + +import io.github.microcks.service.StateStore; +import io.github.microcks.util.http.HttpHeadersUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.http.HttpServletRequest; +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.SimpleScriptContext; +import java.util.Map; + +/** + * Utility class that holds methods for creating binding environments and evaluation context for a JSR 233 ScriptEngine. + * @author laurent + */ +public class ScriptEngineBinder { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(ScriptEngineBinder.class); + + /** Private constructor to hide the implicit public one. */ + private ScriptEngineBinder() { + } + + /** + * Build a ScriptContext from Http request for a ScriptEngine. + * @param engine The engine to enrich with binding environment. + * @param requestContent The content of request to use as data + * @param requestContext The execution context of this request + * @param stateStore A store to save/get state from script + * @param request The wrapped incoming servlet request. + * @return The evaluation context for the script engine eval() method. + */ + public static ScriptContext buildEvaluationContext(ScriptEngine engine, String requestContent, + Map requestContext, StateStore stateStore, HttpServletRequest request) { + StringToStringsMap headers = HttpHeadersUtil.extractFromHttpServletRequest(request); + return buildEvaluationContext(engine, requestContent, requestContext, stateStore, headers, request); + } + + /** + * Create and bind an environment for a ScriptEngine. + * @param engine The engine to enrich with binding environment. + * @param requestContent The content of request to use as data + * @param requestContext The execution context of this request + * @param stateStore A store to save/get state from script + * @param request The wrapped incoming servlet request. + * @param uriParameters The URI parameters of the request + * @return The evaluation context for the script engine eval() method. + */ + public static ScriptContext buildEvaluationContext(ScriptEngine engine, String requestContent, + Map requestContext, StateStore stateStore, HttpServletRequest request, + Map uriParameters) { + StringToStringsMap headers = HttpHeadersUtil.extractFromHttpServletRequest(request); + return buildEvaluationContext(engine, requestContent, requestContext, stateStore, headers, request, + uriParameters); + } + + /** + * Build an evaluation ScriptContext for a ScriptEngine. + * @param engine The engine to enrich with binding environment. + * @param requestContent The content of request to use as data + * @param requestContext The execution context of this request + * @param stateStore A store to save/get state from script + * @param headers The header values of the request + * @param request The wrapped incoming servlet request. + * @return The evaluation context for the script engine eval() method. + */ + public static ScriptContext buildEvaluationContext(ScriptEngine engine, String requestContent, + Map requestContext, StateStore stateStore, StringToStringsMap headers, + HttpServletRequest request) { + return buildEvaluationContext(engine, requestContent, requestContext, stateStore, headers, request, null); + } + + /** + * Build an evaluation ScriptContext for a ScriptEngine. + * @param engine The engine to enrich with binding environment. + * @param requestContent The content of request to use as data + * @param requestContext The execution context of this request + * @param stateStore A store to save/get state from script + * @param headers The header values of the request + * @param request The wrapped incoming servlet request. + * @param uriParameters The URI parameters of the request + * @return The evaluation context for the script engine eval() method. + */ + public static ScriptContext buildEvaluationContext(ScriptEngine engine, String requestContent, + Map requestContext, StateStore stateStore, StringToStringsMap headers, + HttpServletRequest request, Map uriParameters) { + + // Build a fake request container. + FakeScriptMockRequest mockRequest = new FakeScriptMockRequest(requestContent, headers); + mockRequest.setRequest(request); + mockRequest.setURIParameters(uriParameters); + + // Create bindings and put content according to SoapUI binding environment. + Bindings bindings = engine.createBindings(); + bindings.put("mockRequest", mockRequest); + bindings.put("log", log); + bindings.put("requestContext", requestContext); + bindings.put("store", stateStore); + + SimpleScriptContext scriptContext = new SimpleScriptContext(); + scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE); + return scriptContext; + } + + /** + * Review and adapt a script so that we ensure its compatibility with legacy SoapUI helper. + * @param script The script to review and adapt + * @return The script that may have been changed + */ + public static String ensureSoapUICompatibility(String script) { + if (script.contains("com.eviware.soapui.support.XmlHolder")) { + return script.replaceAll("com.eviware.soapui.support.XmlHolder", "io.github.microcks.util.soapui.XmlHolder"); + } + return script; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/StringToStringsMap.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/StringToStringsMap.java new file mode 100644 index 000000000..c7a089e46 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/script/StringToStringsMap.java @@ -0,0 +1,188 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.script; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * A minimalist implementation of com.eviware.soapui.support.types.StringToStringsMap to ensure a compatibility layer + * withe SoapUI scripting. + * @author laurent + */ +public class StringToStringsMap extends HashMap> { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(StringToStringsMap.class); + + private boolean equalsOnThis; + + public StringToStringsMap() { + } + + public List get(String key, List defaultValue) { + List value = this.get(key); + return value == null ? defaultValue : value; + } + + public boolean hasValues(String key) { + return this.containsKey(key) && ((List) this.get(key)).size() > 0; + } + + public void add(String key, boolean value) { + this.add(key, Boolean.toString(value)); + } + + public void add(String key, String string) { + if (!this.containsKey(key)) { + this.put(key, new ArrayList()); + } + + this.get(key).add(string); + } + + public static StringToStringsMap fromHttpHeader(String value) { + StringToStringsMap result = new StringToStringsMap(); + + for (int ix = value.indexOf(59); ix > 0; ix = value.indexOf(59)) { + extractNVPair(value.substring(0, ix), result); + value = value.substring(ix + 1); + } + + if (value.length() > 2) { + extractNVPair(value, result); + } + + return result; + } + + private static void extractNVPair(String value, StringToStringsMap result) { + int ix = value.indexOf(61); + if (ix != -1) { + String str = value.substring(ix + 1).trim(); + if (str.startsWith("\"") && str.endsWith("\"")) { + str = str.substring(1, str.length() - 1); + } + + result.add(value.substring(0, ix).trim(), str); + } + + } + + public void setEqualsOnThis(boolean equalsOnThis) { + this.equalsOnThis = equalsOnThis; + } + + public boolean equals(Object o) { + return this.equalsOnThis ? this == o : super.equals(o); + } + + public String[] getKeys() { + return (String[]) this.keySet().toArray(new String[this.size()]); + } + + public boolean containsKeyIgnoreCase(String string) { + Iterator var2 = this.keySet().iterator(); + + String key; + do { + if (!var2.hasNext()) { + return false; + } + + key = (String) var2.next(); + } while (!key.equalsIgnoreCase(string)); + + return true; + } + + public void put(String name, String value) { + this.add(name, value); + } + + public String get(String key, String defaultValue) { + List value = this.get(key); + return value != null && value.size() != 0 ? value.get(0) : defaultValue; + } + + public String getCaseInsensitive(String key, String defaultValue) { + Iterator var3 = this.entrySet().iterator(); + + Map.Entry stringListEntry; + do { + if (!var3.hasNext()) { + return defaultValue; + } + + stringListEntry = (Map.Entry) var3.next(); + } while (!key.equalsIgnoreCase((String) stringListEntry.getKey()) + || ((List) stringListEntry.getValue()).isEmpty()); + + return (String) ((List) stringListEntry.getValue()).get(0); + } + + public void replace(String key, String oldValue, String value) { + List values = this.get(key); + if (values != null) { + int ix = values.indexOf(oldValue); + if (ix >= 0) { + values.set(ix, value); + } + + } + } + + public void remove(String key, String data) { + List values = this.get(key); + if (values != null) { + values.remove(data); + } + } + + public int valueCount() { + int result = 0; + + String key; + for (Iterator var2 = this.keySet().iterator(); var2.hasNext(); result += ((List) this.get(key)).size()) { + key = (String) var2.next(); + } + + return result; + } + + public String toString() { + StringBuilder result = new StringBuilder(); + Iterator var2 = this.keySet().iterator(); + + while (var2.hasNext()) { + String key = (String) var2.next(); + Iterator var4 = ((List) this.get(key)).iterator(); + + while (var4.hasNext()) { + String value = (String) var4.next(); + result.append(key).append(" : ").append(value).append("\r\n"); + } + } + + return result.toString(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIAssertionsTestRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIAssertionsTestRunner.java new file mode 100644 index 000000000..1ddedb854 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIAssertionsTestRunner.java @@ -0,0 +1,238 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.util.MalformedXmlException; +import io.github.microcks.util.soapui.assertions.AssertionFactory; +import io.github.microcks.util.soapui.assertions.AssertionStatus; +import io.github.microcks.util.soapui.assertions.ExchangeContext; +import io.github.microcks.util.soapui.assertions.RequestResponseExchange; +import io.github.microcks.util.soapui.assertions.SoapUIAssertion; +import io.github.microcks.util.test.HttpTestRunner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpResponse; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.github.microcks.util.soapui.SoapUIProjectParserUtils.getConfigDirectChildren; +import static io.github.microcks.util.soapui.SoapUIProjectParserUtils.getConfigUniqueDirectChild; +import static io.github.microcks.util.soapui.SoapUIProjectParserUtils.hasConfigDirectChild; + +/** + * This is a utility class for running Service tests using assertions defined under a corresponding SoapUI project. + * @author laurent + */ +public class SoapUIAssertionsTestRunner extends HttpTestRunner { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(SoapUIAssertionsTestRunner.class); + + /** The URL of resources used for validation. */ + private String resourceUrl = null; + + private final ResourceRepository resourceRepository; + private final Map cachedResources = new HashMap<>(); + + private Element projectElement; + + private List lastValidationErrors = null; + + private long startTimestamp; + + + public SoapUIAssertionsTestRunner(ResourceRepository resourceRepository) { + this.resourceRepository = resourceRepository; + } + + /** + * The URL of resources used for validation. + * @return The URL of resources used for validation + */ + public String getResourceUrl() { + return resourceUrl; + } + + /** + * The URL of resources used for validation. + * @param resourceUrl The URL of resources used for validation. + */ + public void setResourceUrl(String resourceUrl) { + this.resourceUrl = resourceUrl; + } + + @Override + public HttpMethod buildMethod(String method) { + return super.buildMethod(method); + } + + @Override + protected void prepareRequest(Request request) { + startTimestamp = System.currentTimeMillis(); + } + + @Override + protected int extractTestReturnCode(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse, String responseContent) { + // Stop timer and initialize code. + long duration = System.currentTimeMillis() - startTimestamp; + int code = TestReturn.SUCCESS_CODE; + + // If first request of this runner, we should retrieve resources and cache them. + if (cachedResources.isEmpty()) { + List resources = resourceRepository.findByServiceId(service.getId()); + for (Resource resource : resources) { + cachedResources.put(resource.getType(), resource); + } + } + + // If first request of this runner, we should parse SoapUI project. + if (projectElement == null) { + Resource soapuiProject = cachedResources.get(ResourceType.SOAP_UI_PROJECT); + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + projectElement = documentBuilder.parse(new InputSource(new StringReader(soapuiProject.getContent()))) + .getDocumentElement(); + } catch (Exception e) { + log.error("Exception while parsing SoapUI resource content {}", soapuiProject.getName(), e); + return TestReturn.FAILURE_CODE; + } + } + + try { + Map testConfigRequests = collectTestStepsConfigRequest(operation); + Element testRequest = testConfigRequests.get(getRequestShortName(request.getName())); + + // Now validate the embedded assertions. + List assertions = getConfigDirectChildren(testRequest, "assertion"); + for (Element assertion : assertions) { + String type = assertion.getAttribute("type"); + Map configParams = buildParamsMapFromConfiguration(assertion); + + SoapUIAssertion sAssertion = AssertionFactory.intializeAssertion(type, configParams); + AssertionStatus status = sAssertion.assertResponse( + new RequestResponseExchange(request, httpResponse, responseContent, duration), + new ExchangeContext(service, operation, List.copyOf(cachedResources.values()), resourceUrl)); + + if (status == AssertionStatus.FAILED) { + code = TestReturn.FAILURE_CODE; + if (lastValidationErrors == null) { + lastValidationErrors = new ArrayList<>(); + } + lastValidationErrors.addAll(sAssertion.getErrorMessages()); + } + } + } catch (MalformedXmlException e) { + log.error("Exception while parsing SoapUI resource content for assertion on {}", operation.getName(), e); + return TestReturn.FAILURE_CODE; + } + + return code; + } + + @Override + protected String extractTestReturnMessage(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse) { + StringBuilder builder = new StringBuilder(); + if (lastValidationErrors != null && !lastValidationErrors.isEmpty()) { + for (String error : lastValidationErrors) { + builder.append(error).append('\n'); + } + } + // Reset just after consumption so avoid side-effects. + lastValidationErrors = null; + return builder.toString(); + } + + private Map collectTestStepsConfigRequest(Operation operation) throws MalformedXmlException { + Map results = new HashMap<>(); + + List testSuites = getConfigDirectChildren(projectElement, "testSuite"); + for (Element testSuite : testSuites) { + List testCases = getConfigDirectChildren(testSuite, "testCase"); + for (Element testCase : testCases) { + List testSteps = getConfigDirectChildren(testCase, "testStep"); + for (Element testStep : testSteps) { + Element config = getConfigUniqueDirectChild(testStep, "config"); + + String operationName = null; + if (hasConfigDirectChild(config, "operation")) { + // Soap/Wsdl test request with operation reference. + operationName = getConfigUniqueDirectChild(config, "operation").getTextContent(); + if (operation.getName().equals(operationName)) { + results.put(getRequestShortName(testStep.getAttribute("name")), + getConfigUniqueDirectChild(config, "request")); + } + } else if (config.hasAttribute("resourcePath")) { + // Rest test request with resourcePath as operation reference. + operationName = config.getAttribute("resourcePath"); + if (operation.getName().equals(operationName)) { + results.put(getRequestShortName(testStep.getAttribute("name")), + SoapUIProjectParserUtils.getConfigUniqueDirectChild(config, "restRequest")); + } + } + } + } + } + return results; + } + + /** If a request name ends with " Request", we remove it to get a short name. */ + private String getRequestShortName(String longName) { + if (longName.endsWith(" Request")) { + return longName.substring(0, longName.length() - " Request".length()); + } + return longName; + } + + private Map buildParamsMapFromConfiguration(Element assertion) { + Map params = new HashMap<>(); + if (hasConfigDirectChild(assertion, "configuration")) { + try { + Element configuration = getConfigUniqueDirectChild(assertion, "configuration"); + NodeList children = configuration.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + params.put(child.getLocalName(), child.getTextContent()); + } + } catch (MalformedXmlException mfe) { + // Just ignore this as it must not happen. + } + } + return params; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIProjectImporter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIProjectImporter.java new file mode 100644 index 000000000..e3ac35a03 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIProjectImporter.java @@ -0,0 +1,795 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MalformedXmlException; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.MockRepositoryImporter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static io.github.microcks.util.XmlUtil.WSDL_NS; +import static io.github.microcks.util.XmlUtil.getDirectChildren; +import static io.github.microcks.util.XmlUtil.getUniqueDirectChild; +import static io.github.microcks.util.soapui.SoapUIProjectParserUtils.getConfigDirectChildren; +import static io.github.microcks.util.soapui.SoapUIProjectParserUtils.getConfigUniqueDirectChild; +import static io.github.microcks.util.soapui.SoapUIProjectParserUtils.hasConfigDirectChild; + +/** + * Implement of MockRepositoryImporter that uses a SoapUI project for building domain objects. + * @author laurent + */ +public class SoapUIProjectImporter implements MockRepositoryImporter { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(SoapUIProjectImporter.class); + + /** SoapUI project property that references service version property. */ + public static final String SERVICE_VERSION_PROPERTY = "version"; + + protected static final String NAME_ATTRIBUTE = "name"; + protected static final String ELEMENT_ATTRIBUTE = "element"; + protected static final String MOCK_SERVICE_TAG = "mockService"; + protected static final String REST_MOCK_SERVICE_TAG = "restMockService"; + protected static final String MOCK_OPERATION_TAG = "mockOperation"; + protected static final String REST_MOCK_ACTION_TAG = "restMockAction"; + protected static final String QUERY_TAG = "query"; + protected static final String REQUEST_TAG = "request"; + protected static final String RESPONSE_TAG = "response"; + protected static final String CONTENT_TAG = "content"; + protected static final String VALUE_TAG = "value"; + + private final String projectContent; + private final DocumentBuilder documentBuilder; + private final Element projectElement; + + private final Map interfaces = new HashMap<>(); + + private Element serviceInterface; + + /** + * Build a new importer. + * @param projectFilePath The path to local SoapUI project file + * @throws IOException if project file cannot be found + */ + public SoapUIProjectImporter(String projectFilePath) throws IOException { + try { + // Read project content as string. + byte[] projectBytes = Files.readAllBytes(Paths.get(projectFilePath)); + projectContent = new String(projectBytes, StandardCharsets.UTF_8); + // Then parse it to get DOM root Element. + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + documentBuilder = factory.newDocumentBuilder(); + projectElement = documentBuilder.parse(new InputSource(new StringReader(projectContent))).getDocumentElement(); + } catch (Exception e) { + log.error("Exception while parsing SoapUI file {}", projectFilePath, e); + throw new IOException("SoapUI project file parsing error"); + } + } + + @Override + public List getServiceDefinitions() throws MockRepositoryImportException { + List result = new ArrayList<>(); + + List interfaceNodes = getConfigDirectChildren(projectElement, "interface"); + for (Element interfaceNode : interfaceNodes) { + // Filter complete interface definition with name as attribute. + if (interfaceNode.getAttribute(NAME_ATTRIBUTE) != null) { + log.info("Found a service interface named: {}", interfaceNode.getAttribute(NAME_ATTRIBUTE)); + interfaces.put(interfaceNode.getAttribute(NAME_ATTRIBUTE), interfaceNode); + serviceInterface = interfaceNode; + } + } + + // Try loading definitions from Soap mock services. + List mockServices = getConfigDirectChildren(projectElement, MOCK_SERVICE_TAG); + if (!mockServices.isEmpty()) { + result.addAll(getSoapServicesDefinitions(mockServices)); + } + // Then try loading from Rest mock services. + List restMockServices = getConfigDirectChildren(projectElement, REST_MOCK_SERVICE_TAG); + if (!restMockServices.isEmpty()) { + result.addAll(getRestServicesDefinitions(restMockServices)); + } + + return result; + } + + private List getSoapServicesDefinitions(List mockServices) throws MockRepositoryImportException { + List result = new ArrayList<>(); + + try { + for (Element mockService : mockServices) { + // Build a new Service. + Service service = new Service(); + service.setName(mockService.getAttribute(NAME_ATTRIBUTE)); + service.setType(ServiceType.SOAP_HTTP); + + // Check version property that is mandatory. + Element properties = getConfigUniqueDirectChild(mockService, "properties"); + service.setVersion(extractVersion(properties)); + + List mockOperations = getConfigDirectChildren(mockService, MOCK_OPERATION_TAG); + for (Element mockOperation : mockOperations) { + + Element interfaceElement = interfaces.get(mockOperation.getAttribute("interface")); + if (interfaceElement != null) { + log.info("Got matching service interface"); + String bindingQName = interfaceElement.getAttribute("bindingName"); + service.setXmlNS(extractNSFromQName(bindingQName)); + } + + // Build a new operation from mockOperation. + Operation operation = new Operation(); + operation.setName(mockOperation.getAttribute(NAME_ATTRIBUTE)); + + Element interfaceOperation = getInterfaceOperation(interfaceElement, operation.getName()); + operation.setAction(interfaceOperation.getAttribute("action")); + + try { + completeOperationPartsFromWsdl(interfaceElement, operation); + } catch (Exception e) { + // Fallback to input name as a (mostly?) safe default. + log.warn( + "Was not able to extract element names for input/output payload from WSDL. Defaulting to input and output names."); + } finally { + if (operation.getInputName() == null) { + operation.setInputName(interfaceOperation.getAttribute("inputName")); + } + if (operation.getOutputName() == null) { + operation.setOutputName(interfaceOperation.getAttribute("outputName")); + } + } + + Element dispatchStyle = getConfigUniqueDirectChild(mockOperation, "dispatchStyle"); + operation.setDispatcher(dispatchStyle.getTextContent()); + + if (DispatchStyles.QUERY_MATCH.equals(operation.getDispatcher())) { + // XPath matching rules are under dispatchConfig.query, consider 1st item only. + Element dispatchConfig = getConfigUniqueDirectChild(mockOperation, "dispatchConfig"); + Element firstQuery = getConfigDirectChildren(dispatchConfig, QUERY_TAG).get(0); + operation.setDispatcherRules(getConfigUniqueDirectChild(firstQuery, QUERY_TAG).getTextContent()); + } else if (DispatchStyles.SCRIPT.equals(operation.getDispatcher())) { + // Groovy script is located into dispatchPath element. + operation + .setDispatcherRules(getConfigUniqueDirectChild(mockOperation, "dispatchPath").getTextContent()); + } + + service.addOperation(operation); + } + result.add(service); + } + } catch (MalformedXmlException mspe) { + log.error("Your SoapUI Project seems to be malformed: {}", mspe.getMessage(), mspe); + throw new MockRepositoryImportException("Your SoapUI Project seems to be malformed: " + mspe.getMessage(), + mspe); + } + return result; + } + + private void completeOperationPartsFromWsdl(Element interfaceElement, Operation operation) throws Exception { + Element definitionCache = getConfigUniqueDirectChild(interfaceElement, "definitionCache"); + List parts = getConfigDirectChildren(definitionCache, "part"); + + Element wsdlPart = parts.get(0); + Element wsdlContent = getConfigUniqueDirectChild(wsdlPart, CONTENT_TAG); + String wsdlTextContent = wsdlContent.getTextContent(); + + Element wsdlDoc = documentBuilder.parse(new InputSource(new StringReader(wsdlTextContent))).getDocumentElement(); + Element binding = getUniqueDirectChild(wsdlDoc, WSDL_NS, "binding"); + List wsdlOperations = getDirectChildren(binding, WSDL_NS, "operation"); + for (Element wsdlOperation : wsdlOperations) { + + if (operation.getName().equals(wsdlOperation.getAttribute(NAME_ATTRIBUTE))) { + Element input = getUniqueDirectChild(wsdlOperation, WSDL_NS, "input"); + Element output = getUniqueDirectChild(wsdlOperation, WSDL_NS, "output"); + String inputName = input.getAttribute(NAME_ATTRIBUTE); + String outputName = output.getAttribute(NAME_ATTRIBUTE); + + List messages = getDirectChildren(wsdlDoc, WSDL_NS, "message"); + Optional inputMsg = messages.stream().filter(m -> inputName.equals(m.getAttribute(NAME_ATTRIBUTE))) + .findFirst(); + Optional outputMsg = messages.stream() + .filter(m -> outputName.equals(m.getAttribute(NAME_ATTRIBUTE))).findFirst(); + if (inputMsg.isPresent()) { + Element firstPart = getDirectChildren(inputMsg.get(), WSDL_NS, "part").get(0); + String localTag = firstPart.getAttribute(ELEMENT_ATTRIBUTE) + .substring(firstPart.getAttribute(ELEMENT_ATTRIBUTE).indexOf(":") + 1); + operation.setInputName(localTag); + } + if (outputMsg.isPresent()) { + Element firstPart = getDirectChildren(outputMsg.get(), WSDL_NS, "part").get(0); + String localTag = firstPart.getAttribute(ELEMENT_ATTRIBUTE) + .substring(firstPart.getAttribute(ELEMENT_ATTRIBUTE).indexOf(":") + 1); + operation.setOutputName(localTag); + } + } + } + } + + private List getRestServicesDefinitions(List restMockServices) + throws MockRepositoryImportException { + List result = new ArrayList<>(); + + try { + for (Element mockService : restMockServices) { + // Build a new Service. + Service service = new Service(); + service.setName(mockService.getAttribute(NAME_ATTRIBUTE)); + service.setType(ServiceType.REST); + + // Check version property that is mandatory. + Element properties = getConfigUniqueDirectChild(mockService, "properties"); + service.setVersion(extractVersion(properties)); + + // Actions corresponding to same operations may be defined multiple times in SoapUI + // with different resourcePaths. We have to track them to complete them in second step. + Map collectedOperations = new HashMap<>(); + + List mockOperations = getConfigDirectChildren(mockService, REST_MOCK_ACTION_TAG); + for (Element mockOperation : mockOperations) { + // Check already found operation. + Operation operation = collectedOperations.get(mockOperation.getAttribute(NAME_ATTRIBUTE)); + + if (operation == null) { + // Build a new operation from mockRestAction. + operation = new Operation(); + operation.setName(mockOperation.getAttribute(NAME_ATTRIBUTE)); + operation.setMethod(mockOperation.getAttribute("method")); + + Element dispatchStyle = getConfigUniqueDirectChild(mockOperation, "dispatchStyle"); + operation.setDispatcher(dispatchStyle.getTextContent()); + + if (DispatchStyles.SEQUENCE.equals(operation.getDispatcher())) { + // Extract simple dispatcher rules from operation name. + operation + .setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(operation.getName())); + } else if (DispatchStyles.SCRIPT.equals(operation.getDispatcher())) { + // Groovy script is located into dispatchPath element. + operation.setDispatcherRules( + getConfigUniqueDirectChild(mockOperation, "dispatchPath").getTextContent()); + } + service.addOperation(operation); + } + // Add this configuration resource path. + operation.addResourcePath(mockOperation.getAttribute("resourcePath")); + collectedOperations.put(mockOperation.getAttribute("name"), operation); + } + result.add(service); + } + } catch (MalformedXmlException mspe) { + log.error("Your SoapUI Project seems to be malformed: {}", mspe.getMessage(), mspe); + throw new MockRepositoryImportException("Your SoapUI Project seems to be malformed: " + mspe.getMessage(), + mspe); + } + return result; + } + + @Override + public List getResourceDefinitions(Service service) { + List results = new ArrayList<>(); + + // First record the project itself as an artifact. + Resource projectResource = new Resource(); + projectResource.setName(service.getName() + "-" + service.getVersion() + ".xml"); + projectResource.setType(ResourceType.SOAP_UI_PROJECT); + projectResource.setContent(projectContent); + results.add(projectResource); + + try { + Element definitionCache = getConfigUniqueDirectChild(serviceInterface, "definitionCache"); + List parts = getConfigDirectChildren(definitionCache, "part"); + + if (!parts.isEmpty()) { + Element wsdlPart = parts.get(0); + Element wsdlContent = getConfigUniqueDirectChild(wsdlPart, CONTENT_TAG); + String wsdlTextContent = wsdlContent.getTextContent(); + + for (int i = 1; i < parts.size(); i++) { + Element xsdPart = parts.get(i); + String xsdUrl = getConfigUniqueDirectChild(xsdPart, "url").getTextContent().trim(); + String xsdName = xsdUrl.substring(xsdUrl.lastIndexOf('/') + 1); + // Try also Windows style path separators. + if (xsdUrl.contains("\\")) { + xsdName = xsdUrl.substring(xsdUrl.lastIndexOf("\\") + 1); + } + + String xsdContent = getConfigUniqueDirectChild(xsdPart, CONTENT_TAG).getTextContent(); + + Resource xsdResource = new Resource(); + xsdResource.setName(xsdName); + xsdResource.setType(ResourceType.XSD); + xsdResource.setContent(xsdContent); + results.add(xsdResource); + + // URL references within WSDL must be replaced by their local counterpart. + wsdlTextContent = wsdlTextContent.replace(xsdUrl, "./" + xsdName); + // TODO: have a in-depth review on how xsd are actually resolved (and should probably be fixed) + // wsdlTextContent = wsdlTextContent.replaceAll("schemaLocation=\"(.*)\\/(" + xsdName + ")", "schemaLocation=\"./" + xsdName + "\""); + } + + Resource wsdlResource = new Resource(); + wsdlResource.setName(service.getName() + "-" + service.getVersion() + ".wsdl"); + wsdlResource.setType(ResourceType.WSDL); + wsdlResource.setContent(wsdlTextContent); + results.add(wsdlResource); + } + } catch (MalformedXmlException mxe) { + log.warn("Got a MalformedXmlException while trying to extract WSDL and XSD: {}", mxe.getMessage()); + log.warn("Just failing silently as it's not a critical stuff in SoapUI implementation"); + } + + return results; + } + + @Override + public List getMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + List results = new ArrayList<>(); + + if (ServiceType.SOAP_HTTP == service.getType()) { + results.addAll(getSoapMessageDefinitions(service, operation)); + } else if (ServiceType.REST == service.getType()) { + results.addAll(getRestMessageDefinitions(service, operation)); + } + return results; + } + + private List getSoapMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + Map result = new HashMap<>(); + try { + List mockServices = getConfigDirectChildren(projectElement, MOCK_SERVICE_TAG); + for (Element mockService : mockServices) { + // Find the appropriate mock service. + if (service.getName().equals(mockService.getAttribute(NAME_ATTRIBUTE))) { + + List mockOperations = getConfigDirectChildren(mockService, MOCK_OPERATION_TAG); + for (Element mockOperation : mockOperations) { + // Find the appropriate mock service operation. + if (operation.getName().equals(mockOperation.getAttribute("operation"))) { + // Collect available test requests for this operation. + Map availableRequests = collectTestStepsRequests(operation); + // Then filter only those that are candidates to mock response matching. + List candidateRequests = new ArrayList<>(); + + List mockResponses = getConfigDirectChildren(mockOperation, RESPONSE_TAG); + for (Element mockResponse : mockResponses) { + String responseName = mockResponse.getAttribute(NAME_ATTRIBUTE); + Element matchingRequest = availableRequests.get(responseName); + if (matchingRequest == null) { + matchingRequest = availableRequests.get(responseName + " Request"); + } + if (matchingRequest == null && responseName.contains("Response")) { + matchingRequest = availableRequests.get(responseName.replace("Response", "Request")); + } + + if (matchingRequest == null) { + log.warn("No request found for response '{}' into SoapUI project '{}'", responseName, + projectElement.getAttribute("name")); + continue; + } + candidateRequests.add(matchingRequest); + } + + if (DispatchStyles.QUERY_MATCH.equals(operation.getDispatcher())) { + // Browse candidates and apply query dispatcher criterion to find corresponding response. + try { + XPathExpression xpath = initializeXPathMatcher(operation); + + Map matchToResponseMap = buildQueryMatchDispatchCriteriaToResponseMap( + mockOperation); + for (Element candidateRequest : candidateRequests) { + // Evaluate matcher against request and get name of corresponding response. + String requestContent = getConfigUniqueDirectChild(candidateRequest, REQUEST_TAG) + .getTextContent(); + String dispatchCriteria = xpath + .evaluate(new InputSource(new StringReader(requestContent))); + String correspondingResponse = matchToResponseMap.get(dispatchCriteria); + + Element matchingResponse = getMockResponseByName(mockOperation, correspondingResponse); + if (matchingResponse != null) { + // Build response from MockResponse and response from matching one. + Response response = buildResponse(matchingResponse, dispatchCriteria); + Request request = buildRequest(candidateRequest); + result.put(request, response); + } + } + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + } else if (DispatchStyles.SCRIPT.equals(operation.getDispatcher())) { + for (Element candidateRequest : candidateRequests) { + Element mockResponse = getMockResponseByName(mockOperation, + candidateRequest.getAttribute(NAME_ATTRIBUTE)); + if (mockResponse == null + && candidateRequest.getAttribute(NAME_ATTRIBUTE).contains("Request")) { + mockResponse = getMockResponseByName(mockOperation, + candidateRequest.getAttribute(NAME_ATTRIBUTE).replace(" Request", " Response")); + } + + if (mockResponse == null) { + log.warn("No response found for request {} into SoapUI project {}", + candidateRequest.getAttribute(NAME_ATTRIBUTE), + projectElement.getAttribute(NAME_ATTRIBUTE)); + continue; + } + + // Build response from MockResponse and response from matching one. + Response response = buildResponse(mockResponse, mockResponse.getAttribute(NAME_ATTRIBUTE)); + Request request = buildRequest(candidateRequest); + result.put(request, response); + } + } else if (DispatchStyles.RANDOM.equals(operation.getDispatcher())) { + if (availableRequests.isEmpty()) { + log.warn( + "A request is mandatory even for a RANDOM dispatch. Operation {} into SoapUI project {}", + operation.getName(), projectElement.getAttribute(NAME_ATTRIBUTE)); + } else { + // Use the first one for all the responses + Element mockRequest = availableRequests.values().iterator().next(); + + for (Element mockResponse : mockResponses) { + // Build response from MockResponse and response from matching one. + Response response = buildResponse(mockResponse, DispatchStyles.RANDOM); + Request request = buildRequest(mockRequest); + request.setName(operation.getName()); + result.put(request, response); + } + } + } + break; + } + } + break; + } + } + } catch (Throwable t) { + throw new MockRepositoryImportException(t.getMessage()); + } + + // Adapt map to list of Exchanges. + return result.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private List getRestMessageDefinitions(Service service, Operation operation) + throws MockRepositoryImportException { + Map result = new HashMap<>(); + try { + List mockServices = getConfigDirectChildren(projectElement, REST_MOCK_SERVICE_TAG); + for (Element mockService : mockServices) { + // Find the appropriate mock service. + if (service.getName().equals(mockService.getAttribute(NAME_ATTRIBUTE))) { + + List mockOperations = getConfigDirectChildren(mockService, REST_MOCK_ACTION_TAG); + for (Element mockOperation : mockOperations) { + // Find the appropriate mock service operation. + if (operation.getName().equals(mockOperation.getAttribute(NAME_ATTRIBUTE))) { + + // Collect available test requests for this operation. + Map availableRequests = collectTestStepsRestRequests(operation); + // Collection also mock responses that are sparsly dispatched under mockRestActions having the name same. + List mockResponses = getMockRestResponses(mockService, operation); + + // Then filter only those that are matching with a mock response. + Map requestToResponses = new HashMap<>(); + + for (Element mockResponse : mockResponses) { + String responseName = mockResponse.getAttribute(NAME_ATTRIBUTE); + Element matchingRequest = availableRequests.get(responseName); + if (matchingRequest == null) { + matchingRequest = availableRequests.get(responseName + " Request"); + } + if (matchingRequest == null && responseName.contains("Response")) { + matchingRequest = availableRequests.get(responseName.replace("Response", "Request")); + } + + if (matchingRequest == null) { + log.warn("No request found for response '{}' into SoapUI project '{}'", responseName, + projectElement.getAttribute("name")); + continue; + } + requestToResponses.put(matchingRequest, mockResponse); + } + + for (Map.Entry entry : requestToResponses.entrySet()) { + String dispatchCriteria = null; + + if (DispatchStyles.SEQUENCE.equals(operation.getDispatcher())) { + // Build a dispatch criteria from operation name projected onto resourcePath pattern + // eg. /deployment/byComponent/{component}/{version}.json => /deployment/byComponent/testREST/1.2.json + // for producing /component=testREST/version=1.2 + // resourcePath is actually available in restMockAction wrapping response. Navigate to parent. + String resourcePath = ((Element) entry.getValue().getParentNode()) + .getAttribute("resourcePath"); + dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern( + operation.getDispatcherRules(), operation.getName(), resourcePath); + } else if (DispatchStyles.SCRIPT.equals(operation.getDispatcher())) { + // Build a dispatch criteria that is equal to response name (that script evaluation should return...) + dispatchCriteria = entry.getValue().getAttribute(NAME_ATTRIBUTE); + } + + Response response = buildResponse(entry.getValue(), dispatchCriteria); + Request request = buildRequest(entry.getKey()); + result.put(request, response); + } + break; + } + } + } + } + } catch (Throwable t) { + throw new MockRepositoryImportException(t.getMessage()); + } + + // Adapt map to list of Exchanges. + return result.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private String extractNSFromQName(String qName) { + if (qName.startsWith("{") && qName.indexOf("}") > 1) { + return qName.substring(1, qName.indexOf("}")); + } + return qName; + } + + /** Extract version from mockService properties. */ + private String extractVersion(Element properties) throws MockRepositoryImportException, MalformedXmlException { + List propertyList = getConfigDirectChildren(properties, "property"); + for (Element property : propertyList) { + Element propertyName = getConfigUniqueDirectChild(property, NAME_ATTRIBUTE); + Element propertyValue = getConfigUniqueDirectChild(property, VALUE_TAG); + if (SERVICE_VERSION_PROPERTY.equals(propertyName.getTextContent())) { + return propertyValue.getTextContent(); + } + } + log.error("Version property is missing in Project properties"); + throw new MockRepositoryImportException("Version property is missing in Project properties"); + } + + private Element getInterfaceOperation(Element serviceInterface, String operationName) + throws MockRepositoryImportException { + List operations = getConfigDirectChildren(serviceInterface, "operation"); + for (Element operation : operations) { + if (operationName.equals(operation.getAttribute(NAME_ATTRIBUTE))) { + return operation; + } + } + log.error("Operation {} is missing into Service interface", operationName); + throw new MockRepositoryImportException("Operation " + operationName + " is missing into Service interface"); + } + + private Map collectTestStepsRequests(Operation operation) throws MalformedXmlException { + Map results = new HashMap<>(); + + List testSuites = getConfigDirectChildren(projectElement, "testSuite"); + for (Element testSuite : testSuites) { + List testCases = getConfigDirectChildren(testSuite, "testCase"); + for (Element testCase : testCases) { + List testSteps = getConfigDirectChildren(testCase, "testStep"); + for (Element testStep : testSteps) { + + Element config = getConfigUniqueDirectChild(testStep, "config"); + String interfaceName = getConfigUniqueDirectChild(config, "interface").getTextContent(); + String operationName = getConfigUniqueDirectChild(config, "operation").getTextContent(); + + if (operation.getName().equals(operationName)) { + results.put(testStep.getAttribute(NAME_ATTRIBUTE), getConfigUniqueDirectChild(config, REQUEST_TAG)); + } + } + } + } + return results; + } + + private Map collectTestStepsRestRequests(Operation operation) throws MalformedXmlException { + Map results = new HashMap<>(); + + List testSuites = getConfigDirectChildren(projectElement, "testSuite"); + for (Element testSuite : testSuites) { + List testCases = getConfigDirectChildren(testSuite, "testCase"); + for (Element testCase : testCases) { + List testSteps = getConfigDirectChildren(testCase, "testStep"); + for (Element testStep : testSteps) { + Element config = getConfigUniqueDirectChild(testStep, "config"); + String operationName = config.getAttribute("resourcePath"); + if (operation.getName().equals(operationName)) { + results.put(testStep.getAttribute(NAME_ATTRIBUTE), getConfigUniqueDirectChild(config, "restRequest")); + } else { + // If this artifact is second one and OpenAPI was the main one, operationName may + // start with the verb. We have to extract the verb from elsewhere. + String methodName = config.getAttribute("methodName"); + Optional method = getRestInterfaceResourceMethod(operationName, methodName); + if (method.isPresent()) { + String methodString = method.get().getAttribute("method"); + if (operation.getName().equals(methodString.toUpperCase() + " " + operationName)) { + results.put(testStep.getAttribute(NAME_ATTRIBUTE), + getConfigUniqueDirectChild(config, "restRequest")); + } + } + } + } + } + } + return results; + } + + private Optional getRestInterfaceResourceMethod(String resourcePath, String methodName) + throws MalformedXmlException { + return getConfigDirectChildren(serviceInterface, "resource").stream() + .filter(resource -> resourcePath.equals(resource.getAttribute("path"))) + .flatMap(resource -> getConfigDirectChildren(resource, "method").stream()) + .filter(method -> methodName.equals(method.getAttribute("name"))).findFirst(); + } + + private List getMockRestResponses(Element restMockService, Operation operation) { + List responses = new ArrayList<>(); + List mockActions = getConfigDirectChildren(restMockService, REST_MOCK_ACTION_TAG); + for (Element mockAction : mockActions) { + if (operation.getName().equals(mockAction.getAttribute(NAME_ATTRIBUTE))) { + responses.addAll(getConfigDirectChildren(mockAction, RESPONSE_TAG)); + } + } + return responses; + } + + /** Build a XPathMatcher based on operation dispatcher rules. */ + private XPathExpression initializeXPathMatcher(Operation operation) throws XPathExpressionException { + return SoapUIXPathBuilder.buildXPathMatcherFromRules(operation.getDispatcherRules()); + } + + private Map buildQueryMatchDispatchCriteriaToResponseMap(Element mockOperation) + throws MalformedXmlException { + Map matchResponseMap = new HashMap<>(); + + String dispatcher = getConfigUniqueDirectChild(mockOperation, "dispatchStyle").getTextContent(); + if (DispatchStyles.QUERY_MATCH.equals(dispatcher)) { + Element dispatchConfig = getConfigUniqueDirectChild(mockOperation, "dispatchConfig"); + + List queries = SoapUIProjectParserUtils.getConfigDirectChildren(dispatchConfig, QUERY_TAG); + for (Element query : queries) { + String match = getConfigUniqueDirectChild(query, "match").getTextContent(); + String response = getConfigUniqueDirectChild(query, RESPONSE_TAG).getTextContent(); + matchResponseMap.put(match, response); + } + } + return matchResponseMap; + } + + private Element getMockResponseByName(Element mockOperation, String responseName) { + List responses = getConfigDirectChildren(mockOperation, RESPONSE_TAG); + for (Element response : responses) { + if (responseName.equals(response.getAttribute(NAME_ATTRIBUTE))) { + return response; + } + } + return null; + } + + private Response buildResponse(Element mockResponse, String dispatchCriteria) + throws MockRepositoryImportException, Exception { + Response response = new Response(); + response.setName(mockResponse.getAttribute(NAME_ATTRIBUTE)); + response.setContent(getConfigUniqueDirectChild(mockResponse, "responseContent").getTextContent()); + response.setHeaders(buildHeaders(mockResponse)); + response.setDispatchCriteria(dispatchCriteria); + response.setStatus(mockResponse.getAttribute("httpResponseStatus")); + if ("500".equals(response.getStatus())) { + response.setFault(true); + } + response.setMediaType(mockResponse.getAttribute("mediaType")); + return response; + } + + private Request buildRequest(Element testRequest) throws MockRepositoryImportException, Exception { + Request request = new Request(); + request.setName(testRequest.getAttribute(NAME_ATTRIBUTE)); + if (hasConfigDirectChild(testRequest, REQUEST_TAG)) { + request.setContent(getConfigUniqueDirectChild(testRequest, REQUEST_TAG).getTextContent()); + } + request.setHeaders(buildHeaders(testRequest)); + + // Add query parameters only if presents. + if (hasConfigDirectChild(testRequest, "parameters")) { + List entries = getConfigDirectChildren(getConfigUniqueDirectChild(testRequest, "parameters"), + "entry"); + for (Element entry : entries) { + Parameter param = new Parameter(); + param.setName(entry.getAttribute("key")); + param.setValue(entry.getAttribute(VALUE_TAG)); + request.addQueryParameter(param); + } + } + return request; + } + + private Set
buildHeaders(Element requestOrResponse) throws Exception { + if (hasConfigDirectChild(requestOrResponse, "settings")) { + Element settingsElt = getConfigUniqueDirectChild(requestOrResponse, "settings"); + List settings = getConfigDirectChildren(settingsElt, "setting"); + for (Element setting : settings) { + if (setting.getAttribute("id").contains("@request-headers")) { + + // 2 possibilities here: + // 1) <xml-fragment xmlns:con="http://eviware.com/soapui/config"> + // <con:entry key="Authorization" value="Basic YWRtaW46YWRtaW4="/> + // <con:entry key="x-userid" value="s026210"/> + // </xml-fragment> + // 2) <entry key="toto" + // value="tata" xmlns="http://eviware.com/soapui/config"/> + String headersContent = setting.getTextContent(); + Element fragment = documentBuilder.parse(new InputSource(new StringReader(headersContent))) + .getDocumentElement(); + + Set
headers = new HashSet<>(); + if (headersContent.contains("xml-fragment")) { + List entries = getConfigDirectChildren(fragment, "entry"); + for (Element entry : entries) { + Header header = new Header(); + header.setName(entry.getAttribute("key")); + header.setValues(Set.of(entry.getAttribute(VALUE_TAG))); + headers.add(header); + } + } else { + Header header = new Header(); + header.setName(fragment.getAttribute("key")); + header.setValues(Set.of(fragment.getAttribute(VALUE_TAG))); + headers.add(header); + } + return headers; + } + } + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIProjectParserUtils.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIProjectParserUtils.java new file mode 100644 index 000000000..43c9076ba --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIProjectParserUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui; + +import io.github.microcks.util.MalformedXmlException; +import io.github.microcks.util.XmlUtil; + +import org.w3c.dom.Element; +import java.util.List; + +/** + * Helper class for parsing SoapUI project files. + * @author laurent + */ +public class SoapUIProjectParserUtils { + + /** The SoapUI namespace for configuration found in project files. */ + private static final String SOAPUI_CONFIG_NS = "http://eviware.com/soapui/config"; + + /** + * Retrieve direct children elements of a parent in SoapUI config and tag. Only includes level 1 children. + * @param parent The parent of children to find + * @param tag The tag of children + * @return Children Elements as a list + */ + public static List getConfigDirectChildren(Element parent, String tag) { + return XmlUtil.getDirectChildren(parent, SOAPUI_CONFIG_NS, tag); + } + + /** + * Retrieve a direct child that is expected to be unique under the parent. Throws a MalformedXmlException if no child + * or more than one child present. + * @param parent The parent of child to find + * @param tag The tag of child + * @return The child Element + * @throws MalformedXmlException if no child or more than one child present. + */ + public static Element getConfigUniqueDirectChild(Element parent, String tag) throws MalformedXmlException { + return XmlUtil.getUniqueDirectChild(parent, SOAPUI_CONFIG_NS, tag); + } + + /** + * Check if parent has at least one direct child having namespace and tag. + * @param parent The parent of children to find + * @param tag The tag of children + * @return true if at least one child is present, false otherwise + */ + public static boolean hasConfigDirectChild(Element parent, String tag) { + return XmlUtil.hasDirectChild(parent, SOAPUI_CONFIG_NS, tag); + } + + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIXPathBuilder.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIXPathBuilder.java new file mode 100644 index 000000000..26fb2d5d4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/SoapUIXPathBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui; + +import io.github.microcks.util.WritableNamespaceContext; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +/** + * Builder from creating XPathExpression matcher from SoapUI rules. + * @author laurent + */ +public class SoapUIXPathBuilder { + + /** + * Build a XPath expressions matcher from SoapUI Rules. + * @param rules The string representing the rules. + * @return An XPathExpression following the given rules + * @throws XPathExpressionException if something wrong occurs. + */ + public static XPathExpression buildXPathMatcherFromRules(String rules) throws XPathExpressionException { + XPath xpath = XPathFactory.newInstance().newXPath(); + WritableNamespaceContext nsContext = new WritableNamespaceContext(); + + // Parse SoapUI rules for getting namespaces and expression to evaluate. + // declare namespace ser='http://www.example.com/test/service'; + // //ser:sayHello/name + String xpathExpression = null; + String lines[] = rules.split("\\r?\\n"); + for (String line : lines) { + line = line.trim(); + if (line.startsWith("declare namespace ")) { + String prefix = line.substring(18, line.indexOf("=")); + String namespace = line.substring(line.indexOf("=") + 2, line.lastIndexOf("'")); + nsContext.addNamespaceURI(prefix, namespace); + } else { + xpathExpression = line; + } + } + + // Set namespace context and compile expression. + xpath.setNamespaceContext(nsContext); + return xpath.compile(xpathExpression); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/XmlHolder.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/XmlHolder.java new file mode 100644 index 000000000..9c44cbe4a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/XmlHolder.java @@ -0,0 +1,264 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui; + +import io.github.microcks.util.WritableNamespaceContext; + +import javax.xml.xpath.XPathExpressionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A minimalist implementation of com.eviware.soapui.support.XmlHolder to ensure a compatibility layer withe SoapUI + * scripting. + * @author laurent + */ +public class XmlHolder implements Map { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(XmlHolder.class); + + private Element xmlObject; + + private Map declaredNamespaces; + + private XPath xpath; + + /** + * Build a new XmlHolder from xml string. + * @param xml String representation of Xml managed by this holder. + * @throws Exception + */ + public XmlHolder(String xml) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newDefaultInstance(); + factory.setNamespaceAware(true); + DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + xmlObject = documentBuilder.parse(new InputSource(new StringReader(xml))).getDocumentElement(); + xpath = XPathFactory.newInstance().newXPath(); + updateXPathNamespaces(readXPathNamespaces()); + } + + private void updateXPathNamespaces(Map declaredNamespaces) { + WritableNamespaceContext nsContext = new WritableNamespaceContext(); + for (Entry entry : declaredNamespaces.entrySet()) { + nsContext.addNamespaceURI(entry.getKey(), entry.getValue()); + } + xpath.setNamespaceContext(nsContext); + } + + private Map readXPathNamespaces() { + Map namespaces = new HashMap<>(); + addNamespacesToMap(xmlObject, namespaces); + return namespaces; + } + + private static void addNamespacesToMap(Node node, Map namespaces) { + NamedNodeMap attributes = node.getAttributes(); + if (attributes != null) { + for (int i = 0; i < attributes.getLength(); i++) { + Node attribute = attributes.item(i); + if (attribute.getNodeType() == Node.ATTRIBUTE_NODE && attribute.getNamespaceURI() != null) { + namespaces.put(getLocalPart(attribute.getNodeName()), attribute.getNodeValue()); + } + } + } + + NodeList child = node.getChildNodes(); + for (int i = 0; i < child.getLength(); i++) { + addNamespacesToMap(child.item(i), namespaces); + } + } + + private static String getLocalPart(String localPart) { + localPart = localPart.substring(localPart.indexOf(":") + 1); + return localPart; + } + + private XPathExpression compileXPath(String xpathExpression) throws XPathExpressionException { + if (xpathExpression.trim().startsWith("declare namespace")) { + return SoapUIXPathBuilder.buildXPathMatcherFromRules(xpathExpression); + } + return xpath.compile(xpathExpression); + } + + /** + * Get a string representation of managed Xml. + * @return Xml string + * @throws Exception if Xml cannot be transformed back to string. + */ + public String getXml() throws Exception { + TransformerFactory transformerFactory = TransformerFactory.newDefaultInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + + Writer out = new StringWriter(); + transformer.transform(new DOMSource(xmlObject), new StreamResult(out)); + return out.toString(); + } + + /** + * Get the managed namespaces. + * @return A map of namespaces prefix + URI + */ + public Map getNamespaces() { + if (declaredNamespaces == null) { + declaredNamespaces = new HashMap<>(); + } + return declaredNamespaces; + } + + /** + * Declare a new namespace to manage. + * @param prefix Prefix for the namespace + * @param uri URI of this namespqce + */ + public void declareNamespace(String prefix, String uri) { + if (declaredNamespaces == null) { + declaredNamespaces = new HashMap<>(); + } + declaredNamespaces.put(prefix, uri); + updateXPathNamespaces(declaredNamespaces); + } + + /** + * Get the value of a node given an XPath expression + * @param xpathExpression XPath expression + * @return The single value + * @throws Exception if expression is not correct + */ + public String getNodeValue(String xpathExpression) throws Exception { + XPathExpression expression = compileXPath(xpathExpression); + return expression.evaluate(xmlObject); + } + + /** + * Get the values of a many nodes given an XPath expression + * @param xpathExpression XPath expression + * @return The values of matching nodes + * @throws Exception if expression is not correct + */ + public String[] getNodeValues(String xpathExpression) throws Exception { + XPathExpression expression = compileXPath(xpathExpression); + NodeList nodeList = (NodeList) expression.evaluate(xmlObject, XPathConstants.NODESET); + String[] results = new String[nodeList.getLength()]; + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + results[i] = node.getTextContent(); + } + return results; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean containsKey(Object key) { + return false; + } + + @Override + public boolean containsValue(Object value) { + return false; + } + + @Override + public Object get(Object key) { + String str = key.toString(); + try { + if (str.equals("xml") || str.equals("prettyXml")) { + return getXml(); + } + if (str.equals("namespaces")) { + return getNamespaces(); + } else { + // Assume it's an XPath expression. + String[] nodeValues = this.getNodeValues(str); + return nodeValues != null && nodeValues.length == 1 ? nodeValues[0] : nodeValues; + } + } catch (Exception e) { + log.error("Exception while getting key {}", key, e); + } + return null; + } + + @Override + public Object put(String key, Object value) { + return null; + } + + @Override + public Object remove(Object key) { + return null; + } + + @Override + public void putAll(Map m) { + for (Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + } + + @Override + public Set keySet() { + return null; + } + + @Override + public Collection values() { + return null; + } + + @Override + public Set> entrySet() { + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/AssertionFactory.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/AssertionFactory.java new file mode 100644 index 000000000..be2b28131 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/AssertionFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import java.util.Map; + +/** + * Helper class to build and retrieve assertions using the Soap UI type for this assertion. + * @author laurent + */ +public class AssertionFactory { + + public static final String RESPONSE_SLA_ASSERTION = "Response SLA Assertion"; + public static final String VALID_HTTP_STATUS_CODES = "Valid HTTP Status Codes"; + public static final String SOAP_FAULT_ASSERTION = "Not SOAP Fault Assertion"; + public static final String NOT_SOAP_FAULT_ASSERTION = "SOAP Fault Assertion"; + public static final String SOAP_RESPONSE = "SOAP Response"; + public static final String SCHEMA_COMPLIANCE = "Schema Compliance"; + public static final String XPATH_CONTAINS = "XPath Match"; + public static final String JSONPATH_MATCH = "JsonPath Match"; + public static final String SIMPLE_CONTAINS = "Simple Contains"; + public static final String SIMPLE_NOT_CONTAINS = "Simple NotContains"; + + + private AssertionFactory() { + // Hide default constructor as it's a utility class. + } + + /** + * Build and configuration a new assertion for the given type. + * @param type The type of the assertion (see string constants for available values.) + * @param configParams The configuration parameters for this assertion + * @return A ready-to-use assertion + */ + public static SoapUIAssertion intializeAssertion(String type, Map configParams) { + SoapUIAssertion assertion = null; + + // Depending on type, initialize the correct assertion implementation. + switch (type) { + case RESPONSE_SLA_ASSERTION -> assertion = new SLAAssertion(); + case VALID_HTTP_STATUS_CODES -> assertion = new ValidHttpCodesAssertion(); + case SOAP_FAULT_ASSERTION -> assertion = new SoapFaultAssertion(); + case NOT_SOAP_FAULT_ASSERTION -> assertion = new NotSoapFaultAssertion(); + case SOAP_RESPONSE -> assertion = new SoapResponseAssertion(); + case SCHEMA_COMPLIANCE -> assertion = new SchemaConformanceAssertion(); + case XPATH_CONTAINS -> assertion = new XPathContainsAssertion(); + case JSONPATH_MATCH -> assertion = new JsonPathContentAssertion(); + case SIMPLE_CONTAINS -> assertion = new SimpleContainsAssertion(); + case SIMPLE_NOT_CONTAINS -> assertion = new SimpleNotContainsAssertion(); + default -> assertion = new UnknownAssertion(type); + } + + assertion.configure(configParams); + return assertion; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/AssertionStatus.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/AssertionStatus.java new file mode 100644 index 000000000..ea5f6d543 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/AssertionStatus.java @@ -0,0 +1,26 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +/** + * Possible status of assertion results. + * @author laurent + */ +public enum AssertionStatus { + UNKNOWN, + VALID, + FAILED; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/ExchangeContext.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/ExchangeContext.java new file mode 100644 index 000000000..a97b85209 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/ExchangeContext.java @@ -0,0 +1,33 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Service; + +import java.util.List; + +/** + * Simple record wrapping elements of an exchange context during a test + * @param service The Service the test was issued for + * @param operation The Operation the test was issued for + * @param resources The Resources attached to Service + * @param resourceUrl A base resource url to fetch relative dependencies that may be found in resources + */ +public record ExchangeContext(Service service, Operation operation, List resources, String resourceUrl) { +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/JsonPathContentAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/JsonPathContentAssertion.java new file mode 100644 index 000000000..d06498468 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/JsonPathContentAssertion.java @@ -0,0 +1,104 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.StringReader; +import java.util.List; +import java.util.Map; + +/** + * An assertion that uses a Json path expression to extract content and check it's matching expected content. + * @author laurent + */ +public class JsonPathContentAssertion extends WildcardMatchingAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(JsonPathContentAssertion.class); + + /** The JsonPath expression. Value is a string with a valid JsonPath expression. */ + public static final String PATH_PARAM = "path"; + + /** The expected content param. Value is a string that may contain wildcards if `allowWildcards` is set to true. */ + public static final String EXPECTED_CONTENT_PARAM = "content"; + + + private String path; + + private String expectedContent; + + private String errorMessage; + + + @Override + public void configure(Map configParams) { + super.configure(configParams); + path = configParams.get(PATH_PARAM); + expectedContent = configParams.get(EXPECTED_CONTENT_PARAM); + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting JsonPath on {}, expecting: {}", path, expectedContent); + + if (path == null) { + errorMessage = "Missing path for JsonPath assertion"; + return AssertionStatus.FAILED; + } + if (expectedContent == null) { + errorMessage = "Missing content for JsonPath assertion"; + return AssertionStatus.FAILED; + } + + try { + // Parse json text ang get root node. + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(new StringReader(exchange.responseContent())); + + // Retrieve evaluated node within JSON tree. + JsonNode evaluatedNode = rootNode.at(path); + String result = evaluatedNode.asText(); + + if (allowWildcards) { + if (!isSimilar(expectedContent, result)) { + errorMessage = "Comparison failed for path [" + path + "], expecting [" + expectedContent + + "], actual was [" + result + "]"; + return AssertionStatus.FAILED; + } + } else { + if (!expectedContent.equals(result)) { + errorMessage = "Comparison failed for path [" + path + "], expecting [" + expectedContent + + "], actual was [" + result + "]"; + return AssertionStatus.FAILED; + } + } + } catch (Exception e) { + log.warn("Exception while compiling/evaluating JsonPointer", e); + errorMessage = "Exception while compiling/evaluating JoinPointer: " + path; + return AssertionStatus.FAILED; + } + return AssertionStatus.VALID; + } + + @Override + public List getErrorMessages() { + return List.of(errorMessage); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/NotSoapFaultAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/NotSoapFaultAssertion.java new file mode 100644 index 000000000..c60637394 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/NotSoapFaultAssertion.java @@ -0,0 +1,67 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An assertion that checks if response is not Soap Fault. It fails if it's a Soap Fault. + * @author laurent + */ +public class NotSoapFaultAssertion implements SoapUIAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(NotSoapFaultAssertion.class); + + /** Regular expression pattern for capturing Soap Fault name from body. */ + private static final Pattern FAULT_CAPTURE_PATTERN = Pattern.compile("(.*):Body>(\\s*)<((\\w+):|)Fault(.*)(/)?>(.*)", + Pattern.DOTALL); + + private String errorMessage; + + @Override + public void configure(Map configParams) { + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting response is not a SOAP Fault"); + + String responseContent = exchange.responseContent(); + // First check presence of Fault tag in content. + if (responseContent.contains(":Fault") || responseContent.contains(" getErrorMessages() { + return List.of(errorMessage); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/RequestResponseExchange.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/RequestResponseExchange.java new file mode 100644 index 000000000..521339d29 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/RequestResponseExchange.java @@ -0,0 +1,31 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import io.github.microcks.domain.Request; + +import org.springframework.http.client.ClientHttpResponse; + +/** + * Simple record for wrapping exchange elements of a test. + * @param request The request that was issued to tested endpoint + * @param response The response from Http client + * @param responseContent The body of the response + * @param duration The duration of the exchange + */ +public record RequestResponseExchange(Request request, ClientHttpResponse response, String responseContent, + long duration) { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SLAAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SLAAssertion.java new file mode 100644 index 000000000..cb37198b4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SLAAssertion.java @@ -0,0 +1,66 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * An assertion that checks if duration of an exchange is under given SLA. + * @author laurent + */ +public class SLAAssertion implements SoapUIAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SLAAssertion.class); + + /** The SLA parameter of this assertion. Value is expected be a long integer string representation. */ + public static final String SLA_PARAM = "SLA"; + + private long sla = 200L; + + private String errorMessage; + + @Override + public void configure(Map configParams) { + if (configParams.containsKey(SLA_PARAM)) { + try { + sla = Long.valueOf(configParams.get(SLA_PARAM)); + } catch (NumberFormatException nfe) { + log.warn("SLA config parameters cannot be cast to long, using 200ms as a default"); + } + } + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting response SLA of {}, actual duration: {}", sla, exchange.duration()); + + if (exchange.duration() > sla) { + errorMessage = "Response did not meet SLA " + exchange.duration() + "/" + sla; + return AssertionStatus.FAILED; + } + return AssertionStatus.VALID; + } + + @Override + public List getErrorMessages() { + return List.of(errorMessage); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SchemaConformanceAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SchemaConformanceAssertion.java new file mode 100644 index 000000000..bb21ce7d9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SchemaConformanceAssertion.java @@ -0,0 +1,98 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.XmlSchemaValidator; +import io.github.microcks.util.soap.SoapMessageValidator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.xml.namespace.QName; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * Assertion that checks if response content is actually conformance to a Schema definition. + * @author laurent + */ +public class SchemaConformanceAssertion implements SoapUIAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SchemaConformanceAssertion.class); + + /** The definition parameter to embed a schema definition as a string. */ + public static final String DEFINITION_PARAM = "definition"; + + private String definition; + + private List errorMessages; + + @Override + public void configure(Map configParams) { + if (configParams.containsKey(DEFINITION_PARAM)) { + definition = configParams.get(DEFINITION_PARAM); + } + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + + if (context.service().getType() == ServiceType.SOAP_HTTP) { + log.debug("Asserting Soap response is valid against WSDL"); + + // Validate against Soap message against WSDL. + Resource wsdl = context.resources().stream().filter(r -> r.getType() == ResourceType.WSDL).findFirst().get(); + QName partQName = new QName(context.service().getXmlNS(), context.operation().getOutputName()); + errorMessages = SoapMessageValidator.validateSoapMessage(wsdl.getContent(), partQName, + exchange.responseContent(), context.resourceUrl()); + if (!errorMessages.isEmpty()) { + log.debug("Soap response is not valid: {} errors", errorMessages.size()); + return AssertionStatus.FAILED; + } + } else if (definition != null) { + // Only managed Xml validation at the moment. + if (exchange.response().getHeaders().getConnection() != null + && exchange.response().getHeaders().getConnection().contains("xml")) { + log.debug("Asserting Xml response is valid against local definition"); + + try { + errorMessages = XmlSchemaValidator.validateXml( + new ByteArrayInputStream(definition.getBytes(StandardCharsets.UTF_8)), exchange.responseContent(), + context.resourceUrl()); + } catch (Exception e) { + log.warn("Xml schema validation failed with: {}", e.getMessage()); + errorMessages.add("Xml schema validation failed with: " + e.getMessage()); + } + if (!errorMessages.isEmpty()) { + log.debug("Xml response is not valid according definition: {} errors", errorMessages.size()); + return AssertionStatus.FAILED; + } + } + } + return AssertionStatus.VALID; + } + + @Override + public List getErrorMessages() { + return errorMessages; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SimpleContainsAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SimpleContainsAssertion.java new file mode 100644 index 000000000..7a70e8dec --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SimpleContainsAssertion.java @@ -0,0 +1,111 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import io.micrometer.common.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple assertion that checks for a specified token in response content. + * @author laurent + */ +public class SimpleContainsAssertion implements SoapUIAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SimpleContainsAssertion.class); + + /** Expected token configuration parameter. */ + public static final String TOKEN_PARAM = "token"; + + /** + * Ignore case configuration parameter. Value is expected to be parsed as boolean: "true" or "false". Default is + * false. + */ + public static final String IGNORE_CASE_PARAM = "ignoreCase"; + + /** + * Consider token parameter as regular expression? Value is expected to be parsed as boolean: "true" or "false". + * Default is false. + */ + public static final String USE_REGEX_PARAM = "useRegEx"; + + + protected String token; + + protected boolean ignoreCase = false; + + protected boolean useRegEx = false; + + protected String errorMessage; + + + @Override + public void configure(Map configParams) { + token = configParams.get(TOKEN_PARAM); + if (configParams.containsKey(IGNORE_CASE_PARAM)) { + ignoreCase = Boolean.valueOf(configParams.get(IGNORE_CASE_PARAM)); + } + if (configParams.containsKey(USE_REGEX_PARAM)) { + useRegEx = Boolean.valueOf(configParams.get(USE_REGEX_PARAM)); + } + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting Simple contains for {}", token); + if (token == null) { + token = ""; + } + + String content = normalize(exchange.responseContent()); + + int indexOfToken = -1; + if (useRegEx) { + String tokenToUse = ignoreCase ? "(?i)" + token : token; + Pattern p = Pattern.compile(tokenToUse, Pattern.DOTALL); + Matcher m = p.matcher(content); + if (m.find()) { + indexOfToken = 0; + } + } else { + indexOfToken = ignoreCase ? content.toUpperCase().indexOf(token.toUpperCase()) : content.indexOf(token); + } + if (indexOfToken == -1) { + errorMessage = "Missing token [" + token + "] in Response"; + return AssertionStatus.FAILED; + } + + return AssertionStatus.VALID; + } + + @Override + public List getErrorMessages() { + return List.of(errorMessage); + } + + private String normalize(String string) { + if (StringUtils.isNotEmpty(string)) { + string = string.replace("\r\n", "\n"); + } + return string; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SimpleNotContainsAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SimpleNotContainsAssertion.java new file mode 100644 index 000000000..56223ba28 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SimpleNotContainsAssertion.java @@ -0,0 +1,47 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.soapui.assertions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A simple assertion that checks absence of a specified token in response content. + * @author laurent + */ +public class SimpleNotContainsAssertion extends SimpleContainsAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SimpleNotContainsAssertion.class); + + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting Simple not contains for {}. Apply a 'contains' and then revert result", token); + AssertionStatus containsStatus = super.assertResponse(exchange, context); + if (containsStatus == AssertionStatus.VALID) { + errorMessage = "Response contains token [" + token + "]"; + return AssertionStatus.FAILED; + } + if (containsStatus == AssertionStatus.FAILED) { + errorMessage = null; + return AssertionStatus.VALID; + } + errorMessage = null; + return AssertionStatus.UNKNOWN; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapFaultAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapFaultAssertion.java new file mode 100644 index 000000000..37cf435f4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapFaultAssertion.java @@ -0,0 +1,56 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * An assertion that checks if response is actually a Soap Fault. It fails if not a Soap Fault. + * @author laurent + */ +public class SoapFaultAssertion implements SoapUIAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SoapFaultAssertion.class); + + private String errorMessage; + + @Override + public void configure(Map configParams) { + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting response is a SOAP Fault"); + + String responseContent = exchange.responseContent(); + // First check presence of Fault tag in content. + if (!responseContent.contains(":Fault") && !responseContent.contains(" getErrorMessages() { + return List.of(errorMessage); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapResponseAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapResponseAssertion.java new file mode 100644 index 000000000..5b1ae4798 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapResponseAssertion.java @@ -0,0 +1,56 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import io.github.microcks.util.soap.SoapMessageValidator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * Assertion that checks if response body content is actually a valid Soap Envelope. + * @author laurent + */ +public class SoapResponseAssertion implements SoapUIAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SoapResponseAssertion.class); + + private List errorMessages; + + @Override + public void configure(Map configParams) { + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting response is a SOAP response"); + + errorMessages = SoapMessageValidator.validateSoapEnvelope(exchange.responseContent()); + if (!errorMessages.isEmpty()) { + return AssertionStatus.FAILED; + } + return AssertionStatus.VALID; + } + + @Override + public List getErrorMessages() { + return errorMessages; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapUIAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapUIAssertion.java new file mode 100644 index 000000000..a37c0c2c3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/SoapUIAssertion.java @@ -0,0 +1,46 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import java.util.List; +import java.util.Map; + +/** + * This defines an assertion that may be checked on a test response. + * @author laurent + */ +public interface SoapUIAssertion { + + /** + * Assertion may be configured before using it. + * @param configParams A map of parameters of key:value + */ + void configure(Map configParams); + + /** + * Assert validations on response at the end of an exchange. + * @param exchange The raw data of this exchange (request/response, duration) + * @param context The context of this exchange (service, operation, resources) + * @return The Assertion Status + */ + AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context); + + /** + * If assertion status is failed or unknown, some error messages may be retrieved later one. + * @return A list of error messages if assertion didn't succeed. + */ + List getErrorMessages(); +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/UnknownAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/UnknownAssertion.java new file mode 100644 index 000000000..73ae3ac75 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/UnknownAssertion.java @@ -0,0 +1,47 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import java.util.List; +import java.util.Map; + +/** + * Unknown assertion just fails all the time. Used for un-managed assertion types. + * @author laurent + */ +public class UnknownAssertion implements SoapUIAssertion { + + private String type; + + public UnknownAssertion(String type) { + this.type = type; + } + + @Override + public void configure(Map configParams) { + + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + return AssertionStatus.FAILED; + } + + @Override + public List getErrorMessages() { + return List.of("Assertion '" + type + "' is not managed by Microcks at the moment"); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/ValidHttpCodesAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/ValidHttpCodesAssertion.java new file mode 100644 index 000000000..1caab8c23 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/ValidHttpCodesAssertion.java @@ -0,0 +1,75 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * An assertion that checks if response http code is contained in specified ones. Default code is 200. + * @author laurent + */ +public class ValidHttpCodesAssertion implements SoapUIAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(ValidHttpCodesAssertion.class); + + /** The codes parameter. Value is expected to be a coma separated list of codes. eg: "401,403" */ + public static final String CODES_PARAM = "codes"; + + private String codes = "200,"; + + private String errorMessage; + + @Override + public void configure(Map configParams) { + if (configParams.containsKey(CODES_PARAM)) { + codes = configParams.get(CODES_PARAM); + if (!codes.endsWith(",")) { + codes += ','; + } + } + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting response Http codes in {}", codes); + + String responseCode = null; + try { + responseCode = String.valueOf(exchange.response().getStatusCode().value()); + log.debug("Response status code : " + responseCode); + } catch (IOException ioe) { + log.debug("IOException while getting raw status code in response", ioe); + return AssertionStatus.FAILED; + } + + if (!codes.contains(responseCode + ",")) { + errorMessage = "Response status code:" + responseCode + " is not in acceptable list of status codes"; + return AssertionStatus.FAILED; + } + return AssertionStatus.VALID; + } + + @Override + public List getErrorMessages() { + return List.of(errorMessage); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/WildcardMatchingAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/WildcardMatchingAssertion.java new file mode 100644 index 000000000..d81ed6835 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/WildcardMatchingAssertion.java @@ -0,0 +1,80 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.regex.Pattern; + +/** + * A base class for assertions needing to du fuzzy matching using wildcards. + * @author laurent + */ +public abstract class WildcardMatchingAssertion implements SoapUIAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(WildcardMatchingAssertion.class); + + /** + * Allow wildcards configuration parameter. Value is expected to be parsed as boolean: "true" or "false". Default is + * false. + */ + public static final String ALLOW_WILDCARDS = "allowWildcards"; + + protected boolean allowWildcards = false; + + @Override + public void configure(Map configParams) { + if (configParams.containsKey(ALLOW_WILDCARDS)) { + allowWildcards = Boolean.valueOf(configParams.get(ALLOW_WILDCARDS)); + } + } + + /** + * Managed wildcards specified in expected content to check if real content is similar. + * @param expectedContent Expected content with wildcards + * @param realContent Real content to match + * @return true if matching, false otherwise + */ + protected boolean isSimilar(String expectedContent, String realContent) { + // Build a pattern from expected content. + StringBuilder patternBuilder = new StringBuilder(); + if (expectedContent.startsWith("*")) { + patternBuilder.append(".*"); + } + String[] tokens = expectedContent.split(Pattern.quote("*")); + boolean first = true; + for (int i = 0; i < tokens.length; ++i) { + String token = tokens[i]; + if (!token.isEmpty()) { + if (!first) { + patternBuilder.append(".*"); + } + first = false; + patternBuilder.append(Pattern.quote(token)); + } + } + if (expectedContent.endsWith("*")) { + patternBuilder.append(".*"); + } + + log.debug("Compiled a pattern for wildcard match, will use '{}'", patternBuilder); + + return Pattern.compile(patternBuilder.toString()).matcher(realContent).matches(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/XPathContainsAssertion.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/XPathContainsAssertion.java new file mode 100644 index 000000000..fe4b5b1c8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/soapui/assertions/XPathContainsAssertion.java @@ -0,0 +1,102 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import io.github.microcks.util.soapui.SoapUIXPathBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.InputSource; + +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import java.io.StringReader; +import java.util.List; +import java.util.Map; + +/** + * An assertion that uses a XPath path expression to extract content and check it's matching expected content. + * @author laurent + */ +public class XPathContainsAssertion extends WildcardMatchingAssertion { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(XPathContainsAssertion.class); + + /** The XPath expression. Value is a string with a valid XPath expression. */ + public static final String PATH_PARAM = "path"; + + /** The expected content param. Value is a string that may contain wildcards if `allowWildcards` is set to true. */ + public static final String EXPECTED_CONTENT_PARAM = "content"; + + + private String path; + + private String expectedContent; + + private String errorMessage; + + + @Override + public void configure(Map configParams) { + super.configure(configParams); + path = configParams.get(PATH_PARAM); + expectedContent = configParams.get(EXPECTED_CONTENT_PARAM); + } + + @Override + public AssertionStatus assertResponse(RequestResponseExchange exchange, ExchangeContext context) { + log.debug("Asserting XPath on {}, expecting: {}", path, expectedContent); + + if (path == null) { + errorMessage = "Missing path for XPath assertion"; + return AssertionStatus.FAILED; + } + if (expectedContent == null) { + errorMessage = "Missing content for XPath assertion"; + return AssertionStatus.FAILED; + } + + try { + XPathExpression expression = SoapUIXPathBuilder.buildXPathMatcherFromRules(path); + String result = expression.evaluate(new InputSource(new StringReader(exchange.responseContent()))); + + if (allowWildcards) { + if (!isSimilar(expectedContent, result)) { + errorMessage = "XPathContains comparison failed for path [" + path + "], expecting [" + expectedContent + + "], actual was [" + result + "]"; + return AssertionStatus.FAILED; + } + } else { + if (!expectedContent.equals(result)) { + errorMessage = "XPathContains comparison failed for path [" + path + "], expecting [" + expectedContent + + "], actual was [" + result + "]"; + return AssertionStatus.FAILED; + } + } + } catch (XPathExpressionException e) { + log.warn("Exception while compiling/evaluating XPath", e); + errorMessage = "Exception while compiling/evaluating XPath: " + path; + return AssertionStatus.FAILED; + } + return AssertionStatus.VALID; + } + + @Override + public List getErrorMessages() { + return List.of(errorMessage); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/AbstractTestRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/AbstractTestRunner.java new file mode 100644 index 000000000..347927b66 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/AbstractTestRunner.java @@ -0,0 +1,76 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.test; + +import io.github.microcks.domain.*; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * A runner responsible for running test on a given service endpoint. The tests consists in sending a bunch of reference + * requests to the endpoint and extract informations from acquired responses (or communication failures !) in order to + * determine if test has passed or not. + * @author laurent + */ +public abstract class AbstractTestRunner { + + /** + * Run a test for a specified service and operation. + * @param service The service under test + * @param operation The operation to test + * @param testResult The container for test results + * @param requests A set of reference requests for operation + * @param endpointUrl The URL of endpoint to test + * @param method The method that applies for requesting service (retrieved using buildMethod() method) + * @return A list of TestReturn corresponding to the result of test for each reference requests. Returns indices + * matches reference request indices. + * @throws java.net.URISyntaxException if endpointUrl cannot be transformed as URI + * @throws java.io.IOException in case of network failure mainly + */ + public abstract List runTest(Service service, Operation operation, TestResult testResult, + List requests, String endpointUrl, T method) throws URISyntaxException, IOException; + + /** + * (interpretation is subject to implementation) + * @param method String representation of method + * @return Object representing method + */ + public abstract T buildMethod(String method); + + /** + * Build a single string value from values set. + * @param values Strings to build value from + * @return Comma separated string of values + */ + protected String buildValue(Set values) { + if (values == null || values.isEmpty()) { + return null; + } + StringBuilder result = new StringBuilder(); + Iterator iterator = values.iterator(); + result.append(iterator.next()); + while (iterator.hasNext()) { + result.append(","); + result.append(iterator.next()); + } + + return result.toString(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/HttpTestRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/HttpTestRunner.java new file mode 100644 index 000000000..820f991b2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/HttpTestRunner.java @@ -0,0 +1,360 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.test; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.util.URIBuilder; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +/** + * An extension of AbstractTestRunner that checks that returned response is valid according the HTTP code of the + * response (OK if code in the 20x range, KO in the 30x and over range). + * @author laurent + */ +public class HttpTestRunner extends AbstractTestRunner { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(HttpTestRunner.class); + + private static final String IOEXCEPTION_STATUS_CODE = "IOException while getting raw status code in response"; + + private Secret secret; + + private ClientHttpRequestFactory clientHttpRequestFactory; + + /** + * Set the Secret used for securing the requests. + * @param secret The Secret used or securing the requests. + */ + public void setSecret(Secret secret) { + this.secret = secret; + } + + /** + * Set the ClientHttpRequestFactory used for reaching endpoint. + * @param clientHttpRequestFactory The ClientHttpRequestFactory used for reaching endpoint + */ + public void setClientHttpRequestFactory(ClientHttpRequestFactory clientHttpRequestFactory) { + this.clientHttpRequestFactory = clientHttpRequestFactory; + } + + @Override + public List runTest(Service service, Operation operation, TestResult testResult, List requests, + String endpointUrl, HttpMethod method) throws URISyntaxException, IOException { + + log.debug("Launching test run on {} for {} request(s)", endpointUrl, requests.size()); + + if (requests.isEmpty()) { + return null; + } + + // Initialize result container. + List result = new ArrayList<>(); + + for (Request request : requests) { + // Reset status code, message and request each time. + int code = TestReturn.SUCCESS_CODE; + String message = null; + String customizedEndpointUrl = endpointUrl; + if (ServiceType.REST.equals(service.getType())) { + String operationName = operation.getName(); + // Name may start with verb, remove it if present. + if (operationName.trim().contains(" ")) { + operationName = operationName.trim().split(" ")[1]; + } + + customizedEndpointUrl += URIBuilder.buildURIFromPattern(operationName, request.getQueryParameters()); + log.debug("Using customized endpoint url: {}", customizedEndpointUrl); + } + ClientHttpRequest httpRequest = clientHttpRequestFactory.createRequest(new URI(customizedEndpointUrl), method); + + // Prepare headers on httpRequest and report on request. + Set
headers = prepareRequestHeaders(operation, httpRequest, request, testResult); + + // Allow extensions to realize some pre-processing of request. + prepareRequest(request); + + // If there's input content, add it to request. + if (request.getContent() != null) { + // Update request content with rendered body if necessary. + request.setContent(TestRunnerCommons.renderRequestContent(request, headers)); + log.trace("Sending following request content: {}", request.getContent()); + httpRequest.getBody().write(request.getContent().getBytes()); + } + + // Actually execute request. + long startTime = System.currentTimeMillis(); + ClientHttpResponse httpResponse = null; + try { + httpResponse = httpRequest.execute(); + } catch (IOException ioe) { + log.error("IOException while executing request {} on {}", request.getName(), customizedEndpointUrl, ioe); + code = TestReturn.FAILURE_CODE; + message = ioe.getMessage(); + } + long duration = System.currentTimeMillis() - startTime; + + // Extract and store response body so that stream may not be consumed more than 1 time ;-) + String responseContent = getResponseContent(httpResponse); + + // If still in success, check if http code is out of correct ranges (20x and 30x). + if (code == TestReturn.SUCCESS_CODE) { + code = extractTestReturnCode(service, operation, request, httpResponse, responseContent); + message = extractTestReturnMessage(service, operation, request, httpResponse); + } + + // Create a Response object and complete it for returning. + Response response = new Response(); + completeResponse(response, httpResponse, responseContent); + + result.add(new TestReturn(code, duration, message, request, response)); + } + return result; + } + + /** + * Build the HttpMethod corresponding to string. Default to POST if unknown or unrecognized. + */ + @Override + public HttpMethod buildMethod(String method) { + if (method != null) { + if ("GET".equals(method.toUpperCase().trim())) { + return HttpMethod.GET; + } else if ("PUT".equals(method.toUpperCase().trim())) { + return HttpMethod.PUT; + } else if ("DELETE".equals(method.toUpperCase().trim())) { + return HttpMethod.DELETE; + } else if ("PATCH".equals(method.toUpperCase().trim())) { + return HttpMethod.PATCH; + } else if ("OPTIONS".equals(method.toUpperCase().trim())) { + return HttpMethod.OPTIONS; + } + } + return HttpMethod.POST; + } + + /** + * Manage headers or additional headers preparation on httpRequest and report on request. + * @param operation The operation this request is prepared for + * @param httpRequest The http request to preapre + * @param request The recorded microcks request to report values on + * @param testResult The results that may contain headers override + * @return The prepared headers. + */ + protected Set
prepareRequestHeaders(Operation operation, ClientHttpRequest httpRequest, Request request, + TestResult testResult) { + Set
headers = TestRunnerCommons.collectHeaders(testResult, request, operation); + + if (!headers.isEmpty()) { + for (Header header : headers) { + log.debug("Adding header {} to request", header.getName()); + httpRequest.getHeaders().add(header.getName(), buildValue(header.getValues())); + } + } + // Update request headers for traceability of possibly added ones. + request.setHeaders(headers); + + // Now manage specific authorization headers if there's a secret. + if (secret != null) { + addAuthorizationHeadersFromSecret(httpRequest, request, secret); + } + return headers; + } + + /** + * Extract the Http response body as a string. + * @param httpResponse The http response + * @return The UTF-8 string representation of response body content + * @throws IOException if http response cannot be read + */ + protected String getResponseContent(ClientHttpResponse httpResponse) throws IOException { + if (httpResponse != null) { + StringWriter writer = new StringWriter(); + IOUtils.copy(httpResponse.getBody(), writer, StandardCharsets.UTF_8); + return writer.toString(); + } + return null; + } + + /** + * This is a hook for allowing sub-classes to redefine the criteria for telling a response is a success or a failure. + * This implementation check raw http code (success if in 20x range, failure if not). + * @param service The service under test + * @param operation The tested operation + * @param request The tested reference request + * @param httpResponse The received response from endpoint + * @param responseContent The response body content if any (may be null) + * @return The test result code, whether TestReturn.SUCCESS_CODE or TestReturn.FAILURE_CODE + */ + protected int extractTestReturnCode(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse, String responseContent) { + int code = TestReturn.SUCCESS_CODE; + // Set code to failure if http code out of correct ranges (20x and 30x). + try { + if (httpResponse.getStatusCode().value() > 299) { + log.debug("Http status code is {}, marking test as failed.", httpResponse.getStatusCode().value()); + code = TestReturn.FAILURE_CODE; + } + } catch (IOException ioe) { + log.debug(IOEXCEPTION_STATUS_CODE, ioe); + code = TestReturn.FAILURE_CODE; + } + return code; + } + + /** + * This is a hook for allowing sub-classes to realize some pre-processing on request before it is actually issued to + * test endpoint. + * @param request The request that will be sent to endpoint + */ + protected void prepareRequest(Request request) { + // Nothing to do in this base implementation, use raw request. + } + + /** + * This is a hook for allowing sub-classes to redefine the extraction of success or failure message. This + * implementation just extract raw http code. + * @param service The service under test + * @param operation The tested operation + * @param request The tested reference request + * @param httpResponse The received response from endpoint + * @return The test result message. + */ + protected String extractTestReturnMessage(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse) { + String message = null; + // Set code to failure if http code out of correct ranges (20x and 30x). + try { + message = String.valueOf(httpResponse.getStatusCode().value()); + } catch (IOException ioe) { + log.debug(IOEXCEPTION_STATUS_CODE, ioe); + message = IOEXCEPTION_STATUS_CODE; + } + return message; + } + + /** + * Complete the microcks domain response from http response. + * @param response The Microcks domain response + * @param httpResponse The Http response + * @param responseContent The Http response body content previously extracted (to prevent reading it multiple time) + * @throws IOException If status code of response cannot be read + */ + protected void completeResponse(Response response, ClientHttpResponse httpResponse, String responseContent) + throws IOException { + if (httpResponse != null) { + response.setContent(responseContent); + response.setStatus(String.valueOf(httpResponse.getStatusCode().value())); + log.debug("Response Content-Type: {}", httpResponse.getHeaders().getContentType()); + + MediaType contentType = httpResponse.getHeaders().getContentType(); + if (contentType != null) { + response.setMediaType(contentType.toString()); + } + Set
headers = buildHeaders(httpResponse); + if (!headers.isEmpty()) { + response.setHeaders(headers); + } + log.debug("Closing http response"); + httpResponse.close(); + } + } + + /** Build domain headers from ClientHttpResponse ones. */ + private Set
buildHeaders(ClientHttpResponse httpResponse) { + Set
headers = new HashSet<>(); + if (!httpResponse.getHeaders().isEmpty()) { + HttpHeaders responseHeaders = httpResponse.getHeaders(); + for (Entry> responseHeader : responseHeaders.entrySet()) { + Header header = new Header(); + header.setName(responseHeader.getKey()); + header.setValues(new HashSet<>(responseHeader.getValue())); + headers.add(header); + } + } + return headers; + } + + /** Complete the test request with authorization data coming from secret. */ + private void addAuthorizationHeadersFromSecret(ClientHttpRequest httpRequest, Request request, Secret secret) { + if (secret != null) { + // If Basic authentication required, set request property. + if (secret.getUsername() != null && secret.getPassword() != null) { + log.debug("Secret contains username/password, assuming Authorization Basic"); + // Building a base64 string. + String encoded = Base64.getEncoder() + .encodeToString((secret.getUsername() + ":" + secret.getPassword()).getBytes(StandardCharsets.UTF_8)); + httpRequest.getHeaders().set(HttpHeaders.AUTHORIZATION, "Basic " + encoded); + // Add to Microcks request for traceability. + request.getHeaders().add(buildAuthHeaderWithSecret(HttpHeaders.AUTHORIZATION, "Basic ")); + } + + // If Token authentication required, set request property. + if (secret.getToken() != null) { + if (secret.getTokenHeader() != null && !secret.getTokenHeader().trim().isEmpty()) { + log.debug("Secret contains token and token header, adding them as request header"); + httpRequest.getHeaders().set(secret.getTokenHeader().trim(), secret.getToken()); + // Add to Microcks request for traceability. + request.getHeaders().add(buildAuthHeaderWithSecret(secret.getTokenHeader().trim(), "")); + } else { + log.debug("Secret contains token only, assuming Authorization Bearer"); + httpRequest.getHeaders().set(HttpHeaders.AUTHORIZATION, "Bearer " + secret.getToken()); + // Add to Microcks request for traceability. + request.getHeaders().add(buildAuthHeaderWithSecret(HttpHeaders.AUTHORIZATION, "Bearer ")); + } + } + } + } + + /** Build a Microcks header for traceability. */ + private Header buildAuthHeaderWithSecret(String name, String type) { + Header authHeader = new Header(); + authHeader.setName(name); + authHeader.setValues(new TreeSet<>(Arrays.asList(type + ""))); + return authHeader; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/SoapHttpTestRunner.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/SoapHttpTestRunner.java new file mode 100644 index 000000000..8207fc15d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/SoapHttpTestRunner.java @@ -0,0 +1,125 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.test; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestReturn; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.util.soap.SoapMessageValidator; +/* +import io.github.microcks.util.SoapMessageValidator; +import org.apache.xmlbeans.XmlError; +import org.apache.xmlbeans.XmlException; + */ +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.util.UriUtils; + +import javax.xml.namespace.QName; +import java.util.List; + +/** + * An extension of HttpTestRunner that checks that returned response is valid according the SOAP contract definition. + * + * @see SoapMessageValidator + * @author laurent + */ +public class SoapHttpTestRunner extends HttpTestRunner { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(SoapHttpTestRunner.class); + + /** The URL of resources used for validation. */ + private String resourceUrl = null; + + private ResourceRepository resourceRepository; + + /** + * + * @param resourceRepository + */ + public SoapHttpTestRunner(ResourceRepository resourceRepository) { + this.resourceRepository = resourceRepository; + } + + /** + * The URL of resources used for validation. + * @return The URL of resources used for validation + */ + public String getResourceUrl() { + return resourceUrl; + } + + /** + * The URL of resources used for validation. + * @param resourceUrl The URL of resources used for validation. + */ + public void setResourceUrl(String resourceUrl) { + this.resourceUrl = resourceUrl; + } + + /** + * Build the HttpMethod corresponding to string. Always POST for a SoapHttpTestRunner. + */ + @Override + public HttpMethod buildMethod(String method) { + return HttpMethod.POST; + } + + @Override + protected int extractTestReturnCode(Service service, Operation operation, Request request, + ClientHttpResponse httpResponse, String responseContent) { + int code = TestReturn.SUCCESS_CODE; + + // Checking HTTP return code: delegating to super class. + code = super.extractTestReturnCode(service, operation, request, httpResponse, responseContent); + // If test is already a failure (40x code), no need to pursue... + if (TestReturn.FAILURE_CODE == code) { + return code; + } + + Resource wsdlResource = resourceRepository.findByServiceIdAndType(service.getId(), ResourceType.WSDL).get(0); + List errors = SoapMessageValidator.validateSoapMessage(wsdlResource.getContent(), + new QName(service.getXmlNS(), operation.getOutputName()), responseContent, resourceUrl); + + log.debug("SoapBody validation errors: " + errors.size()); + + if (!errors.isEmpty()) { + log.debug("Soap validation errors found " + errors.size() + ", marking test as failed."); + return TestReturn.FAILURE_CODE; + } + + /* + * try{ // Validate Soap message body according to operation output part. List errors = + * SoapMessageValidator.validateSoapMessage( operation.getOutputName(), service.getXmlNS(), responseContent, + * resourceUrl + UriUtils.encodeFragment(service.getName(), "UTF-8") + "-" + service.getVersion() + ".wsdl", true + * ); + * + * if (!errors.isEmpty()){ log.debug("Soap validation errors found " + errors.size() + + * ", marking test as failed."); return TestReturn.FAILURE_CODE; } } catch (XmlException e) { + * log.debug("XmlException while validating Soap response message", e); return TestReturn.FAILURE_CODE; } + */ + return code; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/TestRunnerCommons.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/TestRunnerCommons.java new file mode 100644 index 000000000..a0af9903c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/util/test/TestRunnerCommons.java @@ -0,0 +1,112 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.test; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.TestResult; +import io.github.microcks.util.el.EvaluableRequest; +import io.github.microcks.util.el.TemplateEngine; +import io.github.microcks.util.el.TemplateEngineFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This class holds commons, utility handlers for different test runner implements (whether it be Soap, OpenAPI, + * Async...) + * @author laurent + */ +public class TestRunnerCommons { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(TestRunnerCommons.class); + + /** + * Render the request content using the Expression Language compatible {@code TemplateEngine} if required. If + * rendering template fails, we just produce a log error message and stick to templatized content. + * @param request The request that will be sent for test. + * @param headers The set of computed headers to use for request body evaluation + * @return The rendered response body payload. + */ + public static String renderRequestContent(Request request, Set
headers) { + if (request.getContent().contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + log.debug("Response contains dynamic EL expression, rendering it..."); + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + + // Create and fill an evaluable request object. + EvaluableRequest evaluableRequest = new EvaluableRequest(request.getContent(), null); + // Adding query parameters... + Map evaluableParams = new HashMap<>(); + for (Parameter parameter : request.getQueryParameters()) { + evaluableParams.put(parameter.getName(), parameter.getValue()); + } + evaluableRequest.setParams(evaluableParams); + // Adding headers... + Map evaluableHeaders = new HashMap<>(); + for (Header header : request.getHeaders()) { + evaluableHeaders.put(header.getName(), String.join(",", header.getValues())); + } + evaluableRequest.setHeaders(evaluableHeaders); + + // Register the request variable and evaluate the request body. + engine.getContext().setVariable("request", evaluableRequest); + try { + return engine.getValue(request.getContent()); + } catch (Throwable t) { + log.error("Failing at evaluating template " + request.getContent(), t); + return request.getContent(); + } + } + return request.getContent(); + } + + /** + * Construct set of headers given specification of request, operations and testResult. TestResult operation-specific + * headers overrule testResult global headers which overrule request headers. + * @param testResult The configured test run containing global and operation-specific headers. + * @param request The example request with headers. + * @param operation The operation to be run. + * @return A set of headers for the given operation + */ + public static Set
collectHeaders(TestResult testResult, Request request, Operation operation) { + Set
headers = new HashSet<>(); + + // Set headers to request if any. Start with those coming from request itself. + if (request.getHeaders() != null) { + headers.addAll(request.getHeaders()); + } + + // Add or override existing headers with test specific ones for operation and globals. + if (testResult.getOperationsHeaders() != null) { + if (testResult.getOperationsHeaders().getGlobals() != null) { + headers.addAll(testResult.getOperationsHeaders().getGlobals()); + } + if (testResult.getOperationsHeaders().get(operation.getName()) != null) { + headers.addAll(testResult.getOperationsHeaders().get(operation.getName())); + } + } + + return headers; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/AICopilotController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/AICopilotController.java new file mode 100644 index 000000000..ae1ab4eff --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/AICopilotController.java @@ -0,0 +1,241 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.security.UserInfo; +import io.github.microcks.service.AICopilotRunnerService; +import io.github.microcks.service.ExchangeSelection; +import io.github.microcks.service.ImportExportService; +import io.github.microcks.service.ServiceService; +import io.github.microcks.util.MockRepositoryExporterFactory; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.ai.AICopilot; + +import org.apache.catalina.User; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A controller for interacting with optional AI Copilot in Microcks. + * @author laurent + */ +@RestController +@RequestMapping("/api/copilot") +public class AICopilotController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(AICopilotController.class); + + private AICopilot copilot; + + private final ServiceService serviceService; + private final ImportExportService importExportService; + private final ServiceRepository serviceRepository; + private final ResourceRepository resourceRepository; + private final AICopilotRunnerService copilotRunnerService; + + // A map to track async task status. + private final Map taskStatus = new ConcurrentHashMap<>(); + + + /** + * Build a AICopilotController with required dependencies. + * @param serviceService The service to managed Services objects + * @param importExportService The service to manage import/export of data + * @param serviceRepository The repository for Services + * @param resourceRepository The repository for Resources + * @param copilot The optional AI Copilot + */ + public AICopilotController(ServiceService serviceService, ImportExportService importExportService, + ServiceRepository serviceRepository, ResourceRepository resourceRepository, Optional copilot, + AICopilotRunnerService copilotRunnerService) { + this.serviceService = serviceService; + this.importExportService = importExportService; + this.serviceRepository = serviceRepository; + this.resourceRepository = resourceRepository; + copilot.ifPresent(aiCopilot -> this.copilot = aiCopilot); + this.copilotRunnerService = copilotRunnerService; + } + + @GetMapping(value = "/samples/{id:.+}") + public ResponseEntity getSamplesSuggestions(@PathVariable("id") String serviceId, + @RequestParam(value = "operation", required = false) String operationName, UserInfo userInfo) { + log.debug("Retrieving service with id {}", serviceId); + + Service service = null; + // serviceId may have the form of : + if (serviceId.contains(":")) { + String name = serviceId.substring(0, serviceId.indexOf(':')); + String version = serviceId.substring(serviceId.indexOf(':') + 1); + + // If service name was encoded with '+' instead of '%20', replace them. + if (name.contains("+")) { + name = name.replace('+', ' '); + } + service = serviceRepository.findByNameAndVersion(name, version); + } else { + service = serviceRepository.findById(serviceId).orElse(null); + } + + if (service != null) { + log.debug("We found service, now looking for required contract..."); + List resources = null; + if (service.getType() == ServiceType.REST) { + resources = resourceRepository.findByServiceIdAndType(service.getId(), ResourceType.OPEN_API_SPEC); + } else if (service.getType() == ServiceType.GRAPHQL) { + resources = resourceRepository.findByServiceIdAndType(service.getId(), ResourceType.GRAPHQL_SCHEMA); + } else if (service.getType() == ServiceType.EVENT) { + resources = resourceRepository.findByServiceIdAndType(service.getId(), ResourceType.ASYNC_API_SPEC); + } else if (service.getType() == ServiceType.GRPC) { + resources = resourceRepository.findByServiceIdAndType(service.getId(), ResourceType.PROTOBUF_SCHEMA); + } + + if (operationName != null) { + // Find the matching operation on service. + Optional operation = service.getOperations().stream() + .filter(op -> operationName.equals(op.getName())).findFirst(); + + // Generate samples in a synchronous way. + if (resources != null && !resources.isEmpty() && operation.isPresent()) { + try { + List exchanges = copilot.suggestSampleExchanges(service, operation.get(), + resources.getFirst(), 2); + return new ResponseEntity<>(exchanges, HttpStatus.OK); + } catch (Exception e) { + log.error("Caught and exception while generating samples", e); + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } else { + // Launch an async generation of samples for all operations. + final String taskId = UUID.randomUUID().toString(); + taskStatus.put(taskId, TaskStatus.PENDING); + copilotRunnerService.generateSamplesForService(service, resources.getFirst(), userInfo) + .thenApply(success -> { + if (success) { + taskStatus.put(taskId, TaskStatus.SUCCESS); + } else { + taskStatus.put(taskId, TaskStatus.FAILURE); + } + return success; + }); + return new ResponseEntity<>("{\"taskId\": \"" + taskId + "\"}", HttpStatus.CREATED); + } + } + log.error("At least one mandatory parameters (serviceId, operationName or contract) is missing"); + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + + @GetMapping(value = "/samples/task/{id}/status") + public ResponseEntity getGenerationTaskStatus(@PathVariable("id") String taskId) { + log.debug("Retrieving status for task {}", taskId); + TaskStatus status = taskStatus.get(taskId); + if (status == null) { + return new ResponseEntity<>("{\"status\": \"NOT_FOUND\"}", HttpStatus.NOT_FOUND); + } + switch (status) { + case PENDING: + return new ResponseEntity<>("{\"status\": \"PENDING\"}", HttpStatus.ACCEPTED); + case SUCCESS: + taskStatus.remove(taskId); + return new ResponseEntity<>("{\"status\": \"SUCCESS\"}", HttpStatus.CREATED); + case FAILURE: + taskStatus.remove(taskId); + return new ResponseEntity<>("{\"status\": \"FAILURE\"}", HttpStatus.INTERNAL_SERVER_ERROR); + default: + return new ResponseEntity<>("{\"status\": \"NOT_FOUND\"}", HttpStatus.NOT_FOUND); + } + } + + @PostMapping(value = "/samples/{id:.+}") + public ResponseEntity addSamplesSuggestions(@PathVariable("id") String serviceId, + @RequestParam(value = "operation") String operationName, @RequestBody List exchanges, + UserInfo userInfo) { + log.debug("Adding new AI samples to service {} and operation {}", serviceId, operationName); + boolean result = serviceService.addAICopilotExchangesToServiceOperation(serviceId, operationName, exchanges, + userInfo); + if (result) { + return new ResponseEntity<>(HttpStatus.CREATED); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @PostMapping(value = "/samples/{id:.+}/cleanup") + public ResponseEntity removeExchanges(@PathVariable("id") String serviceId, + @RequestBody ExchangeSelection exchangeSelection, UserInfo userInfo) { + log.debug("Cleaning AI samples from service {} and multiple operations", serviceId); + boolean result = serviceService.removeAICopilotExchangesFromService(serviceId, exchangeSelection, userInfo); + if (result) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @PostMapping(value = "/samples/{id:.+}/export") + public ResponseEntity exportExchanges(@PathVariable("id") String serviceId, + @RequestParam(value = "format", required = false, defaultValue = MockRepositoryExporterFactory.API_EXAMPLES_FORMAT) String exportFormat, + @RequestBody ExchangeSelection exchangeSelection, UserInfo userInfo) { + log.debug("Generating AI samples export for service {}", serviceId); + + try { + byte[] body = importExportService.exportExchangeSelection(serviceId, exchangeSelection, exportFormat, userInfo) + .getBytes(StandardCharsets.UTF_8); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.APPLICATION_JSON); + responseHeaders.set("Content-Disposition", "attachment; filename=microcks-repository.json"); + responseHeaders.setContentLength(body.length); + + return new ResponseEntity<>(body, responseHeaders, HttpStatus.OK); + } catch (Exception e) { + log.error("Exception while exporting the Exchanges selection", e); + return new ResponseEntity<>("Exception while exporting the Exchanges selection. Check server logs.", + HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + private enum TaskStatus { + PENDING, + SUCCESS, + FAILURE + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/BasicHttpServletRequest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/BasicHttpServletRequest.java new file mode 100644 index 000000000..39ecf5517 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/BasicHttpServletRequest.java @@ -0,0 +1,429 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConnection; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.Part; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Basic implementation of HttpServletRequest that does nothing and just carries basic values. + * @author laurent + */ +public class BasicHttpServletRequest implements HttpServletRequest { + + private final String urlPrefix; + private final String method; + private final String pathInfo; + private final String queryString; + private final Map queryParameters; + private final Map> headers; + + public BasicHttpServletRequest(String urlPrefix, String method, String pathInfo, String queryString, + Map queryParameters, Map> headers) { + this.urlPrefix = urlPrefix; + this.method = method; + this.pathInfo = pathInfo; + this.queryString = queryString; + this.queryParameters = queryParameters; + this.headers = headers; + } + + @Override + public String getAuthType() { + return ""; + } + + @Override + public Cookie[] getCookies() { + return new Cookie[0]; + } + + @Override + public long getDateHeader(String s) { + return 0; + } + + @Override + public String getHeader(String s) { + if (headers.containsKey(s)) { + return headers.get(s).getFirst(); + } + return null; + } + + @Override + public Enumeration getHeaders(String s) { + if (headers.containsKey(s)) { + return Collections.enumeration(headers.get(s)); + } + return null; + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public int getIntHeader(String s) { + return 0; + } + + @Override + public String getMethod() { + return method; + } + + @Override + public String getPathInfo() { + return pathInfo; + } + + @Override + public String getPathTranslated() { + return pathInfo; + } + + @Override + public String getContextPath() { + return ""; + } + + @Override + public String getQueryString() { + return queryString; + } + + @Override + public String getRemoteUser() { + return ""; + } + + @Override + public boolean isUserInRole(String s) { + return false; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public String getRequestedSessionId() { + return ""; + } + + @Override + public String getRequestURI() { + return pathInfo; + } + + @Override + public StringBuffer getRequestURL() { + StringBuffer url = new StringBuffer(urlPrefix); + url.append(pathInfo); + if (queryString != null) { + url.append("?").append(queryString); + } + return url; + } + + @Override + public String getServletPath() { + return ""; + } + + @Override + public HttpSession getSession(boolean b) { + return null; + } + + @Override + public HttpSession getSession() { + return null; + } + + @Override + public String changeSessionId() { + return ""; + } + + @Override + public boolean isRequestedSessionIdValid() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return false; + } + + @Override + public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException { + return false; + } + + @Override + public void login(String s, String s1) throws ServletException { + // Our basic implementation does not support this method. + } + + @Override + public void logout() throws ServletException { + // Our basic implementation does not support this method. + } + + @Override + public Collection getParts() throws IOException, ServletException { + return List.of(); + } + + @Override + public Part getPart(String s) throws IOException, ServletException { + return null; + } + + @Override + public T upgrade(Class aClass) throws IOException, ServletException { + return null; + } + + @Override + public Object getAttribute(String s) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public String getCharacterEncoding() { + return ""; + } + + @Override + public void setCharacterEncoding(String s) throws UnsupportedEncodingException { + // Our basic implementation does not support this method. + } + + @Override + public int getContentLength() { + return 0; + } + + @Override + public long getContentLengthLong() { + return 0; + } + + @Override + public String getContentType() { + return ""; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return null; + } + + @Override + public String getParameter(String s) { + return queryParameters.getOrDefault(s, null); + } + + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(queryParameters.keySet()); + } + + @Override + public String[] getParameterValues(String s) { + return new String[0]; + } + + @Override + public Map getParameterMap() { + return Map.of(); + } + + @Override + public String getProtocol() { + return ""; + } + + @Override + public String getScheme() { + return ""; + } + + @Override + public String getServerName() { + return ""; + } + + @Override + public int getServerPort() { + return 0; + } + + @Override + public BufferedReader getReader() throws IOException { + return null; + } + + @Override + public String getRemoteAddr() { + return ""; + } + + @Override + public String getRemoteHost() { + return ""; + } + + @Override + public void setAttribute(String s, Object o) { + // Our basic implementation does not support this method. + } + + @Override + public void removeAttribute(String s) { + // Our basic implementation does not support this method. + } + + @Override + public Locale getLocale() { + return null; + } + + @Override + public Enumeration getLocales() { + return null; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public RequestDispatcher getRequestDispatcher(String s) { + return null; + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public String getLocalName() { + return ""; + } + + @Override + public String getLocalAddr() { + return ""; + } + + @Override + public int getLocalPort() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } + + @Override + public String getRequestId() { + return ""; + } + + @Override + public String getProtocolRequestId() { + return ""; + } + + @Override + public ServletConnection getServletConnection() { + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DispatchContext.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DispatchContext.java new file mode 100644 index 000000000..fc9762469 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DispatchContext.java @@ -0,0 +1,26 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import java.util.Map; + +/** + * Simple record wrapping a request dispatch criteria and its possibly enabled invocation context + * @param dispatchCriteria The dispatch criteria for a request (mandatory) + * @param requestContext The associated invocation context (optional ; may be null) + */ +record DispatchContext(String dispatchCriteria, Map requestContext) { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DocumentationController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DocumentationController.java new file mode 100644 index 000000000..351588d5d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DocumentationController.java @@ -0,0 +1,149 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.util.SafeLogger; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * A controller for generating API documentation for contracts mocked within Microcks. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/api") +public class DocumentationController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(DocumentationController.class); + + private static final String RESOURCE_URL = "{RESOURCE_URL}"; + + final ResourceRepository resourceRepository; + + /** + * Build a new DocumentationController with a resource repository. + * @param resourceRepository Repository to access resource. + */ + public DocumentationController(ResourceRepository resourceRepository) { + this.resourceRepository = resourceRepository; + } + + @GetMapping(value = "/documentation/{name}/{resourceType}") + public ResponseEntity getDocumentationByResourceName(@PathVariable("name") String name, + @PathVariable("resourceType") String resourceType) { + log.info("Requesting {} documentation for resource {}", resourceType, name); + + Resource resource = null; + if (ResourceType.ASYNC_API_SPEC.toString().equals(resourceType)) { + List resources = resourceRepository.findByName(name); + if (!resources.isEmpty()) { + Optional resourceOpt = resources.stream().filter(Resource::isMainArtifact).findFirst(); + if (resourceOpt.isPresent()) { + resource = resourceOpt.get(); + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + return responseWithResource("/api/resources/" + name, resourceType, resource); + } + + @GetMapping(value = "/documentation/id/{id}/{resourceType}") + public ResponseEntity getDocumentationByResourceId(@PathVariable("id") String id, + @PathVariable("resourceType") String resourceType) { + log.info("Requesting {} documentation for resource with id {}", resourceType, id); + + Resource resource = null; + if (ResourceType.ASYNC_API_SPEC.toString().equals(resourceType)) { + Optional resourceOpt = resourceRepository.findById(id); + if (resourceOpt.isPresent()) { + resource = resourceOpt.get(); + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + return responseWithResource("/api/resources/id/" + id, resourceType, resource); + } + + private ResponseEntity responseWithResource(String resourceUrl, String resourceType, Resource resource) { + // Prepare HttpHeaders. + InputStream stream = null; + HttpHeaders headers = new HttpHeaders(); + org.springframework.core.io.Resource template = null; + + // Get the correct template depending on resource type. + if (ResourceType.OPEN_API_SPEC.toString().equals(resourceType) + || ResourceType.SWAGGER.toString().equals(resourceType)) { + template = new ClassPathResource("templates/redoc.html"); + headers.setContentType(MediaType.TEXT_HTML); + } else if (ResourceType.ASYNC_API_SPEC.toString().equals(resourceType)) { + + if (resource.getContent().contains("asyncapi: 3") || resource.getContent().contains("\"asyncapi\": \"3") + || resource.getContent().contains("'asyncapi': '3")) { + template = new ClassPathResource("templates/asyncapi-v3.html"); + } else { + template = new ClassPathResource("templates/asyncapi.html"); + } + headers.setContentType(MediaType.TEXT_HTML); + } + + if (template != null) { + try { + stream = template.getInputStream(); + } catch (IOException e) { + log.error("IOException while reading template {}", template.getDescription(), e); + } + + // Now process the stream, replacing patterns by value. + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + StringWriter writer = new StringWriter(); + + try (Stream lines = reader.lines()) { + lines.map(line -> replaceResourceUrlInLing(line, resourceUrl)).forEach(line -> writer.write(line + "\n")); + } + return new ResponseEntity<>(writer.toString().getBytes(), headers, HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + private String replaceResourceUrlInLing(String line, String resourceUrl) { + return line.replace(RESOURCE_URL, resourceUrl); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DynamicMockRestController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DynamicMockRestController.java new file mode 100644 index 000000000..d067a3e80 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/DynamicMockRestController.java @@ -0,0 +1,351 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.GenericResource; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.event.MockInvocationEvent; +import io.github.microcks.repository.GenericResourceRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.el.EvaluableRequest; +import io.github.microcks.util.el.TemplateEngine; +import io.github.microcks.util.el.TemplateEngineFactory; + +import org.bson.Document; +import org.bson.json.JsonParseException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriUtils; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * This is the controller for Dynamic mocks in Microcks. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/dynarest") +public class DynamicMockRestController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(DynamicMockRestController.class); + + public static final String ID_FIELD = "id"; + + private final ServiceRepository serviceRepository; + private final GenericResourceRepository genericResourceRepository; + private final ApplicationContext applicationContext; + + @Value("${mocks.enable-invocation-stats}") + private final Boolean enableInvocationStats = null; + + /** + * Build a new DynamicMockRestController with required dependencies. + * @param serviceRepository the repository for services + * @param genericResourceRepository the repository for generic resources + * @param applicationContext the Spring application context + */ + public DynamicMockRestController(ServiceRepository serviceRepository, + GenericResourceRepository genericResourceRepository, ApplicationContext applicationContext) { + this.serviceRepository = serviceRepository; + this.genericResourceRepository = genericResourceRepository; + this.applicationContext = applicationContext; + } + + @PostMapping(value = "/{service}/{version}/{resource}", produces = "application/json") + public ResponseEntity createResource(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @PathVariable("resource") String resource, + @RequestParam(value = "delay", required = false) Long delay, @RequestBody(required = true) String body, + HttpServletRequest request) { + log.debug("Creating a new resource '{}' for service '{}-{}'", resource, serviceName, version); + long startTime = System.currentTimeMillis(); + + serviceName = sanitizeServiceName(serviceName); + + MockContext mockContext = getMockContext(serviceName, version, "POST /" + resource); + if (mockContext != null) { + Document document = null; + GenericResource genericResource = null; + + try { + // Try parsing body payload that should be json. + document = Document.parse(body); + // Now create a generic resource. + genericResource = new GenericResource(); + genericResource.setServiceId(mockContext.service.getId()); + genericResource.setPayload(document); + + genericResource = genericResourceRepository.save(genericResource); + } catch (JsonParseException jpe) { + // Return a 422 code : unprocessable entity. + return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); + } + + // Append id and wait if specified before returning. + document.append(ID_FIELD, genericResource.getId()); + waitForDelay(startTime, delay, mockContext); + return new ResponseEntity<>(document.toJson(), HttpStatus.CREATED); + } + // Return a 400 code : bad request. + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + @GetMapping(value = "/{service}/{version}/{resource}", produces = "application/json") + public ResponseEntity findResources(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @PathVariable("resource") String resource, + @RequestParam(value = "page", required = false, defaultValue = "0") int page, + @RequestParam(value = "size", required = false, defaultValue = "20") int size, + @RequestParam(value = "delay", required = false) Long delay, @RequestBody(required = false) String body, + HttpServletRequest request) { + log.debug("Find resources '{}' for service '{}-{}'", resource, serviceName, version); + long startTime = System.currentTimeMillis(); + + serviceName = sanitizeServiceName(serviceName); + + // Build the encoded URI fragment to retrieve simple resourcePath. + String requestURI = request.getRequestURI(); + String serviceAndVersion = "/" + UriUtils.encodeFragment(serviceName, "UTF-8") + "/" + version; + String resourcePath = requestURI.substring(requestURI.indexOf(serviceAndVersion) + serviceAndVersion.length()); + + MockContext mockContext = getMockContext(serviceName, version, "GET /" + resource); + if (mockContext != null) { + + List genericResources = null; + if (body == null) { + genericResources = genericResourceRepository.findByServiceId(mockContext.service.getId(), + PageRequest.of(page, size)); + } else { + genericResources = genericResourceRepository.findByServiceIdAndJSONQuery(mockContext.service.getId(), body); + } + + // Prepare templating support with evaluable request and engine. + EvaluableRequest evaluableRequest = MockControllerCommons.buildEvaluableRequest(body, resourcePath, request); + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + + // Transform and collect resources. + List resources = genericResources.stream().map(genericResource -> MockControllerCommons + .renderResponseContent(evaluableRequest, engine, transformToResourceJSON(genericResource))) + .collect(Collectors.toList()); + + // Wait if specified before returning. + waitForDelay(startTime, delay, mockContext); + MockControllerCommons.waitForDelay(startTime, delay); + + return new ResponseEntity<>(formatToJSONArray(resources), HttpStatus.OK); + } + // Return a 400 code : bad request. + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + @GetMapping(value = "/{service}/{version}/{resource}/{resourceId}", produces = "application/json") + public ResponseEntity getResource(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @PathVariable("resource") String resource, + @PathVariable("resourceId") String resourceId, @RequestParam(value = "delay", required = false) Long delay, + HttpServletRequest request) { + log.debug("Get resource '{}:{}' for service '{}-{}'", resource, resourceId, serviceName, version); + long startTime = System.currentTimeMillis(); + + serviceName = sanitizeServiceName(serviceName); + + // Build the encoded URI fragment to retrieve simple resourcePath. + String requestURI = request.getRequestURI(); + String serviceAndVersion = "/" + UriUtils.encodeFragment(serviceName, "UTF-8") + "/" + version; + String resourcePath = requestURI.substring(requestURI.indexOf(serviceAndVersion) + serviceAndVersion.length()); + + MockContext mockContext = getMockContext(serviceName, version, "GET /" + resource + "/:id"); + if (mockContext != null) { + // Get the requested generic resource. + GenericResource genericResource = genericResourceRepository.findById(resourceId).orElse(null); + + // Wait if specified before returning. + waitForDelay(startTime, delay, mockContext); + + if (genericResource != null) { + // Prepare templating support with evaluable request and engine. + EvaluableRequest evaluableRequest = MockControllerCommons.buildEvaluableRequest(null, resourcePath, + request); + TemplateEngine engine = TemplateEngineFactory.getTemplateEngine(); + + // Return the resource as well as a 200 code. + return new ResponseEntity<>(MockControllerCommons.renderResponseContent(evaluableRequest, engine, + transformToResourceJSON(genericResource)), HttpStatus.OK); + } else { + // Return a 404 code : not found. + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + // Return a 400 code : bad request. + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + @PutMapping(value = "/{service}/{version}/{resource}/{resourceId}", produces = "application/json") + public ResponseEntity updateResource(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @PathVariable("resource") String resource, + @PathVariable("resourceId") String resourceId, @RequestParam(value = "delay", required = false) Long delay, + @RequestBody(required = true) String body, HttpServletRequest request) { + log.debug("Update resource '{}:{}' for service '{}-{}'", resource, resourceId, serviceName, version); + long startTime = System.currentTimeMillis(); + + serviceName = sanitizeServiceName(serviceName); + + MockContext mockContext = getMockContext(serviceName, version, "PUT /" + resource + "/:id"); + if (mockContext != null) { + // Get the requested generic resource. + GenericResource genericResource = genericResourceRepository.findById(resourceId).orElse(null); + if (genericResource != null) { + Document document = null; + + try { + // Try parsing body payload that should be json. + document = Document.parse(body); + document.remove(ID_FIELD); + + // Now update the generic resource payload. + genericResource.setPayload(document); + + genericResourceRepository.save(genericResource); + } catch (JsonParseException jpe) { + // Return a 422 code : unprocessable entity. + return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); + } + // Wait if specified before returning. + waitForDelay(startTime, delay, mockContext); + + // Return the updated resource as well as a 200 code. + return new ResponseEntity<>(transformToResourceJSON(genericResource), HttpStatus.OK); + + } else { + // Wait if specified before returning. + waitForDelay(startTime, delay, mockContext); + + // Return a 404 code : not found. + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + // Return a 400 code : bad request. + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + @DeleteMapping(value = "/{service}/{version}/{resource}/{resourceId}") + public ResponseEntity deleteResource(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @PathVariable("resource") String resource, + @PathVariable("resourceId") String resourceId, @RequestParam(value = "delay", required = false) Long delay) { + log.debug("Update resource '{}:{}' for service '{}-{}'", resource, resourceId, serviceName, version); + long startTime = System.currentTimeMillis(); + + serviceName = sanitizeServiceName(serviceName); + + MockContext mockContext = getMockContext(serviceName, version, "DELETE /" + resource + "/:id"); + if (mockContext != null) { + genericResourceRepository.deleteById(resourceId); + + // Wait if specified before returning. + waitForDelay(startTime, delay, mockContext); + + // Return a 204 code : done and no content returned. + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + // Return a 400 code : bad request. + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + /** Sanitize the service name (check encoding and so on...) */ + private String sanitizeServiceName(String serviceName) { + // If serviceName was encoded with '+' instead of '%20', replace them. + if (serviceName.contains("+")) { + return serviceName.replace('+', ' '); + } + return serviceName; + } + + /** Retrieve a MockContext corresponding to operation on service. Null if not found or not valid. */ + private MockContext getMockContext(String serviceName, String version, String operationName) { + Service service = serviceRepository.findByNameAndVersion(serviceName, version); + if (service != null && ServiceType.GENERIC_REST.equals(service.getType())) { + for (Operation operation : service.getOperations()) { + if (operationName.equals(operation.getName())) { + return new MockContext(service, operation); + } + } + } + return null; + } + + private String transformToResourceJSON(GenericResource genericResource) { + Document document = genericResource.getPayload(); + document.append(ID_FIELD, genericResource.getId()); + return document.toJson(); + } + + private String formatToJSONArray(List resources) { + StringBuilder builder = new StringBuilder("["); + for (int i = 0; i < resources.size(); i++) { + builder.append(resources.get(i)); + if (i < resources.size() - 1) { + builder.append(", "); + } + } + return builder.append("]").toString(); + } + + private void waitForDelay(Long since, Long delay, MockContext mockContext) { + // Setting delay to default one if not set. + if (delay == null && mockContext.operation.getDefaultDelay() != null) { + delay = mockContext.operation.getDefaultDelay(); + } + + MockControllerCommons.waitForDelay(since, delay); + + // Publish an invocation event before returning if enabled. + if (Boolean.TRUE.equals(enableInvocationStats)) { + MockInvocationEvent event = new MockInvocationEvent(this, mockContext.service.getName(), + mockContext.service.getVersion(), "DynamicMockRestController", new Date(since), + since - System.currentTimeMillis()); + applicationContext.publishEvent(event); + log.debug("Mock invocation event has been published"); + } + } + + private class MockContext { + public Service service; + public Operation operation; + + public MockContext(Service service, Operation operation) { + this.service = service; + this.operation = operation; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ExportController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ExportController.java new file mode 100644 index 000000000..6fc54e5e5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ExportController.java @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.service.ImportExportService; +import io.github.microcks.util.SafeLogger; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * A Controller for getting an export of microcks repository. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/api") +public class ExportController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(ExportController.class); + + private final ImportExportService importExportService; + + /** + * Build a new ExportController with its dependencies. + * @param importExportService to have access to export service + */ + public ExportController(ImportExportService importExportService) { + this.importExportService = importExportService; + } + + @GetMapping(value = "/export") + public ResponseEntity exportRepository(@RequestParam(value = "serviceIds") List serviceIds) { + log.debug("Extracting export for serviceIds {}", serviceIds); + String json = importExportService.exportRepository(serviceIds, "json"); + + byte[] body = json.getBytes(); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.APPLICATION_JSON); + responseHeaders.set("Content-Disposition", "attachment; filename=microcks-repository.json"); + responseHeaders.setContentLength(body.length); + + return new ResponseEntity<>(body, responseHeaders, HttpStatus.OK); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/FeaturesConfigController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/FeaturesConfigController.java new file mode 100644 index 000000000..0d368a64d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/FeaturesConfigController.java @@ -0,0 +1,53 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * A Rest controller for getting optional and additional features configuration as JSON. + * @author laurent + */ +@RestController +@RequestMapping("/api/features") +@PropertySource("features.properties") +@PropertySource(value = "file:/deployments/config/features.properties", ignoreResourceNotFound = true) +@ConfigurationProperties("features") +public class FeaturesConfigController { + + private Map> feature; + + public Map> getFeature() { + return feature; + } + + public void setFeature(Map> feature) { + this.feature = feature; + } + + @GetMapping(value = "/config") + public ResponseEntity getConfig() { + return new ResponseEntity<>(feature, HttpStatus.OK); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GenericResourceController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GenericResourceController.java new file mode 100644 index 000000000..633256e9f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GenericResourceController.java @@ -0,0 +1,82 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.GenericResource; +import io.github.microcks.repository.GenericResourceRepository; +import io.github.microcks.util.SafeLogger; + +import org.bson.Document; +import org.springframework.data.domain.PageRequest; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A controller that expose APIs for browsing Generic resources. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/api") +public class GenericResourceController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(GenericResourceController.class); + + public static final String ID_FIELD = "id"; + + private final GenericResourceRepository genericResourceRepository; + + /** + * Build a new GenericResourceController with its dependencies. + * @param genericResourceRepository to have access to GenericResource definition + */ + public GenericResourceController(GenericResourceRepository genericResourceRepository) { + this.genericResourceRepository = genericResourceRepository; + } + + @GetMapping(value = "/genericresources/service/{serviceId}") + public List listResources(@PathVariable("serviceId") String serviceId, + @RequestParam(value = "page", required = false, defaultValue = "0") int page, + @RequestParam(value = "size", required = false, defaultValue = "10") int size) { + log.debug("List resources for service '{}'", serviceId); + + List genericResources = genericResourceRepository.findByServiceId(serviceId, + PageRequest.of(page, size)); + // Transform and collect resources. + return genericResources.stream().map(this::addIdToPayload).toList(); + } + + @GetMapping(value = "/genericresources/service/{serviceId}/count") + public Map countResources(@PathVariable("serviceId") String serviceId) { + log.debug("Counting resources for service '{}'", serviceId); + + Map counter = new HashMap<>(); + counter.put("counter", genericResourceRepository.countByServiceId(serviceId)); + return counter; + } + + private GenericResource addIdToPayload(GenericResource genericResource) { + Document document = genericResource.getPayload(); + document.append(ID_FIELD, genericResource.getId()); + return genericResource; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GraphQLController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GraphQLController.java new file mode 100644 index 000000000..b7324c3fa --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GraphQLController.java @@ -0,0 +1,487 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.util.ParameterConstraintUtil; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.graphql.GraphQLHttpRequest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.language.Argument; +import graphql.language.Definition; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.FragmentDefinition; +import graphql.language.FragmentSpread; +import graphql.language.OperationDefinition; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import graphql.language.StringValue; +import graphql.language.VariableReference; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import graphql.parser.Parser; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import jakarta.servlet.http.HttpServletRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A controller for mocking GraphQL responses. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/graphql") +public class GraphQLController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(GraphQLController.class); + + private static final String INTROSPECTION_SELECTION = "__schema"; + private static final String TYPENAME_SELECTION = "__typename"; + private static final Set IGNORED_HEADERS = Set.of("transfer-encoding", "content-length"); + + private final ServiceRepository serviceRepository; + private final ResourceRepository resourceRepository; + private final GraphQLInvocationProcessor invocationProcessor; + + private final Parser requestParser = new Parser(); + private final SchemaParser schemaParser = new SchemaParser(); + private final SchemaGenerator schemaGenerator = new SchemaGenerator(); + private final ObjectMapper mapper = new ObjectMapper(); + + + /** + * Build a GraphQLController with required dependencies. + * @param serviceRepository The repository to access services definitions + * @param resourceRepository The repository to access resources artifacts + * @param invocationProcessor The invocation processor to use for processing the call + */ + public GraphQLController(ServiceRepository serviceRepository, ResourceRepository resourceRepository, + GraphQLInvocationProcessor invocationProcessor) { + this.serviceRepository = serviceRepository; + this.resourceRepository = resourceRepository; + this.invocationProcessor = invocationProcessor; + } + + + @RequestMapping(value = "/{service}/{version}/**", method = { RequestMethod.GET, RequestMethod.POST }) + public ResponseEntity execute(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @RequestParam(value = "delay", required = false) Long delay, + @RequestBody(required = false) String body, @RequestHeader HttpHeaders headers, HttpServletRequest request, + HttpMethod method) { + + log.info("Servicing GraphQL mock response for service [{}, {}]", serviceName, version); + log.debug("Request body: {}", body); + + long startTime = System.currentTimeMillis(); + + // If serviceName was encoded with '+' instead of '%20', remove them. + if (serviceName.contains("+")) { + serviceName = serviceName.replace('+', ' '); + } + + Service service = serviceRepository.findByNameAndVersion(serviceName, version); + if (service == null) { + return new ResponseEntity<>( + String.format("The service %s with version %s does not exist!", serviceName, version), + HttpStatus.NOT_FOUND); + } + + GraphQLHttpRequest graphqlHttpReq; + Document graphqlRequest; + try { + graphqlHttpReq = GraphQLHttpRequest.from(body, request); + graphqlRequest = requestParser.parseDocument(graphqlHttpReq.getQuery()); + } catch (Exception e) { + log.error("Error parsing GraphQL request: {}", e.getMessage()); + return new ResponseEntity<>("Error parsing GraphQL request: " + e.getMessage(), HttpStatus.BAD_REQUEST); + } + + Definition graphqlDef = graphqlRequest.getDefinitions().get(0); + OperationDefinition graphqlOperation = (OperationDefinition) graphqlDef; + + log.debug("Got this graphqlOperation: {}", graphqlOperation); + + // Operation type is direct but name depends on syntax (it can be a composite one). Better to use the names of selections... + String operationType = graphqlOperation.getOperation().toString(); + + // Check is it's an introspection query to handle first. + if ("QUERY".equals(operationType) && INTROSPECTION_SELECTION + .equals(((Field) graphqlOperation.getSelectionSet().getSelections().get(0)).getName())) { + log.info("Handling GraphQL schema introspection query..."); + Resource graphqlSchema = resourceRepository + .findByServiceIdAndType(service.getId(), ResourceType.GRAPHQL_SCHEMA).get(0); + + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(graphqlSchema.getContent()); + GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, + RuntimeWiring.MOCKED_WIRING); + + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); + ExecutionResult executionResult = graphQL.execute(graphqlHttpReq.getQuery()); + + String responseContent = null; + try { + responseContent = mapper.writeValueAsString(executionResult); + } catch (JsonProcessingException jpe) { + log.error("Unknown Json processing exception", jpe); + return new ResponseEntity<>(jpe.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + return new ResponseEntity<>(responseContent, HttpStatus.OK); + } + + // Then deal with one or many regular GraphQL selection queries. + List graphqlResponses = new ArrayList<>(); + Long specifiedDelay = MockControllerCommons.getDelay(headers, delay); + Long maxDelay = specifiedDelay == null ? 0L : specifiedDelay; + + for (Selection selection : graphqlOperation.getSelectionSet().getSelections()) { + try { + GraphQLQueryResponse graphqlResponse = processGraphQLQuery(service, operationType, (Field) selection, + graphqlRequest.getDefinitionsOfType(FragmentDefinition.class), body, graphqlHttpReq, headers, + request); + + // if (graphqlResponse.getProxyUrl() != null) { + // // If we've got a proxyUrl, that's the moment! + // return proxyService.callExternal(graphqlResponse.getProxyUrl(), method, headers, body); + // } + + graphqlResponses.add(graphqlResponse); + if (specifiedDelay == null && graphqlResponse.getOperationDelay() != null + && graphqlResponse.getOperationDelay() > maxDelay) { + maxDelay = graphqlResponse.getOperationDelay(); + } + } catch (GraphQLQueryProcessingException e) { + log.error("Caught a GraphQL processing exception", e); + return new ResponseEntity<>(e.getMessage(), e.getStatus()); + } + } + + /* + * Optimized parallel version but need to better handle exception. graphqlResponses = + * graphqlOperation.getSelectionSet().getSelections().stream().parallel().map(selection -> { try { + * GraphQLQueryResponse graphqlResponse = processGraphQLQuery(service, operationType, (Field) selection, + * graphqlRequest.getDefinitionsOfType(FragmentDefinition.class), body, graphqlHttpReq, request); if + * (graphqlResponse.getOperationDelay() != null && graphqlResponse.getOperationDelay() > maxDelay[0]) { + * maxDelay[0] = graphqlResponse.getOperationDelay(); } return graphqlResponse; } catch + * (GraphQLQueryProcessingException e) { log.error("Caught a GraphQL processing exception", e); return null; } + * }).collect(Collectors.toList()); + */ + + // Deal with response headers. + HttpHeaders responseHeaders = new HttpHeaders(); + for (GraphQLQueryResponse response : graphqlResponses) { + if (response.getResponse() != null && response.getResponse().getHeaders() != null) { + for (Header header : response.getResponse().getHeaders()) { + if (!IGNORED_HEADERS.contains(header.getName().toLowerCase())) { + responseHeaders.put(header.getName(), new ArrayList<>(header.getValues())); + } + } + } + } + if (!responseHeaders.containsKey("Content-Type") && !responseHeaders.containsKey("content-type")) { + responseHeaders.put("Content-Type", List.of("application/json")); + } + + // Waiting for delay if any. + MockControllerCommons.waitForDelay(startTime, maxDelay); + + String responseContent = null; + JsonNode responseNode = graphqlResponses.get(0).getJsonResponse(); + + // Aggregate GraphQL query responses into a unified response object. + // Setting each response under its alias (or operation name if no alias is provided), + // ensures that aliasing applies consistently for both multi and single queries, matching actual + // GraphQL behavior. + ObjectNode aggregated = mapper.createObjectNode(); + ObjectNode dataNode = aggregated.putObject("data"); + for (GraphQLQueryResponse response : graphqlResponses) { + dataNode.set(StringUtils.defaultIfBlank(response.getAlias(), response.getOperationName()), + response.getJsonResponse().path("data").path(response.getOperationName()).deepCopy()); + } + responseNode = aggregated; + try { + responseContent = mapper.writeValueAsString(responseNode); + } catch (JsonProcessingException e) { + log.error("Unknown Json processing exception", e); + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + return new ResponseEntity<>(responseContent, responseHeaders, HttpStatus.OK); + } + + /** + * Process a GraphQL field selection query (an Http query may contain many field selection queries). + * @param service The Service this query is targeting + * @param operationType The type of GraphQL operation (QUERY or MUTATION) + * @param graphqlField The Field selection we should apply + * @param fragmentDefinitions A list of fragment field selection + * @param body The Http request body + * @param graphqlHttpReq The Http GraphQL request wrapper + * @param headers The header of the incoming Http request + * @param request The bare Http Servlet request + * @return A GraphQL query response wrapper with some elements from the Microcks domain matching Response + * @throws GraphQLQueryProcessingException if incoming field selection query cannot be processed + */ + protected GraphQLQueryResponse processGraphQLQuery(Service service, String operationType, Field graphqlField, + List fragmentDefinitions, String body, GraphQLHttpRequest graphqlHttpReq, + HttpHeaders headers, HttpServletRequest request) throws GraphQLQueryProcessingException { + + GraphQLQueryResponse result = new GraphQLQueryResponse(); + String operationName = graphqlField.getName(); + + result.setAlias(graphqlField.getAlias()); + result.setOperationName(operationName); + + log.debug("Processing a '{}' operation with name '{}'", operationType, operationName); + + if (TYPENAME_SELECTION.equals(operationName)) { + log.debug("Handling GraphQL __typename query..."); + ObjectNode typenameResponse = mapper.createObjectNode(); + ObjectNode dataNode = typenameResponse.putObject("data"); + dataNode.put(TYPENAME_SELECTION, "QUERY".equalsIgnoreCase(operationType) ? "Query" : "Mutation"); + result.setOperationName(TYPENAME_SELECTION); + result.setJsonResponse(typenameResponse); + return result; + } + + Operation rOperation = null; + for (Operation operation : service.getOperations()) { + // Select operation based on type (QUERY or MUTATION)... + // ... then check the operation name itself. + if (operation.getMethod().equals(operationType) && operation.getName().equals(operationName)) { + rOperation = operation; + break; + } + } + + if (rOperation != null) { + log.debug("Found a valid operation {} with rules: {}", rOperation.getName(), rOperation.getDispatcherRules()); + String violationMsg = validateParameterConstraintsIfAny(rOperation, request); + if (violationMsg != null) { + throw new GraphQLQueryProcessingException(violationMsg + ". Check parameter constraints.", + HttpStatus.BAD_REQUEST); + } + + // We must compute query parameters from the field selection. + Map queryParams = new HashMap<>(); + for (Argument arg : graphqlField.getArguments()) { + if (arg.getValue() instanceof StringValue stringArg) { + queryParams.put(arg.getName(), stringArg.getValue()); + } else if (arg.getValue() instanceof VariableReference varRef && graphqlHttpReq.getVariables() != null) { + queryParams.put(arg.getName(), graphqlHttpReq.getVariables().path(varRef.getName()).asText("")); + } + } + + // Create an invocation context for the invocation processor. + MockInvocationContext ic = new MockInvocationContext(service, rOperation, null); + ResponseResult responseResult = invocationProcessor.processInvocation(ic, System.currentTimeMillis(), + queryParams, body, graphqlHttpReq, headers, request); + + // Complete GraphQLQueryResponse result. + result.setOperationDelay(rOperation.getDefaultDelay()); + + if (responseResult.content() != null) { + try { + JsonNode responseJson = mapper.readTree(responseResult.content()); + filterFieldSelection(graphqlField.getSelectionSet(), fragmentDefinitions, + responseJson.get("data").get(operationName)); + result.setJsonResponse(responseJson); + } catch (Exception pe) { + log.error("Exception while filtering response according GraphQL field selection", pe); + throw new GraphQLQueryProcessingException("Exception while filtering response JSON", + HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + if (HttpStatus.BAD_REQUEST.equals(responseResult.status())) { + log.debug("No response found. Throwing a BAD_REQUEST exception..."); + throw new GraphQLQueryProcessingException("No matching response found", HttpStatus.BAD_REQUEST); + } + + return result; + } + log.debug("No valid operation found. Throwing a NOT_FOUND exception..."); + throw new GraphQLQueryProcessingException("No '" + operationName + "' operation found", HttpStatus.NOT_FOUND); + } + + /** Validate the parameter constraints and return a single string with violation message if any. */ + private String validateParameterConstraintsIfAny(Operation rOperation, HttpServletRequest request) { + if (rOperation.getParameterConstraints() != null) { + for (ParameterConstraint constraint : rOperation.getParameterConstraints()) { + String violationMsg = ParameterConstraintUtil.validateConstraint(request, constraint); + if (violationMsg != null) { + return violationMsg; + } + } + } + return null; + } + + /** + * Apply a FieldSelection filter on Json node. + * @param selectionSet The set of selections to apply + * @param fragmentDefinitions A list of fragment field selection + * @param node The Json node to apply on + */ + protected void filterFieldSelection(SelectionSet selectionSet, List fragmentDefinitions, + JsonNode node) { + // Stop condition: no more selection to apply. + if (selectionSet == null || selectionSet.getSelections() == null || selectionSet.getSelections().isEmpty()) { + return; + } + switch (node.getNodeType()) { + case OBJECT: + // We must retain properties corresponding to field selection + // and recurse on each retrained object property. + List properties = new ArrayList<>(); + for (Selection selection : selectionSet.getSelections()) { + if (selection instanceof Field fieldSelection) { + filterFieldSelection(fieldSelection.getSelectionSet(), fragmentDefinitions, + node.get(fieldSelection.getName())); + properties.add(fieldSelection.getName()); + } else if (selection instanceof FragmentSpread fragmentSpread) { + // FragmentSpread is an indirection to selection find in definitions. + FragmentDefinition fragmentDef = fragmentDefinitions.stream() + .filter(def -> def.getName().equals(fragmentSpread.getName())).findFirst().orElse(null); + if (fragmentDef != null) { + filterFieldSelection(fragmentDef.getSelectionSet(), fragmentDefinitions, node); + } + } + } + // Only filter if properties to retain. + if (!properties.isEmpty()) { + ((ObjectNode) node).retain(properties); + } + break; + case ARRAY: + // We must apply selection on each element of the array. + Iterator children = node.elements(); + while (children.hasNext()) { + filterFieldSelection(selectionSet, fragmentDefinitions, children.next()); + } + break; + default: + break; + } + } + + /** Simple wrapper around a GraphQL query response. */ + protected class GraphQLQueryResponse { + String operationName; + String alias; + Long operationDelay; + Response response; + JsonNode jsonResponse; + URI proxyUrl; + + public String getOperationName() { + return operationName; + } + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public Long getOperationDelay() { + return operationDelay; + } + + public void setOperationDelay(Long operationDelay) { + this.operationDelay = operationDelay; + } + + public JsonNode getJsonResponse() { + return jsonResponse; + } + + public void setJsonResponse(JsonNode jsonResponse) { + this.jsonResponse = jsonResponse; + } + + public Response getResponse() { + return response; + } + + public void setResponse(Response response) { + this.response = response; + } + + public URI getProxyUrl() { + return proxyUrl; + } + + public void setProxyUrl(URI proxyUrl) { + this.proxyUrl = proxyUrl; + } + } + + /** Simple exception wrapping a processing error. */ + protected static class GraphQLQueryProcessingException extends Exception { + + final HttpStatus status; + + public HttpStatus getStatus() { + return status; + } + + public GraphQLQueryProcessingException(String message, HttpStatus status) { + super(message); + this.status = status; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GraphQLInvocationProcessor.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GraphQLInvocationProcessor.java new file mode 100644 index 000000000..548ad1325 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GraphQLInvocationProcessor.java @@ -0,0 +1,282 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.ServiceStateRepository; +import io.github.microcks.service.ProxyService; +import io.github.microcks.service.ServiceStateStore; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.dispatcher.FallbackSpecification; +import io.github.microcks.util.dispatcher.JsonEvaluationSpecification; +import io.github.microcks.util.dispatcher.JsonExpressionEvaluator; +import io.github.microcks.util.dispatcher.JsonMappingException; +import io.github.microcks.util.dispatcher.ProxyFallbackSpecification; +import io.github.microcks.util.graphql.GraphQLHttpRequest; +import io.github.microcks.util.script.ScriptEngineBinder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * A processor for handling GraphQL invocation requests. Is it responsible for applying dispatching logic and finding + * the most appropriate response to return based on the request context. + * @author laurent + */ +@Component +public class GraphQLInvocationProcessor { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(GraphQLInvocationProcessor.class); + + private final ServiceStateRepository serviceStateRepository; + private final ResponseRepository responseRepository; + private final ApplicationContext applicationContext; + private final ProxyService proxyService; + private final ObjectMapper mapper = new ObjectMapper(); + + private ScriptEngine scriptEngine; + + @Value("${mocks.enable-invocation-stats}") + private Boolean enableInvocationStats; + + /** + * Build a GraphQLInvocationProcessor with required dependencies. + * @param serviceStateRepository The repository to access service state + * @param responseRepository The repository to access responses definitions + * @param applicationContext The Spring application context + * @param proxyService The proxy to external URLs or services + */ + public GraphQLInvocationProcessor(ServiceStateRepository serviceStateRepository, + ResponseRepository responseRepository, ApplicationContext applicationContext, ProxyService proxyService) { + this.serviceStateRepository = serviceStateRepository; + this.responseRepository = responseRepository; + this.applicationContext = applicationContext; + this.proxyService = proxyService; + this.scriptEngine = new ScriptEngineManager().getEngineByExtension("groovy"); + } + + /** + * Process a GraphQL invocation. This method is responsible for determining the appropriate response based on the + * request context, applying any necessary dispatching logic, and handling proxying if required. + * @param ic The invocation context containing information about the service and operation being invoked + * @param startTime The start time of the invocation + * @param body The request body + * @param graphqlHttpReq The GraphQL HTTP request wrapper + * @param headers The HTTP headers of the request + * @param request The HTTP servlet request + * @return A ResponseResult containing the status, headers, and body of the response + */ + public ResponseResult processInvocation(MockInvocationContext ic, long startTime, Map queryParams, + String body, GraphQLHttpRequest graphqlHttpReq, Map> headers, + HttpServletRequest request) { + // We must find dispatcher and its rules. Default to operation ones but + // if we have a Fallback or Proxy-Fallback this is the one who is holding the first pass rules. + FallbackSpecification fallback = MockControllerCommons.getFallbackIfAny(ic.operation()); + ProxyFallbackSpecification proxyFallback = MockControllerCommons.getProxyFallbackIfAny(ic.operation()); + String dispatcher = getDispatcher(ic, fallback, proxyFallback); + String dispatcherRules = getDispatcherRules(ic, fallback, proxyFallback); + + // + DispatchContext dispatchContext = computeDispatchCriteria(ic.service(), dispatcher, dispatcherRules, queryParams, + graphqlHttpReq.getVariables(), request, body); + log.debug("Dispatch criteria for finding response is {}", dispatchContext.dispatchCriteria()); + + // First try: using computed dispatchCriteria on main dispatcher. + Response response = null; + List responses = responseRepository.findByOperationIdAndDispatchCriteria( + IdBuilder.buildOperationId(ic.service(), ic.operation()), dispatchContext.dispatchCriteria()); + if (!responses.isEmpty()) { + response = responses.getFirst(); + } + + if (response == null) { + // When using the SCRIPT or JSON_BODY dispatcher, return of evaluation may be the name of response. + log.debug("No responses with dispatch criteria, trying the name..."); + responses = responseRepository.findByOperationIdAndName( + IdBuilder.buildOperationId(ic.service(), ic.operation()), dispatchContext.dispatchCriteria()); + if (!responses.isEmpty()) { + response = responses.getFirst(); + } + } + + if (response == null && fallback != null) { + // If we've found nothing and got a fallback, that's the moment! + log.debug("No responses till now so far, applying the fallback..."); + responses = responseRepository.findByOperationIdAndName( + IdBuilder.buildOperationId(ic.service(), ic.operation()), fallback.getFallback()); + if (!responses.isEmpty()) { + response = responses.getFirst(); + } + } + + // Check if we need to proxy the request. + Optional proxyUrl = MockControllerCommons.getProxyUrlIfProxyIsNeeded(dispatcher, dispatcherRules, "", + proxyFallback, request, response); + if (proxyUrl.isPresent()) { + // Translate generic headers into Spring ones. + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.putAll(headers); + + // If we've got a proxyUrl, that's the moment to tell about it! + ResponseEntity proxyResponse = proxyService.callExternal(proxyUrl.get(), + HttpMethod.valueOf(request.getMethod()), httpHeaders, body); + return new ResponseResult(proxyResponse.getStatusCode(), proxyResponse.getHeaders(), proxyResponse.getBody()); + } + + if (response == null) { + if (dispatcher == null) { + // In case no response found (because dispatcher is null for example), just get one for the operation. + // This will allow also OPTIONS operations (like pre-flight requests) with no dispatch criteria to work. + log.debug("No responses found so far, tempting with just bare operationId..."); + responses = responseRepository.findByOperationId(IdBuilder.buildOperationId(ic.service(), ic.operation())); + if (!responses.isEmpty()) { + response = responses.getFirst(); + } + } + } + + if (response != null) { + HttpStatus status = (response.getStatus() != null ? HttpStatus.valueOf(Integer.parseInt(response.getStatus())) + : HttpStatus.OK); + + // Prepare headers for evaluation. + Map evaluableHeaders = new HashMap<>(); + if (response.getHeaders() != null) { + for (Header header : response.getHeaders()) { + evaluableHeaders.put(header.getName(), request.getHeader(header.getName())); + } + } + + // Render response content before waiting and returning. + String responseContent = MockControllerCommons.renderResponseContent(body, null, evaluableHeaders, + dispatchContext.requestContext(), response); + + + // Evaluate headers and add them to responseHeaders. + HttpHeaders responseHeaders = new HttpHeaders(); + if (response.getHeaders() != null) { + response.getHeaders().stream() + .filter(header -> !HttpHeaders.TRANSFER_ENCODING.equalsIgnoreCase(header.getName())) + .forEach(header -> responseHeaders.put(header.getName(), new ArrayList<>(header.getValues()))); + } + + // Publish an invocation event before returning if enabled. + if (Boolean.TRUE.equals(enableInvocationStats)) { + MockControllerCommons.publishMockInvocation(applicationContext, this, ic.service(), response, startTime); + } + + // Return response content. + return new ResponseResult(status, responseHeaders, + responseContent != null ? responseContent.getBytes(StandardCharsets.UTF_8) : null); + } + + // e found no response => return 400 as per #819 and #1132. + return new ResponseResult(HttpStatus.BAD_REQUEST, null, null); + } + + /** Compute a dispatch context with a dispatchCriteria string from type, rules and request elements. */ + private DispatchContext computeDispatchCriteria(Service service, String dispatcher, String dispatcherRules, + Map queryParams, JsonNode requestVariables, HttpServletRequest request, String body) { + String dispatchCriteria = null; + Map requestContext = null; + + // Depending on dispatcher, evaluate request with rules. + if (dispatcher != null) { + switch (dispatcher) { + case DispatchStyles.QUERY_ARGS: + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, queryParams); + break; + case DispatchStyles.JSON_BODY: + try { + JsonEvaluationSpecification specification = JsonEvaluationSpecification + .buildFromJsonString(dispatcherRules); + dispatchCriteria = JsonExpressionEvaluator.evaluate(mapper.writeValueAsString(requestVariables), + specification); + } catch (JsonMappingException jme) { + log.error("Dispatching rules of operation cannot be interpreted as JsonEvaluationSpecification", jme); + } catch (JsonProcessingException jpe) { + log.error("Request variables cannot be serialized as Json for evaluation", jpe); + } + break; + case DispatchStyles.SCRIPT: + requestContext = new HashMap<>(); + try { + // Evaluating request with script coming from operation dispatcher rules. + ScriptContext scriptContext = ScriptEngineBinder.buildEvaluationContext(scriptEngine, body, + requestContext, new ServiceStateStore(serviceStateRepository, service.getId()), request); + dispatchCriteria = (String) scriptEngine.eval(dispatcherRules, scriptContext); + } catch (Exception e) { + log.error("Error during Script evaluation", e); + } + break; + } + } + return new DispatchContext(dispatchCriteria, requestContext); + } + + /** Get the root dispatcher for the invocation context. */ + private String getDispatcher(MockInvocationContext ic, FallbackSpecification fallback, + ProxyFallbackSpecification proxyFallback) { + String dispatcher = ic.operation().getDispatcher(); + if (fallback != null) { + dispatcher = fallback.getDispatcher(); + } + if (proxyFallback != null) { + dispatcher = proxyFallback.getDispatcher(); + } + return dispatcher; + } + + /** Get the root dispatcher rules for the invocation context. */ + private String getDispatcherRules(MockInvocationContext ic, FallbackSpecification fallback, + ProxyFallbackSpecification proxyFallback) { + String dispatcherRules = ic.operation().getDispatcherRules(); + if (fallback != null) { + dispatcherRules = fallback.getDispatcherRules(); + } + if (proxyFallback != null) { + dispatcherRules = proxyFallback.getDispatcherRules(); + } + return dispatcherRules; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcInvocationProcessor.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcInvocationProcessor.java new file mode 100644 index 000000000..320638ca9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcInvocationProcessor.java @@ -0,0 +1,246 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.ServiceStateRepository; +import io.github.microcks.service.ServiceStateStore; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.dispatcher.FallbackSpecification; +import io.github.microcks.util.dispatcher.JsonEvaluationSpecification; +import io.github.microcks.util.dispatcher.JsonExpressionEvaluator; +import io.github.microcks.util.dispatcher.JsonMappingException; +import io.github.microcks.util.grpc.GrpcMetadataUtil; +import io.github.microcks.util.script.ScriptEngineBinder; +import io.github.microcks.util.script.StringToStringsMap; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import io.grpc.Metadata; +import io.grpc.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * A processor for handling gRPC invocations. It is responsible for applying the dispatching logic and finding the most + * appropriate response based on the request context. + * @author laurent + */ +@Component +public class GrpcInvocationProcessor { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(GrpcInvocationProcessor.class); + + private final ServiceStateRepository serviceStateRepository; + private final ResponseRepository responseRepository; + private final ApplicationContext applicationContext; + private final ObjectMapper mapper = new ObjectMapper(); + + private ScriptEngine scriptEngine; + + @Value("${mocks.enable-invocation-stats}") + private Boolean enableInvocationStats; + + /** + * Build a GrpcInvocationProcessor with required dependencies. + * @param serviceStateRepository The repository to access service state + * @param responseRepository The repository to access responses definitions + * @param applicationContext The Spring application context + */ + public GrpcInvocationProcessor(ServiceStateRepository serviceStateRepository, ResponseRepository responseRepository, + ApplicationContext applicationContext) { + this.serviceStateRepository = serviceStateRepository; + this.responseRepository = responseRepository; + this.applicationContext = applicationContext; + this.scriptEngine = new ScriptEngineManager().getEngineByExtension("groovy"); + } + + /** + * Process a gRPC invocation. This method is responsible for determining the appropriate response based on the + * request context, applying any necessary dispatching logic. + * @param ic The invocation context containing information about the service and operation being invoked + * @param startTime The start time of the invocation + * @param jsonBody The request body expressed in JSON + * @return A GrcpResponseResult containing the status, body or exception message of the response + */ + public GrpcResponseResult processInvocation(MockInvocationContext ic, long startTime, String jsonBody) { + // We must find dispatcher and its rules. Default to operation ones but + // if we have a Fallback this is the one who is holding the first pass rules. + String dispatcher = ic.operation().getDispatcher(); + String dispatcherRules = ic.operation().getDispatcherRules(); + FallbackSpecification fallback = MockControllerCommons.getFallbackIfAny(ic.operation()); + if (fallback != null) { + dispatcher = fallback.getDispatcher(); + dispatcherRules = fallback.getDispatcherRules(); + } + + // + Metadata metadata = GrpcMetadataUtil.METADATA_CTX_KEY.get(); + DispatchContext dispatchContext = computeDispatchCriteria(ic.service(), dispatcher, dispatcherRules, jsonBody, + metadata); + log.debug("Dispatch criteria for finding response is {}", dispatchContext.dispatchCriteria()); + + // Trying to retrieve the responses with context elements. + List responses = findCandidateResponses(ic.service(), ic.operation(), dispatchContext, fallback); + + // No filter to apply, just check that we have a response. + if (!responses.isEmpty()) { + Response response = responses.getFirst(); + + // Render response content before. + String responseContent = MockControllerCommons.renderResponseContent(jsonBody, + dispatchContext.requestContext(), response); + + // Setting delay to default one if not set. + if (ic.operation().getDefaultDelay() != null) { + MockControllerCommons.waitForDelay(startTime, ic.operation().getDefaultDelay()); + } + + // Publish an invocation event before returning if enabled. + if (Boolean.TRUE.equals(enableInvocationStats)) { + MockControllerCommons.publishMockInvocation(applicationContext, this, ic.service(), response, startTime); + } + + // Return a GrpcResponseResult with the response content. + if (response.getStatus() == null || response.getStatus().trim().equals("0") + || statusInHttpRange(response.getStatus())) { + return new GrpcResponseResult(Status.OK, responseContent, null); + } + + // Or, return an error status. + return new GrpcResponseResult(getSafeErrorStatus(response.getStatus()), null, "Mocked response status code"); + } + + // No response found. + log.info("No appropriate response found for this input {}, returning an error", jsonBody); + return new GrpcResponseResult(Status.NOT_FOUND, null, "No response found for the GRPC input request"); + } + + /** Compute a dispatch context with a dispatchCriteria string from type, rules and request elements. */ + private DispatchContext computeDispatchCriteria(Service service, String dispatcher, String dispatcherRules, + String jsonBody, Metadata metadata) { + String dispatchCriteria = null; + Map requestContext = null; + + // Depending on dispatcher, evaluate request with rules. + if (dispatcher != null) { + switch (dispatcher) { + case DispatchStyles.QUERY_ARGS: + try { + Map paramsMap = mapper.readValue(jsonBody, + TypeFactory.defaultInstance().constructMapType(TreeMap.class, String.class, String.class)); + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, paramsMap); + } catch (JsonProcessingException jpe) { + log.error("Incoming body cannot be parsed as JSON", jpe); + } + break; + case DispatchStyles.JSON_BODY: + try { + JsonEvaluationSpecification specification = JsonEvaluationSpecification + .buildFromJsonString(dispatcherRules); + dispatchCriteria = JsonExpressionEvaluator.evaluate(jsonBody, specification); + } catch (JsonMappingException jme) { + log.error("Dispatching rules of operation cannot be interpreted as JsonEvaluationSpecification", jme); + } + break; + case DispatchStyles.SCRIPT: + requestContext = new HashMap<>(); + try { + StringToStringsMap headers = GrpcMetadataUtil.convertToMap(metadata); + // Evaluating request with script coming from operation dispatcher rules. + ScriptContext scriptContext = ScriptEngineBinder.buildEvaluationContext(scriptEngine, jsonBody, + requestContext, new ServiceStateStore(serviceStateRepository, service.getId()), headers, null); + dispatchCriteria = (String) scriptEngine.eval(dispatcherRules, scriptContext); + } catch (Exception e) { + log.error("Error during Script evaluation", e); + } + break; + default: + break; + } + } + return new DispatchContext(dispatchCriteria, requestContext); + } + + /** Find suitable responses regarding dispatch context and criteria. */ + private List findCandidateResponses(Service service, Operation grpcOperation, + DispatchContext dispatchContext, FallbackSpecification fallback) { + // Trying to retrieve the responses with dispatch criteria. + List responses = responseRepository.findByOperationIdAndDispatchCriteria( + IdBuilder.buildOperationId(service, grpcOperation), dispatchContext.dispatchCriteria()); + + if (responses.isEmpty()) { + // When using the SCRIPT or JSON_BODY dispatchers, return of evaluation may be the name of response. + responses = responseRepository.findByOperationIdAndName(IdBuilder.buildOperationId(service, grpcOperation), + dispatchContext.dispatchCriteria()); + } + + if (responses.isEmpty() && fallback != null) { + // If we've found nothing and got a fallback, that's the moment! + responses = responseRepository.findByOperationIdAndName(IdBuilder.buildOperationId(service, grpcOperation), + fallback.getFallback()); + } + + if (responses.isEmpty()) { + // In case no response found (because dispatcher is null for example), just get one for the operation. + log.debug("No responses found so far, tempting with just bare operationId..."); + responses = responseRepository.findByOperationId(IdBuilder.buildOperationId(service, grpcOperation)); + } + return responses; + } + + /** Return true if status is in HTTP status range. */ + private boolean statusInHttpRange(String status) { + try { + int statusCode = Integer.parseInt(status); + if (statusCode >= 200 && statusCode < 600) { + log.warn("Response status code in incorrectly in HTTP code range: {}", status); + return true; + } + } catch (NumberFormatException nfe) { + log.warn("Invalid status code in response: {}", status); + } + return false; + } + + /** Return a safe gRPC Status from a string. */ + private Status getSafeErrorStatus(String status) { + try { + return Status.fromCodeValue(Integer.parseInt(status)); + } catch (NumberFormatException nfe) { + log.warn("Invalid status code in response: {}, using UNKNOWN", status); + return Status.UNKNOWN; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcResponseResult.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcResponseResult.java new file mode 100644 index 000000000..db6fbb49e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcResponseResult.java @@ -0,0 +1,30 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.grpc.Status; + +/** + * A thin wrapper around a gRPC response result. + * @param status The HTTP status code + * @param content The optional content of the response + * @param errorDescription The optional error description + */ +public record GrpcResponseResult(Status status, String content, String errorDescription) { + public boolean isError() { + return status.getCode() != Status.Code.OK; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcServerCallHandler.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcServerCallHandler.java new file mode 100644 index 000000000..a250672d0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/GrpcServerCallHandler.java @@ -0,0 +1,187 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.TypeRegistry; +import com.google.protobuf.util.JsonFormat; + +import io.github.microcks.util.grpc.GrpcUtil; +import io.grpc.ServerCallHandler; +import io.grpc.Status; +import io.grpc.stub.ServerCalls; +import io.grpc.stub.StreamObserver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * A Handler for GRPC Server calls invocation that is using Microcks dispatching and mock definitions. + * @author laurent + */ +@Component +public class GrpcServerCallHandler { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(GrpcServerCallHandler.class); + + private final ServiceRepository serviceRepository; + private final ResourceRepository resourceRepository; + + private final GrpcInvocationProcessor invocationProcessor; + + /** + * Build a new GrpcServerCallHandler with all the repositories it needs and application context. + * @param serviceRepository Repository for getting service definitions + * @param resourceRepository Repository for getting service resources definitions + * @param invocationProcessor The invocation processor to apply gRPC mocks dispatching logic + */ + public GrpcServerCallHandler(ServiceRepository serviceRepository, ResourceRepository resourceRepository, + GrpcInvocationProcessor invocationProcessor) { + this.serviceRepository = serviceRepository; + this.resourceRepository = resourceRepository; + this.invocationProcessor = invocationProcessor; + } + + /** + * Create an ServerCallHandler that uses Microcks mocks for unary calls. + * @param fullMethodName The GRPC method full name. + * @return A ServerCallHandler + */ + public ServerCallHandler getUnaryServerCallHandler(String fullMethodName) { + return ServerCalls.asyncUnaryCall(new MockedUnaryMethod(fullMethodName)); + } + + /** + * This internal class is handling UnaryMethod calls. It takes care of building a JSON representation from input, + * apply a dispatcher to find correct response and serialize response JSON content into binary back. + */ + protected class MockedUnaryMethod implements ServerCalls.UnaryMethod { + + private String fullMethodName; + private String serviceName; + private String serviceVersion; + private String operationName; + + /** + * Build a UnaryMethod for handling GRPC call. + * @param fullMethodName The GRPC method full identifier. + */ + public MockedUnaryMethod(String fullMethodName) { + this.fullMethodName = fullMethodName; + // Retrieve operation name, service name and version from fullMethodName. + operationName = fullMethodName.substring(fullMethodName.indexOf("/") + 1); + serviceName = fullMethodName.substring(0, fullMethodName.indexOf("/")); + String packageName = fullMethodName.substring(0, fullMethodName.lastIndexOf(".")); + String[] parts = packageName.split("\\."); + serviceVersion = (parts.length > 2 ? parts[parts.length - 1] : packageName); + } + + @Override + public void invoke(byte[] bytes, StreamObserver streamObserver) { + log.info("Servicing mock response for service [{}, {}] and method {}", serviceName, serviceVersion, + operationName); + + long startTime = System.currentTimeMillis(); + + try { + // Get service and spotted operation. + Service service = serviceRepository.findByNameAndVersion(serviceName, serviceVersion); + if (service == null) { + // No service found. + log.debug("No GRPC Service def found for [{}, {}]", serviceName, serviceVersion); + streamObserver.onError(Status.UNIMPLEMENTED + .withDescription("No GRPC Service def found for " + fullMethodName).asException()); + return; + } + Operation grpcOperation = null; + for (Operation operation : service.getOperations()) { + if (operation.getName().equals(operationName)) { + grpcOperation = operation; + break; + } + } + + if (grpcOperation != null) { + log.debug("Found a valid operation {} with rules: {}", grpcOperation.getName(), + grpcOperation.getDispatcherRules()); + + // In order to inspect incoming byte array, we need the Protobuf binary descriptor that should + // have been processed while importing the .proto schema for the service. + List resources = resourceRepository.findByServiceIdAndType(service.getId(), + ResourceType.PROTOBUF_DESCRIPTOR); + if (resources == null || resources.size() != 1) { + log.error("Did not found any pre-processed Protobuf binary descriptor..."); + streamObserver.onError(Status.FAILED_PRECONDITION + .withDescription("No pre-processed Protobuf binary descriptor found").asException()); + return; + } + Resource pbResource = resources.getFirst(); + + // Get the method descriptor and type registry. + Descriptors.MethodDescriptor md = GrpcUtil.findMethodDescriptor(pbResource.getContent(), serviceName, + operationName); + TypeRegistry registry = GrpcUtil.buildTypeRegistry(pbResource.getContent()); + + // Now parse the incoming message. + DynamicMessage inMsg = DynamicMessage.parseFrom(md.getInputType(), bytes); + String jsonBody = JsonFormat.printer().usingTypeRegistry(registry).print(inMsg); + log.debug("Request body: {}", jsonBody); + + MockInvocationContext ic = new MockInvocationContext(service, grpcOperation, null); + GrpcResponseResult response = invocationProcessor.processInvocation(ic, startTime, jsonBody); + + if (!response.isError()) { + // Use a builder for out type with a Json parser to merge content and build outMsg. + DynamicMessage.Builder outBuilder = DynamicMessage.newBuilder(md.getOutputType()); + + JsonFormat.parser().usingTypeRegistry(registry).merge(response.content(), outBuilder); + DynamicMessage outMsg = outBuilder.build(); + + // Send the output message and complete the stream. + streamObserver.onNext(outMsg.toByteArray()); + streamObserver.onCompleted(); + + } else { + // Error during invocation processing. Write it to the stream. + streamObserver.onError(response.status().withDescription(response.errorDescription()).asException()); + } + + } else { + // No operation found. + log.debug("No valid operation found for [{}, {}] and {}", serviceName, serviceVersion, operationName); + streamObserver.onError(Status.UNIMPLEMENTED + .withDescription("No valid operation found for " + fullMethodName).asException()); + } + } catch (Exception t) { + log.error("Unexpected throwable during GRPC input request processing", t); + streamObserver + .onError(Status.UNKNOWN.withDescription("Unexpected throwable during GRPC input request processing") + .withCause(t).asException()); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/HealthController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/HealthController.java new file mode 100644 index 000000000..2c684dad0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/HealthController.java @@ -0,0 +1,63 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.repository.ImportJobRepository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/api") +public class HealthController { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(HealthController.class); + + private final ImportJobRepository jobRepository; + + /** + * Build a new HealthController with its dependencies. + * @param jobRepository to have access to ImportJobs + */ + public HealthController(ImportJobRepository jobRepository) { + this.jobRepository = jobRepository; + } + + @GetMapping(value = "/health") + public ResponseEntity health() { + log.trace("Health check endpoint invoked"); + + try { + // Using a single selection query to ensure connection to MongoDB is ok. + jobRepository.findAll(PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "name"))).getContent(); + } catch (Exception e) { + log.error("Health check caught an exception: {}", e.getMessage(), e); + return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); + } + log.trace("Health check is OK"); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ImportController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ImportController.java new file mode 100644 index 000000000..e63799392 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ImportController.java @@ -0,0 +1,67 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.service.ImportExportService; +import io.github.microcks.util.SafeLogger; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +/** + * A Controller for importing new definitions into microcks repository. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/api") +public class ImportController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(ImportController.class); + + private final ImportExportService importExportService; + + /** + * Create new ImportController with required service. + * @param importExportService The service for managing imports. + */ + public ImportController(ImportExportService importExportService) { + this.importExportService = importExportService; + } + + @PostMapping(value = "/import") + public ResponseEntity importRepository(@RequestParam(value = "file") MultipartFile file) { + log.debug("Importing new services and resources definitions"); + if (!file.isEmpty()) { + log.debug("Content type of {} is {}", file.getOriginalFilename(), file.getContentType()); + if (MediaType.APPLICATION_JSON_VALUE.equals(file.getContentType())) { + try { + byte[] bytes = file.getBytes(); + String json = new String(bytes); + importExportService.importRepository(json); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + } + return new ResponseEntity<>(HttpStatus.CREATED); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/JobController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/JobController.java new file mode 100644 index 000000000..5736cd491 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/JobController.java @@ -0,0 +1,214 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.ImportJob; +import io.github.microcks.domain.Metadata; +import io.github.microcks.repository.ImportJobRepository; +import io.github.microcks.security.AuthorizationChecker; +import io.github.microcks.security.UserInfo; +import io.github.microcks.service.JobService; +import io.github.microcks.util.SafeLogger; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A Rest controller for API defined on importers. + * @author laurent + */ +@RestController +@RequestMapping("/api") +public class JobController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(JobController.class); + + private final ImportJobRepository jobRepository; + private final JobService jobService; + private final AuthorizationChecker authorizationChecker; + + /** + * Build a new JobController with its dependencies. + * @param jobRepository to have access to ImportJob definition + * @param jobService to have access to ImportJob service (activate, start, stop) + * @param authorizationChecker to check authorization for operations + */ + public JobController(ImportJobRepository jobRepository, JobService jobService, + AuthorizationChecker authorizationChecker) { + this.jobRepository = jobRepository; + this.jobService = jobService; + this.authorizationChecker = authorizationChecker; + } + + @GetMapping(value = "/jobs") + public List listJobs(@RequestParam(value = "page", required = false, defaultValue = "0") int page, + @RequestParam(value = "size", required = false, defaultValue = "20") int size, + @RequestParam(value = "name", required = false) String name) { + log.debug("Getting job list for page {} and size {}", page, size); + if (name != null) { + return jobRepository.findByNameLike(name); + } + return jobRepository.findAll(PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "name"))).getContent(); + } + + @GetMapping(value = "/jobs/search") + public List searchJobs(@RequestParam Map queryMap) { + // Parse params from queryMap. + String name = null; + Map labels = new HashMap<>(); + for (Map.Entry paramEntry : queryMap.entrySet()) { + String paramKey = paramEntry.getKey(); + if ("name".equals(paramKey)) { + name = paramEntry.getValue(); + } + if (paramKey.startsWith("labels.")) { + labels.put(paramKey.substring(paramKey.indexOf('.') + 1), paramEntry.getValue()); + } + } + + if (labels.isEmpty()) { + log.debug("Searching jobs corresponding to name {}", name); + return jobRepository.findByNameLike(name); + } + if (name == null || name.trim().isEmpty()) { + log.debug("Searching jobs corresponding to labels {}", labels); + return jobRepository.findByLabels(labels); + } + log.debug("Searching jobs corresponding to name {} and labels {}", name, labels); + return jobRepository.findByLabelsAndNameLike(labels, name); + } + + @GetMapping(value = "/jobs/count") + public Map countJobs() { + log.debug("Counting jobs..."); + Map counter = new HashMap<>(); + counter.put("counter", jobRepository.count()); + return counter; + } + + @PostMapping(value = "/jobs") + public ResponseEntity createJob(@RequestBody ImportJob job) { + log.debug("Creating new job: {}", job); + // Store labels somewhere before reinitializing metadata to ensure createdOn is correct. + Map labels = null; + if (job.getMetadata() != null && job.getMetadata().getLabels() != null) { + labels = job.getMetadata().getLabels(); + } + job.setMetadata(new Metadata()); + job.setCreatedDate(new Date()); + // Restore labels. + if (labels != null) { + job.getMetadata().setLabels(labels); + } + return new ResponseEntity<>(jobRepository.save(job), HttpStatus.CREATED); + } + + @GetMapping(value = "/jobs/{id}") + public ResponseEntity getJob(@PathVariable("id") String jobId) { + log.debug("Retrieving job with id {}", jobId); + return new ResponseEntity<>(jobRepository.findById(jobId).orElse(null), HttpStatus.OK); + } + + @PostMapping(value = "/jobs/{id}") + public ResponseEntity saveJob(@RequestBody ImportJob job, UserInfo userInfo) { + log.debug("Saving existing job: {}", job); + if (authorizationChecker.hasRole(userInfo, AuthorizationChecker.ROLE_ADMIN) + || authorizationChecker.hasRoleForImportJob(userInfo, AuthorizationChecker.ROLE_MANAGER, job)) { + initMetadataIfMissing(job); + job.getMetadata().objectUpdated(); + return new ResponseEntity<>(jobRepository.save(job), HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @PutMapping(value = "/jobs/{id}/activate") + public ResponseEntity activateJob(@PathVariable("id") String jobId, UserInfo userInfo) { + log.debug("Activating job with id {}", jobId); + ImportJob job = jobRepository.findById(jobId).orElse(null); + if (job != null && (authorizationChecker.hasRole(userInfo, AuthorizationChecker.ROLE_ADMIN) + || authorizationChecker.hasRoleForImportJob(userInfo, AuthorizationChecker.ROLE_MANAGER, job))) { + job.setActive(true); + initMetadataIfMissing(job); + job.getMetadata().objectUpdated(); + return new ResponseEntity<>(jobRepository.save(job), HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @PutMapping(value = "/jobs/{id}/start") + public ResponseEntity startJob(@PathVariable("id") String jobId, UserInfo userInfo) { + log.debug("Starting job with id {}", jobId); + ImportJob job = jobRepository.findById(jobId).orElse(null); + if (authorizationChecker.hasRole(userInfo, AuthorizationChecker.ROLE_ADMIN) + || authorizationChecker.hasRoleForImportJob(userInfo, AuthorizationChecker.ROLE_MANAGER, job)) { + job.setActive(true); + initMetadataIfMissing(job); + job.getMetadata().objectUpdated(); + jobService.doImportJob(job); + return new ResponseEntity<>(job, HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @PutMapping(value = "/jobs/{id}/stop") + public ResponseEntity stopJob(@PathVariable("id") String jobId, UserInfo userInfo) { + log.debug("Stopping job with id {}", jobId); + ImportJob job = jobRepository.findById(jobId).orElse(null); + if (authorizationChecker.hasRole(userInfo, AuthorizationChecker.ROLE_ADMIN) + || authorizationChecker.hasRoleForImportJob(userInfo, AuthorizationChecker.ROLE_MANAGER, job)) { + job.setActive(false); + initMetadataIfMissing(job); + job.getMetadata().objectUpdated(); + return new ResponseEntity<>(jobRepository.save(job), HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @DeleteMapping(value = "/jobs/{id}") + public ResponseEntity deleteJob(@PathVariable("id") String jobId, UserInfo userInfo) { + log.debug("Removing job with id {}", jobId); + ImportJob job = jobRepository.findById(jobId).orElse(null); + if (authorizationChecker.hasRole(userInfo, AuthorizationChecker.ROLE_ADMIN) + || authorizationChecker.hasRoleForImportJob(userInfo, AuthorizationChecker.ROLE_MANAGER, job)) { + jobRepository.deleteById(jobId); + return new ResponseEntity<>(HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + private void initMetadataIfMissing(ImportJob job) { + if (job.getMetadata() == null) { + job.setMetadata(new Metadata()); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/KeycloakConfigController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/KeycloakConfigController.java new file mode 100644 index 000000000..f28f8d5d7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/KeycloakConfigController.java @@ -0,0 +1,109 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * A Rest controller for dispatching Keycloak configuration to frontend. + * @author laurent + */ +@RestController +@RequestMapping("/api/keycloak") +public class KeycloakConfigController { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(KeycloakConfigController.class); + + @Value("${keycloak.enabled}") + private Boolean keycloakEnabled = true; + + @Value("${sso.public-url}") + private String keycloakServerUrl = null; + + @Value("${keycloak.realm}") + private String keycloakRealmName = null; + + @GetMapping(value = "/config") + public ResponseEntity getConfig() { + final Config config = new Config(keycloakEnabled, keycloakRealmName, keycloakServerUrl); + + log.debug("Returning '{}' realm config, for {}", keycloakRealmName, keycloakServerUrl); + + return new ResponseEntity<>(config, HttpStatus.OK); + } + + + private class Config { + + private boolean enabled = true; + + private String realm = "microcks"; + + @JsonProperty("auth-server-url") + private String authServerUrl = "http://localhost:8180/auth"; + + @JsonProperty("ssl-required") + private String sslRequired = "external"; + + @JsonProperty("public-client") + private boolean publicClient = true; + + private String resource = "microcks-app-js"; + + public Config(boolean enabled, String realmName, String authServerUrl) { + this.enabled = enabled; + if (realmName != null && !realm.isEmpty()) { + this.realm = realmName; + } + if (authServerUrl != null && !authServerUrl.isEmpty()) { + this.authServerUrl = authServerUrl; + } + } + + public boolean isEnabled() { + return enabled; + } + + public String getRealm() { + return realm; + } + + public String getAuthServerUrl() { + return authServerUrl; + } + + public String getSslRequired() { + return sslRequired; + } + + public boolean isPublicClient() { + return publicClient; + } + + public String getResource() { + return resource; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/McpController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/McpController.java new file mode 100644 index 000000000..e6f251124 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/McpController.java @@ -0,0 +1,349 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.util.ai.McpError; +import io.github.microcks.util.ai.McpSchema; +import io.github.microcks.util.ai.McpToolConverter; +import io.github.microcks.util.graphql.GraphQLMcpToolConverter; +import io.github.microcks.util.grpc.GrpcMcpToolConverter; +import io.github.microcks.util.openapi.OpenAPIMcpToolConverter; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * This controller handles the MCP protocol with HTTP/SEE transport to use Microcks mocks as tools. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +public class McpController { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(McpController.class); + + private final ServiceRepository serviceRepository; + private final ResourceRepository resourceRepository; + private final RestInvocationProcessor restInvocationProcessor; + private final GrpcInvocationProcessor grpcInvocationProcessor; + private final GraphQLInvocationProcessor graphQLInvocationProcessor; + + private final ConcurrentHashMap channelsBySessionId = new ConcurrentHashMap<>(); + private final ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor(); + private final ObjectMapper mapper = new ObjectMapper(); + + /** + * Build a McpController with required dependencies. + * @param serviceRepository The repository to access services definitions + * @param resourceRepository The repository to access resources definitions + * @param restInvocationProcessor The invocation processor to apply REST mocks dispatching logic + * @param grpcInvocationProcessor The invocation processor to apply GRPC mocks dispatching logic + * @param graphQLInvocationProcessor The incocation processor to apply GraphQL mocks dispatching logic + */ + public McpController(ServiceRepository serviceRepository, ResourceRepository resourceRepository, + RestInvocationProcessor restInvocationProcessor, GrpcInvocationProcessor grpcInvocationProcessor, + GraphQLInvocationProcessor graphQLInvocationProcessor) { + this.serviceRepository = serviceRepository; + this.resourceRepository = resourceRepository; + this.restInvocationProcessor = restInvocationProcessor; + this.grpcInvocationProcessor = grpcInvocationProcessor; + this.graphQLInvocationProcessor = graphQLInvocationProcessor; + } + + /** + * Handle the initialization of a SSE connection for the MCP protocol. + * @param serviceName The name of the service to connect to. + * @param version The version of the service to connect to. + * @return The SSE emitter to use for the connection. + */ + @RequestMapping(value = "/mcp/{service}/{version}/sse", method = { RequestMethod.GET }) + public SseEmitter handleSSE(@PathVariable("service") String serviceName, @PathVariable("version") String version) { + log.info("Handling a Mcp SSE for service {} and version {}", serviceName, version); + + String sessionId = UUID.randomUUID().toString(); + log.debug("Creating new SSE connection for session: {}", sessionId); + + SseEmitter emitter = new SseEmitter(Duration.ofSeconds(600).toMillis()); + emitter.onCompletion(() -> { + log.debug("SSE connection completed for session: {}", sessionId); + channelsBySessionId.remove(sessionId); + }); + emitter.onTimeout(() -> { + log.debug("SSE connection timed out for session: {}", sessionId); + channelsBySessionId.remove(sessionId); + }); + + channelsBySessionId.put(sessionId, new SseTransportChannel(sessionId, emitter)); + + // If serviceName was encoded with '+' instead of '%20', remove them. + if (serviceName.contains("+")) { + serviceName = serviceName.replace('+', ' '); + } + final String serviceNameFinal = serviceName; + sseMvcExecutor.execute(() -> { + try { + SseEmitter.SseEventBuilder event = SseEmitter.event().name("endpoint").id(sessionId) + .data("/mcp/" + serviceNameFinal + "/" + version + "/message?sessionId=" + sessionId); + emitter.send(event); + } catch (Exception e) { + log.error("Failed to send initial endpoint event: {}", e.getMessage()); + emitter.completeWithError(e); + } + }); + return emitter; + } + + /** + * Handle a MCP message request. + * @param serviceName The name of the service to connect to. + * @param version The version of the service to connect to. + * @param sessionId The MCP session ID of the connection. + * @param request The MCP request to handle. + * @return The response entity. + */ + @PostMapping(value = "/mcp/{service}/{version}/message", produces = "text/event-stream") + public ResponseEntity handleMessage(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @RequestParam(value = "sessionId") String sessionId, + @RequestBody McpSchema.JSONRPCRequest request, @RequestHeader HttpHeaders headers) { + + log.info("Handling a {} Mcp request for service {} and version {}", request.method(), serviceName, version); + + // Check session is known and valid. + SseTransportChannel channel = channelsBySessionId.get(sessionId); + if (channel == null) { + log.debug("Session {} not found", sessionId); + return ResponseEntity.badRequest().body(new McpError("Session not found: " + sessionId)); + } + + McpSchema.JSONRPCResponse response = null; + try { + response = handleMcpRequest(serviceName, version, request, headers); + } catch (McpError e) { + return ResponseEntity.badRequest().body(e); + } + sendSseMessage(channel, response); + + return ResponseEntity.ok().build(); + } + + /** + * Reserved for future usage when HTTP Streamable transport specification will be frozen and implemented. + * @param serviceName The name of the service to connect to. + * @param version The version of the service to connect to. + * @param request The MCP request to handle. + * @return The response entity. + */ + @PostMapping(value = "/mcp/{service}/{version}", produces = { "application/json" }) + public ResponseEntity handleHttpStreamable(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @RequestBody McpSchema.JSONRPCRequest request, + @RequestHeader HttpHeaders headers) { + + log.info("Handling a Mcp Http streamable call for service {} and version {}", serviceName, version); + + try { + McpSchema.JSONRPCResponse response = handleMcpRequest(serviceName, version, request, headers); + return ResponseEntity.ok(response); + } catch (McpError e) { + return ResponseEntity.badRequest().body(e); + } + } + + /** Logic of handling Mcp Request. */ + private McpSchema.JSONRPCResponse handleMcpRequest(String serviceName, String version, + McpSchema.JSONRPCRequest request, HttpHeaders headers) throws McpError { + // If serviceName was encoded with '+' instead of '%20', remove them. + if (serviceName.contains("+")) { + serviceName = serviceName.replace('+', ' '); + } + // Find matching service. + Service service = serviceRepository.findByNameAndVersion(serviceName, version); + if (service == null) { + log.debug("Service {}:{} not found", serviceName, version); + throw new McpError("Invalid service name or version"); + } + + Object result = null; + switch (request.method()) { + case McpSchema.METHOD_INITIALIZE -> { + result = handleInitializeRequest(request, service); + } + case McpSchema.METHOD_TOOLS_LIST -> { + result = handleToolsListRequest(request, service); + } + case McpSchema.METHOD_TOOLS_CALL -> { + result = handleToolsCallRequest(request, headers, service); + } + } + + McpSchema.JSONRPCResponse response = null; + if (result != null) { + response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null); + } else { + response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, + new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND, + "Unsupported method: " + request.method(), null)); + } + + return response; + } + + /** Internal record to hold a SSE transport channel with its emitter. */ + record SseTransportChannel(String sessionId, SseEmitter emitter) { + } + + /** Handle the MCP initialize request. */ + private Object handleInitializeRequest(McpSchema.JSONRPCRequest request, Service service) { + McpSchema.InitializeRequest initializeRequest = mapper.convertValue(request.params(), + new TypeReference() { + }); + + if (initializeRequest.protocolVersion().equals(McpSchema.LATEST_PROTOCOL_VERSION) + || initializeRequest.protocolVersion().equals(McpSchema.FIRST_PROTOCOL_VERSION)) { + McpSchema.ClientCapabilities clientCapabilities = initializeRequest.capabilities(); + McpSchema.Implementation clientInfo = initializeRequest.clientInfo(); + + McpSchema.ServerCapabilities serverCapabilities = new McpSchema.ServerCapabilities(null, null, + new McpSchema.ServerCapabilities.PromptCapabilities(false), + new McpSchema.ServerCapabilities.ResourceCapabilities(false, false), + new McpSchema.ServerCapabilities.ToolCapabilities(false)); + + return new McpSchema.InitializeResult(initializeRequest.protocolVersion(), serverCapabilities, + new McpSchema.Implementation(service.getName() + " MCP server", service.getVersion()), null); + } + return new McpError("Unsupported protocol version: " + initializeRequest.protocolVersion()); + } + + /** Handle the MCP tools/list request. */ + private Object handleToolsListRequest(McpSchema.JSONRPCRequest request, Service service) { + // Find the contract resource to build the correct converter. + Resource resource = getContractResource(service); + McpToolConverter converter = buildMcpToolConverter(service, resource); + + List tools = service.getOperations().stream() + .map(operation -> new McpSchema.Tool(converter.getToolName(operation), + converter.getToolDescription(operation), converter.getInputSchema(operation))) + .toList(); + + return new McpSchema.ListToolsResult(tools, null); + } + + /** Handle the MCP tools/call request. */ + private Object handleToolsCallRequest(McpSchema.JSONRPCRequest request, Map> headers, + Service service) { + McpSchema.CallToolRequest toolRequest = mapper.convertValue(request.params(), + new TypeReference() { + }); + + // Find the contract resource to build the correct converter. + Resource resource = getContractResource(service); + McpToolConverter converter = buildMcpToolConverter(service, resource); + + Operation callOperation = service.getOperations().stream() + .filter(operation -> toolRequest.name().equals(converter.getToolName(operation))).findFirst().orElse(null); + if (callOperation == null) { + return new McpError("Unknown tool name: " + toolRequest.name()); + } + + Response response = converter.getCallResponse(callOperation, toolRequest, headers); + + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(response.getContent())), + response.isFault()); + } + + /** Send a JSONRCPResponse as an SSE event. */ + private void sendSseMessage(SseTransportChannel channel, McpSchema.JSONRPCResponse response) { + sseMvcExecutor.execute(() -> { + try { + // spotless:off + String jsonText = mapper.writeValueAsString(response); + SseEmitter.SseEventBuilder event = SseEmitter.event() + .name("message") + .id(channel.sessionId()) + .data(jsonText); + channel.emitter.send(event); + // spotless:on + } catch (Exception e) { + log.error("Failed to send message to session {}: {}", channel.sessionId(), e.getMessage()); + channel.emitter.completeWithError(e); + } + }); + } + + + private Resource getContractResource(Service service) { + List resources = resourceRepository.findByServiceIdAndType(service.getId(), getResourceType(service)); + if (!resources.isEmpty()) { + return resources.getFirst(); + } + return null; + } + + private ResourceType getResourceType(Service service) { + ResourceType resourceType = ResourceType.OPEN_API_SPEC; + switch (service.getType()) { + case GRPC -> resourceType = ResourceType.PROTOBUF_DESCRIPTOR; + case GRAPHQL -> resourceType = ResourceType.GRAPHQL_SCHEMA; + } + return resourceType; + } + + private McpToolConverter buildMcpToolConverter(Service service, Resource resource) { + McpToolConverter converter = null; + + switch (service.getType()) { + case GRPC -> converter = new GrpcMcpToolConverter(service, resource, grpcInvocationProcessor, mapper); + case GRAPHQL -> converter = new GraphQLMcpToolConverter(service, resource, graphQLInvocationProcessor, mapper); + default -> converter = new OpenAPIMcpToolConverter(service, resource, restInvocationProcessor, mapper); + } + return converter; + } + + private Header getHeader(String name, Set
headers) { + if (headers != null) { + return headers.stream().filter(header -> header.getName().equalsIgnoreCase(name)).findFirst().orElse(null); + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MetricsController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MetricsController.java new file mode 100644 index 000000000..44c5acd32 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MetricsController.java @@ -0,0 +1,216 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.DailyStatistic; +import io.github.microcks.domain.TestConformanceMetric; +import io.github.microcks.domain.WeightedMetricValue; +import io.github.microcks.listener.DailyStatisticsFeeder; +import io.github.microcks.listener.StatisticsFlusher; +import io.github.microcks.repository.CustomDailyStatisticRepository; +import io.github.microcks.repository.DailyStatisticRepository; +import io.github.microcks.repository.TestConformanceMetricRepository; +import io.github.microcks.repository.TestResultRepository; +import io.github.microcks.util.SafeLogger; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * A REST controller for managing metrics consultation API endpoints. + * @author laurent + */ +@RestController +@RequestMapping("/api") +public class MetricsController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(MetricsController.class); + + private final DailyStatisticRepository invocationsRepository; + private final TestConformanceMetricRepository metricRepository; + private final TestResultRepository testResultRepository; + private final StatisticsFlusher statisticsFlusher; + + /** + * Build a new MetricsController with its required dependencies. + * @param invocationsRepository The repository for daily statistics. + * @param metricRepository The repository for test conformance metrics. + * @param testResultRepository The repository for test results. + * @param statisticsFlusher The statistics flusher. + */ + public MetricsController(DailyStatisticRepository invocationsRepository, + TestConformanceMetricRepository metricRepository, TestResultRepository testResultRepository, + StatisticsFlusher statisticsFlusher) { + this.invocationsRepository = invocationsRepository; + this.metricRepository = metricRepository; + this.testResultRepository = testResultRepository; + this.statisticsFlusher = statisticsFlusher; + } + + @GetMapping(value = "/metrics/invocations/global") + public DailyStatistic getInvocationStatGlobal(@RequestParam(value = "day", required = false) String day) { + // Ensure we got accurate statistics by flushing the feeder to database. + statisticsFlusher.flushToDatabase(); + // Now process the request. + log.debug("Getting invocations stats for day {}", day); + if (day == null) { + day = getTodaysDate(); + } + return invocationsRepository.aggregateDailyStatistics(day); + } + + @GetMapping(value = "/metrics/invocations/top") + public List getInvocationTopStats(@RequestParam(value = "day", required = false) String day, + @RequestParam(value = "limit", required = false, defaultValue = "20") Integer limit) { + // Ensure we got accurate statistics by flushing the feeder to database. + statisticsFlusher.flushToDatabase(); + // Now process the request. + log.debug("Getting top {} invocations stats for day {}", limit, day); + if (day == null) { + day = getTodaysDate(); + } + return invocationsRepository.findTopStatistics(day, limit); + } + + @GetMapping(value = "/metrics/invocations/{service}/{version}") + public DailyStatistic getInvocationStatForService(@PathVariable("service") String serviceName, + @PathVariable("version") String serviceVersion, @RequestParam(value = "day", required = false) String day) { + // Ensure we got accurate statistics by flushing the feeder to database. + statisticsFlusher.flushToDatabase(); + // Now process the request. + log.debug("Getting invocations stats for service [{}, {}] and day {}", serviceName, serviceVersion, day); + if (day == null) { + day = getTodaysDate(); + } + List statistics = invocationsRepository.findByDayAndServiceNameAndServiceVersion(day, serviceName, + serviceVersion); + if (!statistics.isEmpty()) { + return statistics.get(0); + } + return null; + } + + @GetMapping(value = "/metrics/invocations/global/latest") + public Map getLatestInvocationStatGlobal( + @RequestParam(value = "limit", required = false, defaultValue = "20") Integer limit) { + // Ensure we got accurate statistics by flushing the feeder to database. + statisticsFlusher.flushToDatabase(); + // Now process the request. + log.debug("Getting invocations stats for last {} days", limit); + + String day = getTodaysDate(); + String dayBefore = getPastDateAsString(limit); + Map invocations = new TreeMap<>(); + List results = invocationsRepository + .aggregateDailyStatistics(dayBefore, day); + for (CustomDailyStatisticRepository.InvocationCount count : results) { + invocations.put(count.getDay(), count.getNumber()); + } + return invocations; + } + + @GetMapping(value = "/metrics/conformance/aggregate") + public List getAggregatedTestCoverageMetrics() { + log.debug("Computing TestConformanceMetric aggregates"); + + return metricRepository.aggregateTestConformanceMetric(); + } + + @GetMapping(value = "/metrics/conformance/service/{serviceId:.+}") + public TestConformanceMetric getTestConformanceMetric(@PathVariable("serviceId") String serviceId) { + log.debug("Retrieving TestConformanceMetric for service with id {}", serviceId); + + return metricRepository.findByServiceId(serviceId); + } + + @GetMapping(value = "/metrics/tests/latest") + public List getLatestTestResults( + @RequestParam(value = "limit", required = false, defaultValue = "7") Integer limit) { + log.debug("Getting tests trend for last {} days", limit); + + // Compute last date and retrieve test results. + Date lastDate = getPastDate(limit); + return testResultRepository.findAllWithTestDateAfter(lastDate).stream() + .map(res -> new TestResultSummary(res.getId(), res.getTestDate(), res.getServiceId(), res.isSuccess())) + .collect(Collectors.toList()); + } + + private String getTodaysDate() { + Calendar calendar = Calendar.getInstance(); + int month = calendar.get(Calendar.MONTH) + 1; + String monthStr = (month < 10 ? "0" : "") + month; + int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + String dayOfMonthStr = (dayOfMonth < 10 ? "0" : "") + dayOfMonth; + return calendar.get(Calendar.YEAR) + monthStr + dayOfMonthStr; + } + + private Date getPastDate(Integer daysBack) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_YEAR, -daysBack); + return calendar.getTime(); + } + + private String getPastDateAsString(Integer daysBack) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_YEAR, -daysBack); + int month = calendar.get(Calendar.MONTH) + 1; + String monthStr = (month < 10 ? "0" : "") + (month); + int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + String dayOfMonthStr = (dayOfMonth < 10 ? "0" : "") + (dayOfMonth); + return calendar.get(Calendar.YEAR) + monthStr + dayOfMonthStr; + } + + public static class TestResultSummary { + String id; + Date testDate; + String serviceId; + boolean success; + + public TestResultSummary(String id, Date testDate, String serviceId, boolean success) { + this.id = id; + this.testDate = testDate; + this.serviceId = serviceId; + this.success = success; + } + + public String getId() { + return id; + } + + public Date getTestDate() { + return testDate; + } + + public String getServiceId() { + return serviceId; + } + + public boolean isSuccess() { + return success; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MockControllerCommons.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MockControllerCommons.java new file mode 100644 index 000000000..06dcda101 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MockControllerCommons.java @@ -0,0 +1,376 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.event.MockInvocationEvent; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.dispatcher.FallbackSpecification; +import io.github.microcks.util.dispatcher.JsonMappingException; +import io.github.microcks.util.dispatcher.ProxyFallbackSpecification; +import io.github.microcks.util.el.EvaluableRequest; +import io.github.microcks.util.el.TemplateEngine; +import io.github.microcks.util.el.TemplateEngineFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +import java.net.URI; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Optional; + +/** + * This class holds commons, utility handlers for different mock controller implements (whether it be Soap, Rest, Async + * or whatever ...) + * @author laurent + */ +public class MockControllerCommons { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(MockControllerCommons.class); + + /** The header name used to specify a wait delay in the request. */ + public static final String X_MICROCKS_DELAY_HEADER = "x-microcks-delay"; + + private static final String RENDERING_MESSAGE = "Response contains dynamic EL expression, rendering it..."; + + + /** Private constructor to avoid instantiation. */ + private MockControllerCommons() { + } + + /** + * Retrieve a fallback specification for this operation if one is defined. + * @param rOperation The operation to get fallback specification + * @return A fallback specification or null if none defined + */ + public static FallbackSpecification getFallbackIfAny(Operation rOperation) { + FallbackSpecification fallback = null; + if (DispatchStyles.FALLBACK.equals(rOperation.getDispatcher())) { + try { + fallback = FallbackSpecification.buildFromJsonString(rOperation.getDispatcherRules()); + } catch (JsonMappingException jme) { + log.error("Dispatching rules of operation cannot be interpreted as FallbackSpecification", jme); + } + } + return fallback; + } + + /** + * Retrieve a proxyFallback specification for this operation if one is defined. + * @param rOperation The operation to get proxyFallback specification + * @return A proxy specification or null if none defined + */ + public static ProxyFallbackSpecification getProxyFallbackIfAny(Operation rOperation) { + ProxyFallbackSpecification proxyFallback = null; + if (DispatchStyles.PROXY_FALLBACK.equals(rOperation.getDispatcher())) { + try { + proxyFallback = ProxyFallbackSpecification.buildFromJsonString(rOperation.getDispatcherRules()); + } catch (JsonMappingException jme) { + log.error("Dispatching rules of operation cannot be interpreted as ProxyFallbackSpecification", jme); + } + } + return proxyFallback; + } + + /** + * Check if proxy behavior is requested and extract proxyUrl. + * @param dispatcher The original dispatcher for the Proxy dispatcher checking. + * @param dispatcherRules The original dispatcherRules for URL extracting. + * @param resourcePath The original resourcePath for URL compilation. + * @param proxyFallback The proxyFallbackSpec for the Proxy-Fallback dispatcher checking and URL extracting. + * @param request The original request for URL comparing on cycling. + * @param response The response that was found(or not) by dispatcher. + * @return The optional container with URI for the proxy service if the request needs to be proxied. + */ + public static Optional getProxyUrlIfProxyIsNeeded(String dispatcher, String dispatcherRules, + String resourcePath, ProxyFallbackSpecification proxyFallback, HttpServletRequest request, Response response) { + String externalUrl = null; + if (DispatchStyles.PROXY.equals(dispatcher)) { + externalUrl = dispatcherRules; + } + if (response == null && proxyFallback != null) { + externalUrl = proxyFallback.getProxyUrl(); + } + if (externalUrl != null) { + externalUrl = externalUrl.replaceFirst("/$", "") + resourcePath; + if (!externalUrl.contentEquals(request.getRequestURL())) { + try { + return Optional + .of(UriComponentsBuilder.fromHttpUrl(externalUrl).query(request.getQueryString()).build().toUri()); + } catch (IllegalArgumentException ex) { + log.warn("Invalid external URL in the dispatcher - {}", externalUrl); + } + } + } + return Optional.empty(); + } + + /** + * Render the response headers using the Expression Language compatible {@code TemplateEngine} if required. If + * rendering template fails, we just produce a log error message and stick to templatized values. + * @param evaluableRequest The request that can be evaluated in templating. + * @param requestContext The invocation context of the request + * @param response The response that was found by dispatcher + * @return The rendered response headers values. + */ + public static Set
renderResponseHeaders(EvaluableRequest evaluableRequest, + Map requestContext, Response response) { + TemplateEngine engine = null; + Set
headers = new HashSet<>(); + + if (response.getHeaders() != null) { + for (Header header : response.getHeaders()) { + Set renderedValues = new HashSet<>(); + for (String value : header.getValues()) { + // Only render and build an engine if we have an expression. + if (value.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + // Evaluate the header value. + if (engine == null) { + engine = TemplateEngineFactory.getTemplateEngine(); + } + renderedValues.add(unguardedRenderResponseContent(evaluableRequest, requestContext, engine, value)); + } else { + renderedValues.add(value); + } + } + headers.add(new Header(header.getName(), renderedValues)); + } + } + return headers; + } + + /** + * Render the response content using the Expression Language compatible {@code TemplateEngine} if required. If + * rendering template fails, we just produce a log error message and stick to templatized response. + * @param requestBody The body payload of incoming request. + * @param requestContext The invocation context of the request + * @param response The response that was found by dispatcher + * @return The rendered response body payload. + */ + public static String renderResponseContent(String requestBody, Map requestContext, + Response response) { + if (response.getContent() != null && response.getContent().contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + log.debug(RENDERING_MESSAGE); + + // Create and fill an evaluable request object. + EvaluableRequest evaluableRequest = new EvaluableRequest(requestBody, null); + + // Evaluate the response. + return unguardedRenderResponseContent(evaluableRequest, requestContext, + TemplateEngineFactory.getTemplateEngine(), response.getContent()); + } + return response.getContent(); + } + + /** + * Render the response content using the Expression Language compatible {@code TemplateEngine} if required. If + * rendering template fails, we just produce a log error message and stick to templatized response. + * @param requestBody The body payload of incoming request. + * @param evaluableParams A map of params for evaluation. + * @param evaluableHeaders A map of headers for evaluation. + * @param requestContext The invocation context of the request + * @param response The response that was found by dispatcher + * @return The rendered response body payload. + */ + public static String renderResponseContent(String requestBody, Map evaluableParams, + Map evaluableHeaders, Map requestContext, Response response) { + if (response.getContent() != null && response.getContent().contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + log.debug(RENDERING_MESSAGE); + + // Create and fill an evaluable request object. + EvaluableRequest evaluableRequest = new EvaluableRequest(requestBody, null); + // Adding query and header parameters... + evaluableRequest.setParams(evaluableParams); + evaluableRequest.setHeaders(evaluableHeaders); + + // Evaluate the response. + return unguardedRenderResponseContent(evaluableRequest, requestContext, + TemplateEngineFactory.getTemplateEngine(), response.getContent()); + } + return response.getContent(); + } + + /** + * Render the response content using the Expression Language compatible {@code TemplateEngine} if required. If + * rendering template fails, we just produce a log error message and stick to templatized response. + * @param requestBody The body payload of incoming request. + * @param requestResourcePath The resource path of mock request (if any, may be null) + * @param request The incoming servlet request + * @param requestContext The invocation context of the request + * @param response The response that was found by dispatcher + * @return The rendered response body payload. + */ + public static String renderResponseContent(String requestBody, String requestResourcePath, + HttpServletRequest request, Map requestContext, Response response) { + if (response.getContent() != null && response.getContent().contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + log.debug(RENDERING_MESSAGE); + + // Create and fill an evaluable request object. + EvaluableRequest evaluableRequest = buildEvaluableRequest(requestBody, requestResourcePath, request); + return unguardedRenderResponseContent(evaluableRequest, requestContext, + TemplateEngineFactory.getTemplateEngine(), response.getContent()); + } + return response.getContent(); + } + + /** + * Render the response content using the Expression Language compatible {@code TemplateEngine} if required. If + * rendering template fails, we just produce a log error message and stick to templatized response. + * @param evaluableRequest The request that can be evaluated in templating. + * @param engine The template engine to use for this rendering + * @param responseContent The response content found by dispatcher + * @return The rendered response body payload. + */ + public static String renderResponseContent(EvaluableRequest evaluableRequest, TemplateEngine engine, + String responseContent) { + if (responseContent != null && responseContent.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) { + return unguardedRenderResponseContent(evaluableRequest, null, engine, responseContent); + } + return responseContent; + } + + private static String unguardedRenderResponseContent(EvaluableRequest evaluableRequest, + Map requestContext, TemplateEngine engine, String responseContent) { + // Register the request variable and evaluate the response. + engine.getContext().setVariable("request", evaluableRequest); + if (requestContext != null) { + engine.getContext().setVariables(requestContext); + } + try { + return engine.getValue(responseContent); + } catch (Throwable t) { + log.error("Failing at evaluating template {}", responseContent, t); + } + return responseContent; + } + + /** + * Build an Evaluable request from various request elements. + * @param requestBody The body of request to evaluate + * @param requestResourcePath The resource path this request is bound to + * @param request The underlying Http request + * @return The Evaluable request for later templating rendering + */ + public static EvaluableRequest buildEvaluableRequest(String requestBody, String requestResourcePath, + HttpServletRequest request) { + // Create and fill an evaluable request object. + EvaluableRequest evaluableRequest = new EvaluableRequest(requestBody, + requestResourcePath != null ? requestResourcePath.split("/") : null); + // Adding query parameters... + Map evaluableParams = new HashMap<>(); + List parameterNames = Collections.list(request.getParameterNames()); + for (String parameter : parameterNames) { + evaluableParams.put(parameter, request.getParameter(parameter)); + } + evaluableRequest.setParams(evaluableParams); + // Adding headers... + Map evaluableHeaders = new HashMap<>(); + if (request.getHeaderNames() != null) { + List headerNames = Collections.list(request.getHeaderNames()); + for (String header : headerNames) { + evaluableHeaders.put(header, request.getHeader(header)); + } + } + evaluableRequest.setHeaders(evaluableHeaders); + + return evaluableRequest; + } + + /** Retrieve delay header or default to the one provided as parameter. */ + public static Long getDelay(HttpHeaders headers, Long delayParameter) { + if (headers.containsKey(MockControllerCommons.X_MICROCKS_DELAY_HEADER)) { + String delayHeader = headers.getFirst(MockControllerCommons.X_MICROCKS_DELAY_HEADER); + try { + return Long.parseLong(delayHeader); + } catch (NumberFormatException nfe) { + log.debug("Invalid delay header value: {}", delayHeader); + } + } + return delayParameter; + } + + /** + * Block current thread for specified delay (if not null) from @{code startTime}. + * @param startTime The starting time of mock request invocation + * @param delay The delay to wait for + */ + public static void waitForDelay(Long startTime, Long delay) { + if (delay != null && delay > -1) { + log.debug("Mock delay is turned on, waiting if necessary..."); + long duration = System.currentTimeMillis() - startTime; + if (duration < delay) { + Object semaphore = new Object(); + synchronized (semaphore) { + try { + semaphore.wait(delay - duration); + } catch (Exception e) { + log.debug("Delay semaphore was interrupted"); + } + } + } + log.debug("Delay now expired, releasing response !"); + } + } + + /** + * Publish a mock invocation event on Spring ApplicationContext internal bus. + * @param applicationContext The context to use for publication + * @param eventSource The source of this event + * @param service The mocked Service that was invoked + * @param response The response it has been dispatched to + * @param startTime The start time of the invocation + */ + public static void publishMockInvocation(ApplicationContext applicationContext, Object eventSource, Service service, + Response response, Long startTime) { + // Publish an invocation event before returning. + MockInvocationEvent event = new MockInvocationEvent(eventSource, service.getName(), service.getVersion(), + response.getName(), new Date(startTime), startTime - System.currentTimeMillis()); + applicationContext.publishEvent(event); + log.debug("Mock invocation event has been published"); + } + + public static String composeServiceAndVersion(String serviceName, String version) { + return "/" + UriUtils.encodeFragment(serviceName, "UTF-8") + "/" + version; + } + + public static String extractResourcePath(HttpServletRequest request, String serviceAndVersion) { + String requestURI = request.getRequestURI(); + String resourcePath = requestURI.substring(requestURI.indexOf(serviceAndVersion) + serviceAndVersion.length()); + log.debug("Found resourcePath: {}", resourcePath); + + // If resourcePath was encoded with '+' instead of '%20', replace them . + if (resourcePath.contains("+")) { + resourcePath = resourcePath.replace("+", "%20"); + } + return resourcePath; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MockInvocationContext.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MockInvocationContext.java new file mode 100644 index 000000000..fa9ad5344 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/MockInvocationContext.java @@ -0,0 +1,28 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; + +/** + * A mock invocation context that carries the service, operation and resource path. + * @param service The invoked service + * @param operation The invoked operation + * @param resourcePath The resource path of the operation used + */ +public record MockInvocationContext(Service service, Operation operation, String resourcePath) { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ResourceController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ResourceController.java new file mode 100644 index 000000000..b859a79d5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ResourceController.java @@ -0,0 +1,187 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.GenericResource; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.repository.GenericResourceRepository; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.util.ResourceUtil; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.openapi.OpenAPISchemaBuilder; + +import com.fasterxml.jackson.databind.JsonNode; +import org.bson.Document; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; + +import static io.github.microcks.web.DynamicMockRestController.ID_FIELD; + +/** + * A controller for distributing or generating resources associated to services mocked within Microcks. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/api") +public class ResourceController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(ResourceController.class); + + private static final String SWAGGER_20 = "swagger_20"; + private static final String OPENAPI_30 = "openapi_30"; + + private final ResourceRepository resourceRepository; + private final ServiceRepository serviceRepository; + private final GenericResourceRepository genericResourceRepository; + + + /** + * Create a new ResourceController with mandatory components. + * @param resourceRepository The repository to access resource definitions. + * @param serviceRepository The repository to access service definitions. + * @param genericResourceRepository The repository to access generic resource definitions. + */ + public ResourceController(ResourceRepository resourceRepository, ServiceRepository serviceRepository, + GenericResourceRepository genericResourceRepository) { + this.resourceRepository = resourceRepository; + this.serviceRepository = serviceRepository; + this.genericResourceRepository = genericResourceRepository; + } + + @GetMapping(value = "/resources/{name}") + public ResponseEntity getResourceByName(@PathVariable("name") String name, HttpServletRequest request) { + name = URLDecoder.decode(name, StandardCharsets.UTF_8); + log.info("Requesting resource named {}", name); + + List resources = resourceRepository.findByName(name); + if (!resources.isEmpty()) { + Optional resourceOpt = resources.stream().filter(Resource::isMainArtifact).findFirst(); + return resourceOpt.map(this::responseWithResource).orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + @GetMapping(value = "/resources/id/{id}") + public ResponseEntity getResourceById(@PathVariable("id") String id, HttpServletRequest request) { + log.info("Requesting resource with id {}", id); + Optional resourceOpt = resourceRepository.findById(id); + + return resourceOpt.map(this::responseWithResource).orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + + @GetMapping(value = "/resources/service/{serviceId}") + public List getServiceResources(@PathVariable("serviceId") String serviceId) { + log.debug("Request resources for service {}", serviceId); + return resourceRepository.findByServiceId(serviceId); + } + + @GetMapping(value = "/resources/{serviceId}/{resourceType}") + public ResponseEntity getServiceResource(@PathVariable("serviceId") String serviceId, + @PathVariable("resourceType") String resourceType, HttpServletResponse response) { + log.info("Requesting {} resource for service {}", resourceType, serviceId); + + Service service = serviceRepository.findById(serviceId).orElse(null); + if (service != null && ServiceType.GENERIC_REST.equals(service.getType())) { + // Check if there's one reference resource... + JsonNode referenceSchema = null; + List genericResources = genericResourceRepository.findReferencesByServiceId(serviceId); + if (genericResources != null && !genericResources.isEmpty()) { + try { + Document reference = genericResources.get(0).getPayload(); + reference.append(ID_FIELD, genericResources.get(0).getId()); + referenceSchema = OpenAPISchemaBuilder.buildTypeSchemaFromJson(reference.toJson()); + } catch (Exception e) { + log.warn("Exception while building reference schema", e); + } + } + + // Prepare HttpHeaders. + InputStream stream = null; + String resource = findResource(service); + HttpHeaders headers = new HttpHeaders(); + + // Get the correct template depending on resource type. + String templatePath = null; + if (SWAGGER_20.equals(resourceType)) { + templatePath = "templates/swagger-2.0.yaml"; + } else if (OPENAPI_30.equals(resourceType)) { + templatePath = "templates/openapi-3.0.yaml"; + } + + // Read the template and set headers. + try { + stream = ResourceUtil.getClasspathResource(templatePath); + } catch (IOException e) { + log.error("IOException while reading {} template: {}", templatePath, e.getMessage()); + } + headers.set("Content-Type", "text/yaml"); + + // Now process the stream, replacing patterns by value. + if (stream != null) { + String result = ResourceUtil.replaceTemplatesInSpecStream(stream, service, resource, referenceSchema, null); + + return new ResponseEntity<>(result.getBytes(), headers, HttpStatus.OK); + } + } + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + private ResponseEntity responseWithResource(Resource resource) { + String extension = resource.getName().substring(resource.getName().lastIndexOf('.')); + HttpHeaders headers = new HttpHeaders(); + + if (".json".equals(extension)) { + headers.setContentType(MediaType.APPLICATION_JSON); + } else if (".yaml".equals(extension) || ".yml".equals(extension)) { + headers.set("Content-Type", "text/yaml"); + headers.setContentDisposition(ContentDisposition.builder("inline").filename(resource.getName()).build()); + } else if (".wsdl".equals(extension) || ".xsd".equals(extension)) { + headers.setContentType(MediaType.TEXT_XML); + } else if (".avsc".equals(extension)) { + headers.setContentType(MediaType.APPLICATION_JSON); + } + return new ResponseEntity<>(resource.getContent(), headers, HttpStatus.OK); + } + + private String findResource(Service service) { + for (Operation operation : service.getOperations()) { + if (operation.getName().startsWith("GET /") && !operation.getName().endsWith("/:id")) { + return operation.getName().substring("GET /".length()); + } + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ResponseResult.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ResponseResult.java new file mode 100644 index 000000000..2b87a0f1f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ResponseResult.java @@ -0,0 +1,28 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; + +/** + * A thin wrapper around a response result. + * @param status The HTTP status code + * @param headers The HTTP headers + * @param content The content of the response + */ +public record ResponseResult(HttpStatusCode status, HttpHeaders headers, byte[] content) { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/RestController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/RestController.java new file mode 100644 index 000000000..88b452997 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/RestController.java @@ -0,0 +1,398 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.util.ParameterConstraintUtil; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.openapi.OpenAPISchemaValidator; +import io.github.microcks.util.openapi.OpenAPITestRunner; +import io.github.microcks.util.openapi.SwaggerSchemaValidator; + +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import jakarta.servlet.http.HttpServletRequest; + +import javax.annotation.CheckForNull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * A controller for mocking Rest responses. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +public class RestController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(RestController.class); + + private final ServiceRepository serviceRepository; + private final ResourceRepository resourceRepository; + private final RestInvocationProcessor invocationProcessor; + + @Value("${mocks.rest.enable-cors-policy}") + private Boolean enableCorsPolicy; + @Value("${mocks.rest.cors.allowedOrigins}") + private String corsAllowedOrigins; + @Value("${mocks.rest.cors.allowCredentials}") + private Boolean corsAllowCredentials; + + @Value("${validation.resourceUrl}") + private String validationResourceUrl; + + /** + * Build a RestController with required dependencies. + * @param serviceRepository The repository to access services definitions + * @param resourceRepository The repository to access resources definitions + * @param invocationProcessor The invocation processor to apply REST mocks dispatching logic + */ + public RestController(ServiceRepository serviceRepository, ResourceRepository resourceRepository, + RestInvocationProcessor invocationProcessor) { + this.serviceRepository = serviceRepository; + this.resourceRepository = resourceRepository; + this.invocationProcessor = invocationProcessor; + } + + @SuppressWarnings("java:S3752") + @RequestMapping(value = "/rest/{service}/{version}/**", method = { RequestMethod.HEAD, RequestMethod.OPTIONS, + RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE }) + public ResponseEntity execute(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @RequestParam(value = "delay", required = false) Long delay, + @RequestBody(required = false) String body, @RequestHeader HttpHeaders headers, HttpServletRequest request, + HttpMethod method) { + + log.info("Servicing mock response for service [{}, {}] on uri {} with verb {}", serviceName, version, + request.getRequestURI(), method); + log.debug("Request body: {}", body); + + long startTime = System.currentTimeMillis(); + + // Find matching service and operation. + MockInvocationContext ic = findInvocationContext(serviceName, version, request, method); + if (ic.service() == null) { + return new ResponseEntity<>( + String.format("The service %s with version %s does not exist!", serviceName, version).getBytes(), + HttpStatus.NOT_FOUND); + } + + // Check matching operation. + if (ic.operation() == null) { + // Handle OPTIONS request if CORS policy is enabled. + if (Boolean.TRUE.equals(enableCorsPolicy) && HttpMethod.OPTIONS.equals(method)) { + log.debug("No valid operation found but Microcks configured to apply CORS policy"); + return handleCorsRequest(request); + } + + log.debug("No valid operation found and Microcks configured to not apply CORS policy..."); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + log.debug("Found a valid operation {} with rules: {}", ic.operation().getName(), + ic.operation().getDispatcherRules()); + + return processMockInvocationRequest(ic, startTime, MockControllerCommons.getDelay(headers, delay), body, headers, + request, method); + } + + @SuppressWarnings("java:S3752") + @RequestMapping(value = "/rest-valid/{service}/{version}/**", method = { RequestMethod.HEAD, RequestMethod.OPTIONS, + RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE }) + public ResponseEntity validateAndExecute(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @RequestParam(value = "delay", required = false) Long delay, + @RequestBody(required = false) String body, @RequestHeader HttpHeaders headers, HttpServletRequest request, + HttpMethod method) { + + log.info("Servicing mock response for service [{}, {}] on uri {} with verb {}", serviceName, version, + request.getRequestURI(), method); + log.debug("Request body: {}", body); + + long startTime = System.currentTimeMillis(); + + // Find matching service and operation. + MockInvocationContext ic = findInvocationContext(serviceName, version, request, method); + if (ic.service() == null) { + return new ResponseEntity<>( + String.format("The service %s with version %s does not exist!", serviceName, version).getBytes(), + HttpStatus.NOT_FOUND); + } + + // Check matching operation. + if (ic.operation() == null) { + // Handle OPTIONS request if CORS policy is enabled. + if (Boolean.TRUE.equals(enableCorsPolicy) && HttpMethod.OPTIONS.equals(method)) { + log.debug("No valid operation found but Microcks configured to apply CORS policy"); + return handleCorsRequest(request); + } + + log.debug("No valid operation found and Microcks configured to not apply CORS policy..."); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + log.debug("Found a valid operation {} with rules: {}", ic.operation().getName(), + ic.operation().getDispatcherRules()); + + // Try to validate request payload (body) if we have one. + String shortContentType = getShortContentType(request.getContentType()); + if (body != null && !body.trim().isEmpty() + && OpenAPITestRunner.APPLICATION_JSON_TYPES_PATTERN.matcher(shortContentType).matches()) { + log.debug("Looking for an OpenAPI/Swagger schema to validate request body"); + + Resource openapiSpecResource = findResourceCandidate(ic.service()); + if (openapiSpecResource == null) { + return new ResponseEntity<>( + String.format("The service %s with version %s does not have an OpenAPI/Swagger schema!", serviceName, + version).getBytes(), + HttpStatus.PRECONDITION_FAILED); + } + + List errors = getErrors(body, ic, shortContentType, openapiSpecResource); + + log.debug("Schema validation errors: {}", errors.size()); + // Return a 400 http code with errors. + if (!errors.isEmpty()) { + return new ResponseEntity<>(errors.toString().getBytes(), HttpStatus.BAD_REQUEST); + } + } + + return processMockInvocationRequest(ic, startTime, MockControllerCommons.getDelay(headers, delay), body, headers, + request, method); + } + + /** Get the errors from OpenAPI/Swagger schema validation. */ + private List getErrors(String body, MockInvocationContext ic, String shortContentType, + Resource openapiSpecResource) { + boolean isOpenAPIv3 = !ResourceType.SWAGGER.equals(openapiSpecResource.getType()); + + JsonNode openApiSpec = null; + try { + openApiSpec = OpenAPISchemaValidator.getJsonNodeForSchema(openapiSpecResource.getContent()); + } catch (IOException ioe) { + log.debug("OpenAPI specification cannot be transformed into valid JsonNode schema, so failing"); + } + + // Extract JsonNode corresponding to operation. + String verb = ic.operation().getName().split(" ")[0].toLowerCase(); + String path = ic.operation().getName().split(" ")[1].trim(); + + // Get body content as a string. + JsonNode contentNode = null; + try { + contentNode = OpenAPISchemaValidator.getJsonNode(body); + } catch (IOException ioe) { + log.debug("Response body cannot be accessed or transformed as Json, returning failure"); + } + String jsonPointer = "/paths/" + path.replace("/", "~1") + "/" + verb + "/requestBody"; + + List errors = null; + if (isOpenAPIv3) { + errors = OpenAPISchemaValidator.validateJsonMessage(openApiSpec, contentNode, jsonPointer, shortContentType, + validationResourceUrl); + } else { + errors = SwaggerSchemaValidator.validateJsonMessage(openApiSpec, contentNode, jsonPointer, + validationResourceUrl); + } + return errors; + } + + /** Process REST mock invocation. */ + private ResponseEntity processMockInvocationRequest(MockInvocationContext ic, long startTime, Long delay, + String body, HttpHeaders headers, HttpServletRequest request, HttpMethod method) { + + String violationMsg = validateParameterConstraintsIfAny(ic.operation(), request); + if (violationMsg != null) { + return new ResponseEntity<>((violationMsg + ". Check parameter constraints.").getBytes(), + HttpStatus.BAD_REQUEST); + } + + ResponseResult response = invocationProcessor.processInvocation(ic, startTime, delay, body, headers, request); + return new ResponseEntity<>(response.content(), response.headers(), response.status()); + } + + /** Find the invocation context for this mock request. */ + private MockInvocationContext findInvocationContext(String serviceName, String version, HttpServletRequest request, + HttpMethod method) { + // Extract resourcePath for matching with correct operation and build the encoded URI fragment to retrieve simple resourcePath. + String serviceAndVersion = MockControllerCommons.composeServiceAndVersion(serviceName, version); + String resourcePath = MockControllerCommons.extractResourcePath(request, serviceAndVersion); + log.debug("Found resourcePath: {}", resourcePath); + + // If serviceName was encoded with '+' instead of '%20', remove them. + if (serviceName.contains("+")) { + serviceName = serviceName.replace('+', ' '); + } + + // Find matching service. + Service service = serviceRepository.findByNameAndVersion(serviceName, version); + if (service == null) { + return new MockInvocationContext(null, null, resourcePath); + } + + // Find matching operation. + Operation operation = findOperation(service, method, resourcePath); + return new MockInvocationContext(service, operation, resourcePath); + } + + @CheckForNull + private Operation findOperation(Service service, HttpMethod method, String resourcePath) { + // Remove trailing '/' if any. + String trimmedResourcePath = trimResourcePath(resourcePath); + + Operation result = findOperationByResourcePath(service, method, resourcePath, trimmedResourcePath); + + if (result == null) { + // We may not have found an Operation because of not exact resource path matching with an operation + // using a Fallback dispatcher. Try again, just considering the verb and path pattern of operation. + result = findOperationByPathPattern(service, method, resourcePath); + } + return result; + } + + @CheckForNull + private Operation findOperationByPathPattern(Service service, HttpMethod method, String resourcePath) { + for (Operation operation : service.getOperations()) { + // Select operation based onto Http verb (GET, POST, PUT, etc ...) + // ... then check is current resource path matches operation path pattern. + if (operation.getMethod().equals(method.name()) && operation.getResourcePaths() != null) { + // Produce a matching regexp removing {part} and :part from pattern. + String operationPattern = getURIPattern(operation.getName()); + //operationPattern = operationPattern.replaceAll("\\{.+\\}", "([^/])+"); + operationPattern = operationPattern.replaceAll("\\{[\\w-]+\\}", "([^/])+"); + operationPattern = operationPattern.replaceAll("(/:[^:^/]+)", "\\/([^/]+)"); + if (resourcePath.matches(operationPattern)) { + return operation; + } + } + } + return null; + } + + @CheckForNull + private Operation findOperationByResourcePath(Service service, HttpMethod method, String resourcePath, + String trimmedResourcePath) { + for (Operation operation : service.getOperations()) { + // Select operation based onto Http verb (GET, POST, PUT, etc ...) + // ... then check is we have a matching resource path. + if (operation.getMethod().equals(method.name()) && operation.getResourcePaths() != null + && (operation.getResourcePaths().contains(resourcePath) + || operation.getResourcePaths().contains(trimmedResourcePath))) { + return operation; + } + // Check by simple name comparison if no resource path is defined. + if (operation.getName().equals(method.name().toUpperCase() + " " + resourcePath)) { + return operation; + } + } + return null; + } + + /** Trim the resource path if it ends with a '/'. */ + private String trimResourcePath(String resourcePath) { + if (resourcePath.endsWith("/")) { + return resourcePath.substring(0, resourcePath.length() - 1); + } + return resourcePath; + } + + /** Get the short content type without charset information. */ + private String getShortContentType(String contentType) { + // Sanitize charset information from media-type. + if (contentType != null && contentType.contains("charset=") && contentType.indexOf(";") >= 1) { + return contentType.substring(0, contentType.indexOf(";")); + } + return contentType; + } + + /** Validate the parameter constraints and return a single string with violation message if any. */ + private String validateParameterConstraintsIfAny(Operation rOperation, HttpServletRequest request) { + if (rOperation.getParameterConstraints() != null) { + for (ParameterConstraint constraint : rOperation.getParameterConstraints()) { + String violationMsg = ParameterConstraintUtil.validateConstraint(request, constraint); + if (violationMsg != null) { + return violationMsg; + } + } + } + return null; + } + + /** Retrieve URI Pattern from operation name (remove starting verb name). */ + private String getURIPattern(String operationName) { + if (operationName.startsWith("GET ") || operationName.startsWith("POST ") || operationName.startsWith("PUT ") + || operationName.startsWith("DELETE ") || operationName.startsWith("PATCH ") + || operationName.startsWith("OPTIONS ")) { + return operationName.substring(operationName.indexOf(' ') + 1); + } + return operationName; + } + + /** Handle a CORS request putting the correct headers in response entity. */ + private ResponseEntity handleCorsRequest(HttpServletRequest request) { + // Retrieve and set access control headers from those coming in request. + List accessControlHeaders = new ArrayList<>(); + Collections.list(request.getHeaders("Access-Control-Request-Headers")).forEach(accessControlHeaders::add); + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setAccessControlAllowHeaders(accessControlHeaders); + requestHeaders.setAccessControlExposeHeaders(accessControlHeaders); + + // Apply CORS headers to response with 204 response code. + return ResponseEntity.noContent().header("Access-Control-Allow-Origin", corsAllowedOrigins) + .header("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE, PATCH").headers(requestHeaders) + .header("Access-Allow-Credentials", String.valueOf(corsAllowCredentials)) + .header("Access-Control-Max-Age", "3600").header("Vary", "Accept-Encoding, Origin").build(); + } + + private Resource findResourceCandidate(Service service) { + Optional candidate = Optional.empty(); + // Try resources marked within mainArtifact first. + List resources = resourceRepository.findMainByServiceId(service.getId()); + if (!resources.isEmpty()) { + candidate = getResourceCandidate(resources); + } + // Else try all the services resources... + if (candidate.isEmpty()) { + resources = resourceRepository.findByServiceId(service.getId()); + if (!resources.isEmpty()) { + candidate = getResourceCandidate(resources); + } + } + return candidate.orElse(null); + } + + private Optional getResourceCandidate(List resources) { + return resources.stream() + .filter(r -> ResourceType.OPEN_API_SPEC.equals(r.getType()) || ResourceType.SWAGGER.equals(r.getType())) + .findFirst(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/RestInvocationProcessor.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/RestInvocationProcessor.java new file mode 100644 index 000000000..a919a5c6b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/RestInvocationProcessor.java @@ -0,0 +1,413 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.ParameterLocation; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.ServiceStateRepository; +import io.github.microcks.service.ProxyService; +import io.github.microcks.service.ServiceStateStore; +import io.github.microcks.util.AbsoluteUrlMatcher; +import io.github.microcks.util.DispatchCriteriaHelper; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.dispatcher.FallbackSpecification; +import io.github.microcks.util.dispatcher.JsonEvaluationSpecification; +import io.github.microcks.util.dispatcher.JsonExpressionEvaluator; +import io.github.microcks.util.dispatcher.JsonMappingException; +import io.github.microcks.util.dispatcher.ProxyFallbackSpecification; +import io.github.microcks.util.el.EvaluableRequest; +import io.github.microcks.util.script.ScriptEngineBinder; + +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriUtils; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * A processor for handling REST invocations. It is responsible for applying the dispatching logic and finding the most + * appropriate response based on the request context. + * @author laurent + */ +@Component +public class RestInvocationProcessor { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(RestInvocationProcessor.class); + + private final ServiceStateRepository serviceStateRepository; + private final ResponseRepository responseRepository; + private final ApplicationContext applicationContext; + private final ProxyService proxyService; + + private ScriptEngine scriptEngine; + + @Value("${mocks.enable-invocation-stats}") + private Boolean enableInvocationStats; + + /** + * Build a RestMockInvocationProcessor with required dependencies. + * @param serviceStateRepository The repository to access service state + * @param responseRepository The repository to access responses definitions + * @param applicationContext The Spring application context + * @param proxyService The proxy to external URLs or services + */ + public RestInvocationProcessor(ServiceStateRepository serviceStateRepository, ResponseRepository responseRepository, + ApplicationContext applicationContext, ProxyService proxyService) { + this.serviceStateRepository = serviceStateRepository; + this.responseRepository = responseRepository; + this.applicationContext = applicationContext; + this.proxyService = proxyService; + this.scriptEngine = new ScriptEngineManager().getEngineByExtension("groovy"); + } + + /** + * Process a REST invocation. This method is responsible for determining the appropriate response based on the + * request context, applying any necessary dispatching logic, and handling proxying if required. + * @param ic The invocation context containing information about the service and operation being invoked + * @param startTime The start time of the invocation + * @param delay The delay to apply before returning the response + * @param body The request body + * @param headers The HTTP headers of the request + * @param request The HTTP servlet request + * @return A ResponseResult containing the status, headers, and body of the response + */ + public ResponseResult processInvocation(MockInvocationContext ic, long startTime, Long delay, String body, + Map> headers, HttpServletRequest request) { + + // We must find dispatcher and its rules. Default to operation ones but + // if we have a Fallback or Proxy-Fallback this is the one who is holding the first pass rules. + FallbackSpecification fallback = MockControllerCommons.getFallbackIfAny(ic.operation()); + ProxyFallbackSpecification proxyFallback = MockControllerCommons.getProxyFallbackIfAny(ic.operation()); + String dispatcher = getDispatcher(ic, fallback, proxyFallback); + String dispatcherRules = getDispatcherRules(ic, fallback, proxyFallback); + + // + DispatchContext dispatchContext = computeDispatchCriteria(ic.service(), dispatcher, dispatcherRules, + getURIPattern(ic.operation().getName()), UriUtils.decode(ic.resourcePath(), StandardCharsets.UTF_8), + request, body); + log.debug("Dispatch criteria for finding response is {}", dispatchContext.dispatchCriteria()); + + List responses; + Response response = getResponse(ic, request, dispatchContext); + + if (response == null && fallback != null) { + // If we've found nothing and got a fallback, that's the moment! + responses = responseRepository.findByOperationIdAndName( + IdBuilder.buildOperationId(ic.service(), ic.operation()), fallback.getFallback()); + response = getResponseByMediaType(responses, request); + } + + // Setting delay to default one if not set. + if (delay == null && ic.operation().getDefaultDelay() != null) { + delay = ic.operation().getDefaultDelay(); + } + + // Check if we need to proxy the request. + Optional proxyUrl = MockControllerCommons.getProxyUrlIfProxyIsNeeded(dispatcher, dispatcherRules, + ic.resourcePath(), proxyFallback, request, response); + if (proxyUrl.isPresent()) { + // Delay response here as the returning content will be returned directly. + MockControllerCommons.waitForDelay(startTime, delay); + + // Translate generic headers into Spring ones. + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.putAll(headers); + + // If we've got a proxyUrl, that's the moment! + ResponseEntity proxyResponse = proxyService.callExternal(proxyUrl.get(), + HttpMethod.valueOf(ic.operation().getMethod()), httpHeaders, body); + return new ResponseResult(proxyResponse.getStatusCode(), proxyResponse.getHeaders(), proxyResponse.getBody()); + } + + if (response == null) { + if (dispatcher == null) { + response = getOneForOperation(ic, request, response); + } else { + // There is a dispatcher, but we found no response => return 400 as per #819 and #1132. + return new ResponseResult(HttpStatus.BAD_REQUEST, null, + String.format("The response %s does not exist!", dispatchContext.dispatchCriteria()).getBytes()); + } + } + + if (response != null) { + HttpStatus status = (response.getStatus() != null ? HttpStatus.valueOf(Integer.parseInt(response.getStatus())) + : HttpStatus.OK); + + // Deal with specific headers (content-type and redirect directive). + HttpHeaders responseHeaders = getResponseHeaders(ic, body, request, dispatchContext, response); + String responseContent = getResponseContent(ic, startTime, delay, body, request, dispatchContext, response); + + // Return response content. + return new ResponseResult(status, responseHeaders, + responseContent != null ? responseContent.getBytes(StandardCharsets.UTF_8) : null); + } + + return new ResponseResult(HttpStatus.BAD_REQUEST, null, null); + } + + /** Get the root dispatcher for the invocation context. */ + private String getDispatcher(MockInvocationContext ic, FallbackSpecification fallback, + ProxyFallbackSpecification proxyFallback) { + String dispatcher = ic.operation().getDispatcher(); + if (fallback != null) { + dispatcher = fallback.getDispatcher(); + } + if (proxyFallback != null) { + dispatcher = proxyFallback.getDispatcher(); + } + return dispatcher; + } + + /** Get the root dispatcher rules for the invocation context. */ + private String getDispatcherRules(MockInvocationContext ic, FallbackSpecification fallback, + ProxyFallbackSpecification proxyFallback) { + String dispatcherRules = ic.operation().getDispatcherRules(); + if (fallback != null) { + dispatcherRules = fallback.getDispatcherRules(); + } + if (proxyFallback != null) { + dispatcherRules = proxyFallback.getDispatcherRules(); + } + return dispatcherRules; + } + + /** Get one random response for operation. */ + private Response getOneForOperation(MockInvocationContext ic, HttpServletRequest request, Response response) { + List responses; + // In case no response found because dispatcher is null, just get one for the operation. + // This will allow also OPTIONS operations (like pre-flight requests) with no dispatch criteria to work. + log.debug("No responses found so far, tempting with just bare operationId..."); + responses = responseRepository.findByOperationId(IdBuilder.buildOperationId(ic.service(), ic.operation())); + if (!responses.isEmpty()) { + response = getResponseByMediaType(responses, request); + } + return response; + } + + /** Filter responses using the Accept header for content-type, default to the first. Return null if no responses. */ + private Response getResponseByMediaType(List responses, HttpServletRequest request) { + if (!responses.isEmpty()) { + String accept = request.getHeader("Accept"); + return responses.stream().filter(r -> !StringUtils.isNotEmpty(accept) || accept.equals(r.getMediaType())) + .findFirst().orElse(responses.getFirst()); + } + return null; + } + + /** Retrieve URI Pattern from operation name (remove starting verb name). */ + private String getURIPattern(String operationName) { + if (operationName.startsWith("GET ") || operationName.startsWith("POST ") || operationName.startsWith("PUT ") + || operationName.startsWith("DELETE ") || operationName.startsWith("PATCH ") + || operationName.startsWith("OPTIONS ")) { + return operationName.substring(operationName.indexOf(' ') + 1); + } + return operationName; + } + + /** Compute a dispatch context with a dispatchCriteria string from type, rules and request elements. */ + private DispatchContext computeDispatchCriteria(Service service, String dispatcher, String dispatcherRules, + String uriPattern, String resourcePath, HttpServletRequest request, String body) { + String dispatchCriteria = null; + Map requestContext = null; + + // Depending on dispatcher, evaluate request with rules. + if (dispatcher != null) { + switch (dispatcher) { + case DispatchStyles.SEQUENCE: + dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(dispatcherRules, uriPattern, + resourcePath); + break; + case DispatchStyles.SCRIPT: + requestContext = new HashMap<>(); + Map uriParameters = DispatchCriteriaHelper.extractMapFromURIPattern(uriPattern, + resourcePath); + try { + // Evaluating request with script coming from operation dispatcher rules. + String script = ScriptEngineBinder.ensureSoapUICompatibility(dispatcherRules); + ScriptContext scriptContext = ScriptEngineBinder.buildEvaluationContext(scriptEngine, body, + requestContext, new ServiceStateStore(serviceStateRepository, service.getId()), request, + uriParameters); + dispatchCriteria = (String) scriptEngine.eval(script, scriptContext); + } catch (Exception e) { + log.error("Error during Script evaluation", e); + } + break; + case DispatchStyles.URI_PARAMS: + String fullURI = request.getRequestURL() + "?" + request.getQueryString(); + dispatchCriteria = DispatchCriteriaHelper.extractFromURIParams(dispatcherRules, fullURI); + break; + case DispatchStyles.URI_PARTS: + // /tenantId?t1/userId=x + dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(dispatcherRules, uriPattern, + resourcePath); + break; + case DispatchStyles.URI_ELEMENTS: + dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(dispatcherRules, uriPattern, + resourcePath); + fullURI = request.getRequestURL() + "?" + request.getQueryString(); + dispatchCriteria += DispatchCriteriaHelper.extractFromURIParams(dispatcherRules, fullURI); + break; + case DispatchStyles.JSON_BODY: + try { + JsonEvaluationSpecification specification = JsonEvaluationSpecification + .buildFromJsonString(dispatcherRules); + dispatchCriteria = JsonExpressionEvaluator.evaluate(body, specification); + } catch (JsonMappingException jme) { + log.error("Dispatching rules of operation cannot be interpreted as JsonEvaluationSpecification", jme); + } + break; + case DispatchStyles.QUERY_HEADER: + // Extract headers from request and put them into a simple map to reuse extractFromParamMap(). + dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap(dispatcherRules, + extractRequestHeaders(request)); + break; + default: + log.error("Unknown dispatcher type: {}", dispatcher); + break; + } + } + + return new DispatchContext(dispatchCriteria, requestContext); + } + + private Response getResponse(MockInvocationContext ic, HttpServletRequest request, DispatchContext dispatchContext) { + Response response = null; + + // Filter depending on requested media type. + // TODO: validate dispatchCriteria with dispatcherRules + List responses = responseRepository.findByOperationIdAndDispatchCriteria( + IdBuilder.buildOperationId(ic.service(), ic.operation()), dispatchContext.dispatchCriteria()); + response = getResponseByMediaType(responses, request); + + if (response == null) { + // When using the SCRIPT or JSON_BODY dispatchers, return of evaluation may be the name of response. + responses = responseRepository.findByOperationIdAndName( + IdBuilder.buildOperationId(ic.service(), ic.operation()), dispatchContext.dispatchCriteria()); + response = getResponseByMediaType(responses, request); + } + return response; + } + + /** Extract request headers from request. */ + private Map extractRequestHeaders(HttpServletRequest request) { + Map headers = new HashMap<>(); + Collections.list(request.getHeaderNames()).forEach(name -> headers.put(name, request.getHeader(name))); + return headers; + } + + private HttpHeaders getResponseHeaders(MockInvocationContext ic, String body, HttpServletRequest request, + DispatchContext dispatchContext, Response response) { + HttpHeaders responseHeaders = new HttpHeaders(); + if (response.getMediaType() != null) { + responseHeaders.setContentType(MediaType.valueOf(response.getMediaType() + ";charset=UTF-8")); + } + + // Deal with headers from parameter constraints if any? + recopyHeadersFromParameterConstraints(ic.operation(), request, responseHeaders); + + // Adding other generic headers (caching directives and so on...) + if (response.getHeaders() != null) { + // First check if they should be rendered. + EvaluableRequest evaluableRequest = MockControllerCommons.buildEvaluableRequest(body, ic.resourcePath(), + request); + Set
renderedHeaders = MockControllerCommons.renderResponseHeaders(evaluableRequest, + dispatchContext.requestContext(), response); + + handleHeaders(ic, request, responseHeaders, renderedHeaders); + } + return responseHeaders; + } + + /** Recopy headers defined with parameter constraints. */ + private void recopyHeadersFromParameterConstraints(Operation rOperation, HttpServletRequest request, + HttpHeaders responseHeaders) { + if (rOperation.getParameterConstraints() != null) { + for (ParameterConstraint constraint : rOperation.getParameterConstraints()) { + if (ParameterLocation.header == constraint.getIn() && constraint.isRecopy()) { + String value = request.getHeader(constraint.getName()); + if (value != null) { + responseHeaders.set(constraint.getName(), value); + } + } + } + } + } + + private void handleHeaders(MockInvocationContext ic, HttpServletRequest request, HttpHeaders responseHeaders, + Set
renderedHeaders) { + for (Header renderedHeader : renderedHeaders) { + if ("Location".equals(renderedHeader.getName())) { + String location = renderedHeader.getValues().iterator().next(); + if (!AbsoluteUrlMatcher.matches(location)) { + // We should process location in order to make relative URI specified an absolute one from + // the client perspective. + location = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + + request.getContextPath() + "/rest" + + MockControllerCommons.composeServiceAndVersion(ic.service().getName(), ic.service().getVersion()) + + location; + } + responseHeaders.add(renderedHeader.getName(), location); + } else { + if (!HttpHeaders.TRANSFER_ENCODING.equalsIgnoreCase(renderedHeader.getName())) { + responseHeaders.put(renderedHeader.getName(), new ArrayList<>(renderedHeader.getValues())); + } + } + } + } + + private String getResponseContent(MockInvocationContext ic, long startTime, Long delay, String body, + HttpServletRequest request, DispatchContext dispatchContext, Response response) { + // Render response content before waiting and returning. + String responseContent = MockControllerCommons.renderResponseContent(body, ic.resourcePath(), request, + dispatchContext.requestContext(), response); + + // Delay response. + MockControllerCommons.waitForDelay(startTime, delay); + + // Publish an invocation event before returning if enabled. + if (Boolean.TRUE.equals(enableInvocationStats)) { + MockControllerCommons.publishMockInvocation(applicationContext, this, ic.service(), response, startTime); + } + return responseContent; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/SecretController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/SecretController.java new file mode 100644 index 000000000..7aa2803fc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/SecretController.java @@ -0,0 +1,135 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Secret; +import io.github.microcks.repository.SecretRepository; +import io.github.microcks.security.AuthorizationChecker; +import io.github.microcks.security.UserInfo; +import io.github.microcks.util.SafeLogger; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A Rest controller for API defined on services. + * @author laurent + */ +@RestController +@RequestMapping("/api") +public class SecretController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(SecretController.class); + + private final SecretRepository secretRepository; + private final AuthorizationChecker authorizationChecker; + + /** + * Build a new SecretController with its dependencies. + * @param secretRepository to have access to Secrets definition + * @param authorizationChecker to check user authorization + */ + public SecretController(SecretRepository secretRepository, AuthorizationChecker authorizationChecker) { + this.secretRepository = secretRepository; + this.authorizationChecker = authorizationChecker; + } + + @GetMapping(value = "/secrets") + public List listSecrets(@RequestParam(value = "page", required = false, defaultValue = "0") int page, + @RequestParam(value = "size", required = false, defaultValue = "20") int size, UserInfo userInfo) { + log.debug("Getting secrets list for page {} and size {}", page, size); + + if (!authorizationChecker.hasRole(userInfo, AuthorizationChecker.ROLE_ADMIN)) { + return secretRepository.findAll(PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "name"))).getContent() + .stream().map(this::filterSensitiveData).toList(); + } + // We're admin, so we can return all secrets with sensitive data. + return secretRepository.findAll(PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "name"))).getContent(); + } + + @GetMapping(value = "/secrets/search") + public List searchSecrets(@RequestParam(value = "name") String name) { + log.debug("Searching secrets corresponding to {}", name); + return secretRepository.findByNameLike(name); + } + + @GetMapping(value = "/secrets/count") + public Map countSecrets() { + log.debug("Counting secrets..."); + Map counter = new HashMap<>(); + counter.put("counter", secretRepository.count()); + return counter; + } + + @PostMapping(value = "/secrets") + public ResponseEntity createSecret(@RequestBody Secret secret) { + log.debug("Creating new secret: {}", secret); + return new ResponseEntity<>(secretRepository.save(secret), HttpStatus.CREATED); + } + + @GetMapping(value = "/secrets/{id}") + public ResponseEntity getSecret(@PathVariable("id") String secretId, UserInfo userInfo) { + log.debug("Getting secret with id {}", secretId); + + if (!authorizationChecker.hasRole(userInfo, AuthorizationChecker.ROLE_ADMIN)) { + Secret secret = secretRepository.findById(secretId).orElse(null); + if (secret != null) { + return new ResponseEntity<>(filterSensitiveData(secret), HttpStatus.OK); + } + return new ResponseEntity<>(null, HttpStatus.OK); + } + // We're admin, so we can return all secrets with sensitive data. + return new ResponseEntity<>(secretRepository.findById(secretId).orElse(null), HttpStatus.OK); + } + + @PutMapping(value = "/secrets/{id}") + public ResponseEntity saveSecret(@PathVariable("id") String secretId, @RequestBody Secret secret) { + log.debug("Saving existing secret: {}", secret); + return new ResponseEntity<>(secretRepository.save(secret), HttpStatus.OK); + } + + @DeleteMapping(value = "/secrets/{id}") + public ResponseEntity deleteService(@PathVariable("id") String secretId) { + log.debug("Removing secret with id {}", secretId); + secretRepository.deleteById(secretId); + return new ResponseEntity<>(HttpStatus.OK); + } + + /** Filter sensitive data from a Secret before returning it to the user. */ + private Secret filterSensitiveData(Secret secret) { + secret.setUsername(null); + secret.setPassword(null); + secret.setToken(null); + secret.setCaCertPem(null); + return secret; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ServiceController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ServiceController.java new file mode 100644 index 000000000..54fff1e4e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/ServiceController.java @@ -0,0 +1,242 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Metadata; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.ServiceView; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.repository.CustomServiceRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.security.UserInfo; +import io.github.microcks.service.MessageService; +import io.github.microcks.service.ServiceService; +import io.github.microcks.util.EntityAlreadyExistsException; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.web.dto.GenericResourceServiceDTO; +import io.github.microcks.web.dto.OperationOverrideDTO; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A Rest controller for API defined on services. + * @author laurent + */ +@RestController +@RequestMapping("/api") +public class ServiceController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(ServiceController.class); + + private final ServiceService serviceService; + private final ServiceRepository serviceRepository; + private final MessageService messageService; + + /** + * Build a new ServiceController with its dependencies. + * @param serviceService to perform business logic on Services + * @param serviceRepository to have access to Services definition + * @param messageService to have acces to Services messages + */ + public ServiceController(ServiceService serviceService, ServiceRepository serviceRepository, + MessageService messageService) { + this.serviceService = serviceService; + this.serviceRepository = serviceRepository; + this.messageService = messageService; + } + + @GetMapping(value = "/services") + public List listServices(@RequestParam(value = "page", required = false, defaultValue = "0") int page, + @RequestParam(value = "size", required = false, defaultValue = "20") int size) { + log.debug("Getting service list for page {} and size {}", page, size); + return serviceRepository.findAll(PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "name", "version"))) + .getContent(); + } + + @GetMapping(value = "/services/search") + public List searchServices(@RequestParam Map queryMap) { + // Parse params from queryMap. + String name = null; + Map labels = new HashMap<>(); + for (Map.Entry entry : queryMap.entrySet()) { + if ("name".equals(entry.getKey())) { + name = entry.getValue(); + } else if (entry.getKey().startsWith("labels.")) { + labels.put(entry.getKey().substring(entry.getKey().indexOf('.') + 1), entry.getValue()); + } + } + + if (labels.isEmpty()) { + log.debug("Searching services corresponding to name {}", name); + return serviceRepository.findByNameLike(name); + } + if (name == null || name.trim().isEmpty()) { + log.debug("Searching services corresponding to labels {}", labels); + return serviceRepository.findByLabels(labels); + } + log.debug("Searching services corresponding to name {} and labels {}", name, labels); + return serviceRepository.findByLabelsAndNameLike(labels, name); + } + + @GetMapping(value = "/services/count") + public Map countServices() { + log.debug("Counting services..."); + Map counter = new HashMap<>(); + counter.put("counter", serviceRepository.count()); + return counter; + } + + @GetMapping(value = "/services/map") + public Map getServicesMap() { + log.debug("Counting services by type..."); + Map map = new HashMap<>(); + List results = serviceRepository.countServicesByType(); + for (CustomServiceRepository.ServiceCount count : results) { + map.put(count.getType(), count.getNumber()); + } + return map; + } + + @GetMapping(value = "/services/labels") + public Map getServicesLabels() { + log.debug("Retrieving available services labels..."); + Map labelValues = new HashMap<>(); + List results = serviceRepository.listLabels(); + for (CustomServiceRepository.LabelValues values : results) { + labelValues.put(values.getKey(), values.getValues()); + } + return labelValues; + } + + @GetMapping(value = "/services/{id:.+}", produces = "application/json") + public ResponseEntity getService(@PathVariable("id") String serviceId, + @RequestParam(value = "messages", required = false, defaultValue = "true") boolean messages) { + log.debug("Retrieving service with id {}", serviceId); + + // Just retrieve the service and return it. + Service service = serviceService.getServiceById(serviceId); + if (service == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + if (messages) { + // Put messages into a map where key is operation name. + Map> messagesMap = new HashMap<>(); + for (Operation operation : service.getOperations()) { + if (service.getType() == ServiceType.EVENT || service.getType() == ServiceType.GENERIC_EVENT) { + // If an event, we should explicitly retrieve event messages. + List events = messageService + .getEventByOperation(IdBuilder.buildOperationId(service, operation)); + messagesMap.put(operation.getName(), events); + } else { + // Otherwise we have traditional request / response pairs. + List pairs = messageService + .getRequestResponseByOperation(IdBuilder.buildOperationId(service, operation)); + messagesMap.put(operation.getName(), pairs); + } + } + return new ResponseEntity<>(new ServiceView(service, messagesMap), HttpStatus.OK); + } + return new ResponseEntity<>(service, HttpStatus.OK); + } + + @PostMapping(value = "/services/generic") + public ResponseEntity createGenericResourceService(@RequestBody GenericResourceServiceDTO serviceDTO) { + log.debug("Creating a new Service '{}-{}' for generic resource '{}'", serviceDTO.getName(), + serviceDTO.getVersion(), serviceDTO.getResource()); + + try { + Service service = serviceService.createGenericResourceService(serviceDTO.getName(), serviceDTO.getVersion(), + serviceDTO.getResource(), serviceDTO.getReferencePayload()); + return new ResponseEntity<>(service, HttpStatus.CREATED); + } catch (EntityAlreadyExistsException eaee) { + log.error("Service '{}-{} already exists'", serviceDTO.getName(), serviceDTO.getVersion()); + return new ResponseEntity<>(HttpStatus.CONFLICT); + } + } + + @PostMapping(value = "/services/generic/event") + public ResponseEntity createGenericEventService(@RequestBody GenericResourceServiceDTO serviceDTO) { + log.debug("Creating a new Service '{}-{}' for generic resource '{}'", serviceDTO.getName(), + serviceDTO.getVersion(), serviceDTO.getResource()); + + try { + Service service = serviceService.createGenericEventService(serviceDTO.getName(), serviceDTO.getVersion(), + serviceDTO.getResource(), serviceDTO.getReferencePayload()); + return new ResponseEntity<>(service, HttpStatus.CREATED); + } catch (EntityAlreadyExistsException eaee) { + log.error("Service '{}-{} already exists'", serviceDTO.getName(), serviceDTO.getVersion()); + return new ResponseEntity<>(HttpStatus.CONFLICT); + } + } + + @PutMapping(value = "/services/{id}/metadata") + public ResponseEntity updateMetadata(@PathVariable("id") String serviceId, @RequestBody Metadata metadata, + UserInfo userInfo) { + log.debug("Updating the metadata of service {}", serviceId); + boolean result = serviceService.updateMetadata(serviceId, metadata, userInfo); + if (result) { + return new ResponseEntity<>(HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @PutMapping(value = "/services/{id}/operation") + public ResponseEntity overrideServiceOperation(@PathVariable("id") String serviceId, + @RequestParam(value = "operationName") String operationName, + @RequestBody OperationOverrideDTO operationOverride, UserInfo userInfo) { + log.debug("Updating operation {} of service {}", operationName, serviceId); + log.debug("ParameterConstraints?: {}", operationOverride.getParameterConstraints()); + boolean result = serviceService.updateOperation(serviceId, operationName, operationOverride.getDispatcher(), + operationOverride.getDispatcherRules(), operationOverride.getDefaultDelay(), + operationOverride.getParameterConstraints(), userInfo); + if (result) { + return new ResponseEntity<>(HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @DeleteMapping(value = "/services/{id}") + public ResponseEntity deleteService(@PathVariable("id") String serviceId, UserInfo userInfo) { + log.debug("Removing service with id {}", serviceId); + boolean result = serviceService.deleteService(serviceId, userInfo); + if (result) { + return new ResponseEntity<>(HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/SoapController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/SoapController.java new file mode 100644 index 000000000..b4e8dc8b4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/SoapController.java @@ -0,0 +1,424 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.repository.ServiceStateRepository; +import io.github.microcks.service.ProxyService; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.dispatcher.FallbackSpecification; +import io.github.microcks.util.dispatcher.ProxyFallbackSpecification; +import io.github.microcks.util.script.ScriptEngineBinder; +import io.github.microcks.service.ServiceStateStore; +import io.github.microcks.util.soap.SoapMessageValidator; +import io.github.microcks.util.soapui.SoapUIXPathBuilder; + +import org.apache.commons.lang3.RandomUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ResponseStatusException; +import org.xml.sax.InputSource; + +import jakarta.servlet.http.HttpServletRequest; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.xml.namespace.QName; +import javax.xml.xpath.XPathExpression; +import java.io.StringReader; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A controller for mocking Soap responses. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/soap") +public class SoapController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(SoapController.class); + + /** Regular expression pattern for capturing Soap Operation name from body. */ + private static final Pattern OPERATION_CAPTURE_PATTERN = Pattern + .compile("(.*):Body>(\\s*)<((\\w+):|)(?\\w+)(.*)(/)?>(.*)", Pattern.DOTALL); + /** Regular expression replacement pattern for chnging SoapUI {@code ${}} in Microcks {@code {{}}}. */ + private static final Pattern SOAPUI_TEMPLATE_PARAMETER_REPLACE_PATTERN = Pattern + .compile("\\$\\{\s*([a-zA-Z0-9-_]+)\s*\\}", Pattern.DOTALL); + + private final ServiceRepository serviceRepository; + private final ServiceStateRepository serviceStateRepository; + private final ResponseRepository responseRepository; + private final ResourceRepository resourceRepository; + private final ApplicationContext applicationContext; + private final ProxyService proxyService; + + private ScriptEngine scriptEngine; + + @Value("${mocks.enable-invocation-stats}") + private Boolean enableInvocationStats; + + @Value("${validation.resourceUrl}") + private String resourceUrl; + + + /** + * Build a SoapController with required dependencies. + * @param serviceRepository The repository to access services definitions + * @param serviceStateRepository The repository to access service state + * @param responseRepository The repository to access responses definitions + * @param resourceRepository The repository to access resources artifacts + * @param applicationContext The Spring application context + * @param proxyService The proxy to external URLs or services + */ + public SoapController(ServiceRepository serviceRepository, ServiceStateRepository serviceStateRepository, + ResponseRepository responseRepository, ResourceRepository resourceRepository, + ApplicationContext applicationContext, ProxyService proxyService) { + this.serviceRepository = serviceRepository; + this.serviceStateRepository = serviceStateRepository; + this.responseRepository = responseRepository; + this.resourceRepository = resourceRepository; + this.applicationContext = applicationContext; + this.proxyService = proxyService; + this.scriptEngine = new ScriptEngineManager().getEngineByExtension("groovy"); + } + + + @PostMapping(value = "/{service}/{version}/**") + public ResponseEntity execute(@PathVariable("service") String serviceName, + @PathVariable("version") String version, @RequestParam(value = "validate", required = false) Boolean validate, + @RequestParam(value = "delay", required = false) Long delay, @RequestBody String body, + @RequestHeader HttpHeaders headers, HttpServletRequest request, HttpMethod method) { + log.info("Servicing mock response for service [{}, {}]", serviceName, version); + log.debug("Request body: {}", body); + + long startTime = System.currentTimeMillis(); + + // Setup serviceAndVersion for proxy dispatchers + String serviceAndVersion = MockControllerCommons.composeServiceAndVersion(serviceName, version); + + // If serviceName was encoded with '+' instead of '%20', replace them. + if (serviceName.contains("+")) { + serviceName = serviceName.replace('+', ' '); + } + log.debug("Service name: {}", serviceName); + // Retrieve service and correct operation. + Service service = serviceRepository.findByNameAndVersion(serviceName, version); + if (service == null) { + return new ResponseEntity<>( + String.format("The service %s with version %s does not exist!", serviceName, version), + HttpStatus.NOT_FOUND); + } + Operation rOperation = null; + + // Enhancement : retrieve SOAPAction from request headers + String action = extractSoapAction(request); + log.debug("Extracted SOAP action from headers: {}", action); + + if (StringUtils.hasText(action)) { + for (Operation operation : service.getOperations()) { + if (action.equals(operation.getAction())) { + rOperation = operation; + log.info("Found valid operation {}", rOperation.getName()); + break; + } + } + } + + // Enhancement : if not found, try getting operation from soap:body directly! + if (rOperation == null) { + String operationName = extractOperationName(body); + if (!StringUtils.hasText(action)) { + // if the action is not in the header, we override it with the action from the body + action = operationName; + } + log.debug("Extracted operation name from payload: {}", operationName); + + if (operationName != null) { + for (Operation operation : service.getOperations()) { + if (operationName.equals(operation.getInputName()) || operationName.equals(operation.getName())) { + rOperation = operation; + log.debug("Found valid operation {}", rOperation.getName()); + break; + } + } + } + } + + // Now processing the request and send a response. + if (rOperation != null) { + log.debug("Found a valid operation with rules: {}", rOperation.getDispatcherRules()); + + if (validate != null && validate) { + log.debug("Soap message validation is turned on, validating..."); + + List wsdlResources = resourceRepository.findByServiceIdAndType(service.getId(), + ResourceType.WSDL); + if (wsdlResources.isEmpty()) { + return new ResponseEntity<>( + String.format("The service %s with version %s does not have a wsdl!", serviceName, version), + HttpStatus.PRECONDITION_FAILED); + } + Resource wsdlResource = wsdlResources.get(0); + List errors = SoapMessageValidator.validateSoapMessage(wsdlResource.getContent(), + new QName(service.getXmlNS(), rOperation.getInputName()), body, resourceUrl); + + log.debug("SoapBody validation errors: {}", errors.size()); + + // Return a 400 http code with errors. + if (errors != null && !errors.isEmpty()) { + return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); + } + } + + // We must find dispatcher and its rules. Default to operation ones but + // if we have a Fallback this is the one who is holding the first pass rules. + String dispatcher = rOperation.getDispatcher(); + String dispatcherRules = rOperation.getDispatcherRules(); + FallbackSpecification fallback = MockControllerCommons.getFallbackIfAny(rOperation); + if (fallback != null) { + dispatcher = fallback.getDispatcher(); + dispatcherRules = fallback.getDispatcherRules(); + } + ProxyFallbackSpecification proxyFallback = MockControllerCommons.getProxyFallbackIfAny(rOperation); + if (proxyFallback != null) { + dispatcher = proxyFallback.getDispatcher(); + dispatcherRules = proxyFallback.getDispatcherRules(); + } + + Response response = null; + DispatchContext dispatchContext = null; + + try { + // Depending on dispatcher, evaluate request with rules. + if (DispatchStyles.QUERY_MATCH.equals(dispatcher)) { + dispatchContext = getDispatchCriteriaFromXPathEval(dispatcherRules, body); + } else if (DispatchStyles.SCRIPT.equals(dispatcher)) { + dispatchContext = getDispatchCriteriaFromScriptEval(service, dispatcherRules, body, request); + } else if (DispatchStyles.RANDOM.equals(dispatcher)) { + dispatchContext = new DispatchContext(DispatchStyles.RANDOM, null); + } else if (DispatchStyles.PROXY.equals(dispatcher)) { + dispatchContext = new DispatchContext(DispatchStyles.PROXY, null); + } else { + return new ResponseEntity<>(String.format("The dispatch %s is not supported!", dispatcher), + HttpStatus.NOT_FOUND); + } + } catch (ResponseStatusException e) { + return new ResponseEntity<>(e.getMessage(), e.getStatusCode()); + } + + log.debug("Dispatch criteria for finding response is {}", dispatchContext.dispatchCriteria()); + List responses = responseRepository.findByOperationIdAndDispatchCriteria( + IdBuilder.buildOperationId(service, rOperation), dispatchContext.dispatchCriteria()); + + if (responses.isEmpty() && fallback != null) { + // If we've found nothing and got a fallback, that's the moment! + responses = responseRepository.findByOperationIdAndName(IdBuilder.buildOperationId(service, rOperation), + fallback.getFallback()); + } + + Optional proxyUrl = MockControllerCommons.getProxyUrlIfProxyIsNeeded(dispatcher, dispatcherRules, + MockControllerCommons.extractResourcePath(request, serviceAndVersion), proxyFallback, request, + responses.isEmpty() ? null : responses.get(0)); + if (proxyUrl.isPresent()) { + // If we've got a proxyUrl, that's the moment! + return proxyService.callExternal(proxyUrl.get(), method, headers, body); + } + + if (!responses.isEmpty()) { + int idx = DispatchStyles.RANDOM.equals(dispatcher) ? RandomUtils.nextInt(0, responses.size()) : 0; + response = responses.get(idx); + } else { + return new ResponseEntity<>( + String.format("The response %s does not exist!", dispatchContext.dispatchCriteria()), + HttpStatus.BAD_REQUEST); + } + + // Set Content-Type to "text/xml". + HttpHeaders responseHeaders = new HttpHeaders(); + + // Check to see if we are processing a SOAP 1.2 request + if (request.getContentType() == null || request.getContentType().startsWith("application/soap+xml")) { + // we are; set Content-Type to "application/soap+xml" + responseHeaders.setContentType(MediaType.valueOf("application/soap+xml;charset=UTF-8")); + } else { + // Set Content-Type to "text/xml". + responseHeaders.setContentType(MediaType.valueOf("text/xml;charset=UTF-8")); + } + + // Render response content before waiting and returning. + // Response coming from SoapUI may contain specific template markers, we have to convert them first. + response.setContent(convertSoapUITemplate(response.getContent())); + String responseContent = MockControllerCommons.renderResponseContent(body, null, request, + dispatchContext.requestContext(), response); + + // Setting delay to default one if not set. + delay = MockControllerCommons.getDelay(headers, delay); + if (delay == null && rOperation.getDefaultDelay() != null) { + delay = rOperation.getDefaultDelay(); + } + MockControllerCommons.waitForDelay(startTime, delay); + + // Publish an invocation event before returning if enabled. + if (Boolean.TRUE.equals(enableInvocationStats)) { + MockControllerCommons.publishMockInvocation(applicationContext, this, service, response, startTime); + } + + if (response.isFault()) { + return new ResponseEntity<>(responseContent, responseHeaders, HttpStatus.INTERNAL_SERVER_ERROR); + } + return new ResponseEntity<>(responseContent, responseHeaders, HttpStatus.OK); + } + + return new ResponseEntity<>(String.format("The operation %s does not exist!", action), HttpStatus.NOT_FOUND); + } + + /** + * Check if given SOAP payload has a correct structure for given operation name. + * @param payload SOAP payload to check structure + * @param operationName Name of operation to check structure against + * @return True if payload is correct for operation, false otherwise. + */ + protected static boolean hasPayloadCorrectStructureForOperation(String payload, String operationName) { + String openingPattern = "(.*):Body>(\\s*)<((\\w+):|)" + operationName + "(.*)>(.*)"; + String closingPattern = "(.*)(\\s*)(.*)"; + String shortPattern = "(.*):Body>(\\s*)<((\\w+):|)" + operationName + "(.*)/>(\\s*)(.*)"; + + Pattern op = Pattern.compile(openingPattern, Pattern.DOTALL); + Pattern cp = Pattern.compile(closingPattern, Pattern.DOTALL); + Pattern sp = Pattern.compile(shortPattern, Pattern.DOTALL); + return (op.matcher(payload).matches() && cp.matcher(payload).matches()) || sp.matcher(payload).matches(); + } + + /** + * Extract operation name from payload. Indeed we extract the wrapping element name inside SOAP body. + * @param payload SOAP payload to extract from + * @return The wrapping Xml element name with body if matches SOAP. Null otherwise. + */ + protected static String extractOperationName(String payload) { + Matcher matcher = OPERATION_CAPTURE_PATTERN.matcher(payload); + if (matcher.find()) { + return matcher.group("operation"); + } + return null; + } + + /** + * Extraction Soap Action from request headers if specified. + * @param request The incoming HttpServletRequest to extract from + * @return The found Soap action if any. Can be null. + */ + protected String extractSoapAction(HttpServletRequest request) { + String action = null; + // If Soap 1.2, SOAPAction is in Content-Type header. + String contentType = request.getContentType(); + if (contentType != null && contentType.startsWith("application/soap+xml") && contentType.contains("action=")) { + action = contentType.substring(contentType.indexOf("action=") + 7); + // Remove any other optional param in content-type if any. + if (action.contains(";")) { + action = action.substring(0, action.indexOf(";")); + } + } else { + // Else, SOAPAction is in dedicated header. + action = request.getHeader("SOAPAction"); + } + // Sanitize action value if any. + if (action != null) { + // Remove starting double-quote if any. + if (action.startsWith("\"")) { + action = action.substring(1); + } + // Remove ending double-quote if any. + if (action.endsWith("\"")) { + action = action.substring(0, action.length() - 1); + } + } + return action; + } + + /** Build a dispatch context after a XPath evaluation coming from rules. */ + private DispatchContext getDispatchCriteriaFromXPathEval(String dispatcherRules, String body) { + try { + // Evaluating request regarding XPath build with operation dispatcher rules. + XPathExpression xpath = SoapUIXPathBuilder.buildXPathMatcherFromRules(dispatcherRules); + return new DispatchContext(xpath.evaluate(new InputSource(new StringReader(body))), null); + } catch (Exception e) { + log.error("Error during Xpath evaluation", e); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, + "Error during Xpath evaluation: " + e.getMessage()); + } + } + + /** Build a dispatch context after a Groovy script evaluation coming from rules. */ + private DispatchContext getDispatchCriteriaFromScriptEval(Service service, String dispatcherRules, String body, + HttpServletRequest request) { + Map requestContext = new HashMap<>(); + try { + // Evaluating request with script coming from operation dispatcher rules. + String script = ScriptEngineBinder.ensureSoapUICompatibility(dispatcherRules); + ScriptContext scriptContext = ScriptEngineBinder.buildEvaluationContext(scriptEngine, body, requestContext, + new ServiceStateStore(serviceStateRepository, service.getId()), request); + + return new DispatchContext((String) scriptEngine.eval(script, scriptContext), requestContext); + } catch (Exception e) { + log.error("Error during Script evaluation", e); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, + "Error during Script evaluation: " + e.getMessage()); + } + } + + /** + * Convert a SoapUI template like {@code ${myParam}} into a Microcks one that could be later + * rendered through the template engine. ie: {@code {{ myParam }}}. Supports multi-lines and + * multi-parameters replacement. + * @param responseTemplate The SoapUI template to convert + * @return The converted template or the original template if not recognized as a SoapUI one. + */ + protected static String convertSoapUITemplate(String responseTemplate) { + if (responseTemplate.contains("${")) { + return SOAPUI_TEMPLATE_PARAMETER_REPLACE_PATTERN.matcher(responseTemplate).replaceAll("{{ $1 }}"); + } + return responseTemplate; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/TestController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/TestController.java new file mode 100644 index 000000000..697d8950e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/TestController.java @@ -0,0 +1,203 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.OperationsHeaders; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.SecretRef; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestCaseResult; +import io.github.microcks.domain.TestOptionals; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestRunnerType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.repository.SecretRepository; +import io.github.microcks.repository.TestResultRepository; +import io.github.microcks.service.MessageService; +import io.github.microcks.service.ServiceService; +import io.github.microcks.service.TestService; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.web.dto.HeaderDTO; +import io.github.microcks.web.dto.TestCaseReturnDTO; +import io.github.microcks.web.dto.TestRequestDTO; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A Rest controller for API defined on test results. + * @author laurent + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/api") +public class TestController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(TestController.class); + + private final TestResultRepository testResultRepository; + private final SecretRepository secretRepository; + private final TestService testService; + private final MessageService messageService; + private final ServiceService serviceService; + + + /** + * Create a TestController with required dependencies. + * @param testService Service to launch tests and report results + * @param messageService Service to report new test messages + * @param testResultRepository Get access to test results + * @param serviceService Service to get access to Services + * @param secretRepository Get access to Secrets + */ + public TestController(TestService testService, MessageService messageService, ServiceService serviceService, + TestResultRepository testResultRepository, SecretRepository secretRepository) { + this.testService = testService; + this.messageService = messageService; + this.serviceService = serviceService; + this.testResultRepository = testResultRepository; + this.secretRepository = secretRepository; + } + + @GetMapping(value = "/tests/service/{serviceId}") + public List listTestsByService(@PathVariable("serviceId") String serviceId, + @RequestParam(value = "page", required = false, defaultValue = "0") int page, + @RequestParam(value = "size", required = false, defaultValue = "20") int size) { + log.debug("Getting tests list for service {}, page {} and size {}", serviceId, page, size); + return testResultRepository.findByServiceId(serviceId, + PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "testNumber"))); + } + + @GetMapping(value = "/tests/service/{serviceId}/count") + public Map countTestsByService(@PathVariable("serviceId") String serviceId) { + log.debug("Counting tests for service..."); + Map counter = new HashMap<>(); + counter.put("counter", testResultRepository.countByServiceId(serviceId)); + return counter; + } + + @PostMapping(value = "/tests") + public ResponseEntity createTest(@RequestBody TestRequestDTO test) { + log.debug("Creating new test for {} on endpoint {}", test.getServiceId(), test.getTestEndpoint()); + // serviceId may have the form of : or just + Service service = serviceService.getServiceById(test.getServiceId()); + TestRunnerType testRunner = TestRunnerType.valueOf(test.getRunnerType()); + + // Build additional header entries for operations. + OperationsHeaders operationsHeaders = buildOperationsHeaders(test.getOperationsHeaders()); + + // Deal with Secret check and retrieval if specified. + SecretRef secretRef = null; + if (test.getSecretName() != null) { + List secrets = secretRepository.findByName(test.getSecretName()); + if (!secrets.isEmpty()) { + secretRef = new SecretRef(secrets.getFirst().getId(), secrets.getFirst().getName()); + } + // TODO: should we return an error and refuse creating the test without secret ? + } + + TestOptionals testOptionals = new TestOptionals(secretRef, test.getTimeout(), test.getFilteredOperations(), + operationsHeaders, test.getOAuth2Context()); + TestResult testResult = testService.launchTests(service, test.getTestEndpoint(), testRunner, testOptionals); + return new ResponseEntity<>(testResult, HttpStatus.CREATED); + } + + @GetMapping(value = "/tests/{id}") + public ResponseEntity getTestResult(@PathVariable("id") String testResultId) { + log.debug("Getting TestResult with id {}", testResultId); + return new ResponseEntity<>(testResultRepository.findById(testResultId).orElse(null), HttpStatus.OK); + } + + @GetMapping(value = "tests/{id}/messages/{testCaseId}") + public List getMessagesForTestCase(@PathVariable("id") String testResultId, + @PathVariable("testCaseId") String testCaseId) { + // We may have testCaseId being URLEncoded, with forbidden '/' replaced by '_' so unwrap id. + // Switched form _ to ! in replacement as less commonly used in URL parameters, in line with other frameworks e.g. Drupal + testCaseId = URLDecoder.decode(testCaseId, StandardCharsets.UTF_8); + testCaseId = testCaseId.replace('!', '/'); + log.debug("Getting messages for testCase {} on test {}", testCaseId, testResultId); + return messageService.getRequestResponseByTestCase(testCaseId); + } + + @GetMapping(value = "tests/{id}/events/{testCaseId}") + public List getEventMessagesForTestCase(@PathVariable("id") String testResultId, + @PathVariable("testCaseId") String testCaseId) { + // We may have testCaseId being URLEncoded, with forbidden '/' replaced by '_' so unwrap id. + // Switched form _ to ! in replacement as less commonly used in URL parameters, in line with other frameworks e.g. Drupal + testCaseId = URLDecoder.decode(testCaseId, StandardCharsets.UTF_8); + testCaseId = testCaseId.replace('!', '/'); + log.debug("Getting messages for testCase {} on test {}", testCaseId, testResultId); + return messageService.getEventByTestCase(testCaseId); + } + + @PostMapping(value = "tests/{id}/testCaseResult") + public ResponseEntity reportTestCaseResult(@PathVariable("id") String testResultId, + @RequestBody TestCaseReturnDTO testCaseReturn) { + log.debug("Reporting testCase results on test {}", testResultId); + TestCaseResult testCaseResult = testService.reportTestCaseResult(testResultId, testCaseReturn.getOperationName(), + testCaseReturn.getTestReturns()); + if (testCaseResult != null) { + return new ResponseEntity<>(testCaseResult, HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + /** + * Build OperationsHeaders domain object from basic Map. Key is operation name, Value is a header data transfer + * object. + */ + private OperationsHeaders buildOperationsHeaders(Map> operationsHeaders) { + OperationsHeaders result = new OperationsHeaders(); + if (operationsHeaders != null) { + // Now browse different operations (globals included). + for (Map.Entry> operationsHeadersEntry : operationsHeaders.entrySet()) { + String operationName = operationsHeadersEntry.getKey(); + List operationHeaders = operationsHeadersEntry.getValue(); + Set
headers = new HashSet<>(); + // Browse each header entry. Values are comma separated. + for (HeaderDTO operationHeadersEntry : operationHeaders) { + String[] headerValues = operationHeadersEntry.getValues().split(","); + Header header = new Header(); + header.setName(operationHeadersEntry.getName()); + header.setValues(new HashSet<>(Arrays.asList(headerValues))); + headers.add(header); + } + result.put(operationName, headers); + } + return result; + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/UploadArtifactController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/UploadArtifactController.java new file mode 100644 index 000000000..3128168ae --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/UploadArtifactController.java @@ -0,0 +1,182 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Secret; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.SecretRepository; +import io.github.microcks.service.ArtifactInfo; +import io.github.microcks.service.ServiceService; +import io.github.microcks.util.HTTPDownloader; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.ReferenceResolver; +import io.github.microcks.util.RelativeReferenceURLBuilderFactory; +import io.github.microcks.util.SafeLogger; +import io.github.microcks.util.SimpleReferenceURLBuilder; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; + +/** + * This is a controller for managing import of uploaded or downloaded artifact contract files. + * @author laurent + */ +@RestController +@RequestMapping("/api") +public class UploadArtifactController { + + /** A safe logger for filtering user-controlled data in diagnostic messages. */ + private static final SafeLogger log = SafeLogger.getLogger(UploadArtifactController.class); + + private final ServiceService serviceService; + private final SecretRepository secretRepository; + + @Value("${default-artifacts-repository.url:#{null}}") + private Optional defaultArtifactsRepositoryUrl; + + /** + * Build a new UploadArtifactController with its dependencies. + * @param serviceService to perform business logic on Services + * @param secretRepository to retrieve requested Secrets + */ + public UploadArtifactController(ServiceService serviceService, SecretRepository secretRepository) { + this.serviceService = serviceService; + this.secretRepository = secretRepository; + } + + @PostMapping(value = "/artifact/download") + public ResponseEntity importArtifact(@RequestParam(value = "url", required = true) String url, + @RequestParam(value = "mainArtifact", defaultValue = "true") boolean mainArtifact, + @RequestParam(value = "secretName", required = false) String secretName) { + if (!url.isEmpty()) { + List services = null; + + Secret secret = null; + if (secretName != null) { + secret = secretRepository.findByName(secretName).stream().findFirst().orElse(null); + log.debug("Secret {} was requested. Have we found it? {}", secretName, (secret != null)); + } + + File localFile = null; + try { + // Download remote to local file before import. + HTTPDownloader.FileAndHeaders fileAndHeaders = HTTPDownloader.handleHTTPDownloadToFileAndHeaders(url, + secret, true); + localFile = fileAndHeaders.getLocalFile(); + + // Now try importing services. + services = serviceService.importServiceDefinition(localFile, + new ReferenceResolver(url, secret, true, + RelativeReferenceURLBuilderFactory + .getRelativeReferenceURLBuilder(fileAndHeaders.getResponseHeaders())), + new ArtifactInfo(url, mainArtifact)); + } catch (IOException ioe) { + log.error("Exception while retrieving remote item {}", url, ioe); + return new ResponseEntity<>("Exception while retrieving remote item", HttpStatus.INTERNAL_SERVER_ERROR); + } catch (MockRepositoryImportException mrie) { + log.error("Exception while reading remote item {}", url, mrie); + return new ResponseEntity<>(mrie.getMessage(), HttpStatus.BAD_REQUEST); + } finally { + // Cleanup and remove local file. + if (localFile != null) { + localFile.delete(); + } + } + + if (services != null && !services.isEmpty()) { + return new ResponseEntity<>( + "{\"name\": \"" + services.get(0).getName() + ":" + services.get(0).getVersion() + "\"}", + HttpStatus.CREATED); + } + } + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @PostMapping(value = "/artifact/upload") + public ResponseEntity importArtifact(@RequestParam(value = "file") MultipartFile file, + @RequestParam(value = "mainArtifact", defaultValue = "true") boolean mainArtifact) { + if (!file.isEmpty()) { + log.debug("Content type of {} is {}", file.getOriginalFilename(), file.getContentType()); + + List services = null; + String localFile = null; + try { + // Save upload to local file before import. + localFile = System.getProperty("java.io.tmpdir") + "/microcks-" + System.currentTimeMillis() + ".artifact"; + + try (ReadableByteChannel rbc = Channels.newChannel(file.getInputStream()); + FileOutputStream fos = new FileOutputStream(localFile)) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } + + // Now try importing services. + ReferenceResolver referenceResolver = getDefaultReferenceResolver(file.getOriginalFilename()); + services = serviceService.importServiceDefinition(new File(localFile), referenceResolver, + new ArtifactInfo(file.getOriginalFilename(), mainArtifact)); + + + } catch (IOException ioe) { + log.error("Exception while writing uploaded item {}", file.getOriginalFilename(), ioe); + return new ResponseEntity<>("Exception while writing uploaded item", HttpStatus.INTERNAL_SERVER_ERROR); + } catch (MockRepositoryImportException mrie) { + log.error("Exception while reading uploaded item {}", file.getOriginalFilename(), mrie); + return new ResponseEntity<>(mrie.getMessage(), HttpStatus.BAD_REQUEST); + } finally { + // Cleanup and remove local file. + if (localFile != null) { + Paths.get(localFile).toFile().delete(); + } + } + + if (services != null && !services.isEmpty()) { + return new ResponseEntity<>(services.get(0).getName() + ":" + services.get(0).getVersion(), + HttpStatus.CREATED); + } + } + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + private ReferenceResolver getDefaultReferenceResolver(String fileName) { + log.debug("defaultArtifactsRepositoryUrl is {}", defaultArtifactsRepositoryUrl); + if (defaultArtifactsRepositoryUrl.isPresent()) { + String repositoryUrl = defaultArtifactsRepositoryUrl.get(); + String baseRepositoryUrl = repositoryUrl.endsWith("/") ? repositoryUrl + fileName + : repositoryUrl + "/" + fileName; + ReferenceResolver resolver = new ReferenceResolver(baseRepositoryUrl, null, true, + new SimpleReferenceURLBuilder()); + if (!repositoryUrl.startsWith("http")) { + resolver.setCleanResolvedFiles(false); + } + return resolver; + } + return null; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/VersionInfoController.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/VersionInfoController.java new file mode 100644 index 000000000..08e2d5b67 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/VersionInfoController.java @@ -0,0 +1,76 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * A Rest controller for dispatching Microcks version information to frontend. + * @author laurent + */ +@RestController +@RequestMapping("/api/version") +@PropertySource("classpath:version.properties") +public class VersionInfoController { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(VersionInfoController.class); + + @Value("${versionId}") + private String versionId = null; + + @Value("${buildTimestamp}") + private String buildTimestamp = null; + + @GetMapping(value = "/info") + public ResponseEntity getConfig() { + final VersionInfo info = new VersionInfo(versionId, buildTimestamp); + + log.debug("Returning '{}' version information", info.getVersionId()); + + return new ResponseEntity<>(info, HttpStatus.OK); + } + + private class VersionInfo { + @JsonProperty("versionId") + private String versionId; + + @JsonProperty("buildTimestamp") + private String buildTimestamp; + + public VersionInfo(String versionId, String buildTimestamp) { + this.versionId = versionId; + this.buildTimestamp = buildTimestamp; + } + + public String getVersionId() { + return versionId; + } + + public String getBuildTimestamp() { + return buildTimestamp; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/GenericResourceServiceDTO.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/GenericResourceServiceDTO.java new file mode 100644 index 000000000..6ac7ebc72 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/GenericResourceServiceDTO.java @@ -0,0 +1,63 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.dto; + +/** + * This is DTO bean for handling creation of Service for GenericResource and dynamic mocking. + * @author laurent + */ +public class GenericResourceServiceDTO { + + private String name; + private String version; + private String resource; + private String referencePayload; + + public GenericResourceServiceDTO() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getReferencePayload() { + return referencePayload; + } + + public void setReferencePayload(String referencePayload) { + this.referencePayload = referencePayload; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/HeaderDTO.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/HeaderDTO.java new file mode 100644 index 000000000..45a6e28d5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/HeaderDTO.java @@ -0,0 +1,42 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.dto; + +/** + * Data Transfer object for basic header with its values (comma separated string). + * @author laurent + */ +public class HeaderDTO { + + private String name; + private String values; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValues() { + return values; + } + + public void setValues(String values) { + this.values = values; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/OperationOverrideDTO.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/OperationOverrideDTO.java new file mode 100644 index 000000000..78a25bf35 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/OperationOverrideDTO.java @@ -0,0 +1,64 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.dto; + +import io.github.microcks.domain.ParameterConstraint; + +import java.util.Set; + +/** + * Data Transfer object for grouping the mutable properties of an Operation. + * @author laurent + */ +public class OperationOverrideDTO { + + private String dispatcher; + private String dispatcherRules; + private Long defaultDelay; + private Set parameterConstraints; + + public String getDispatcher() { + return dispatcher; + } + + public void setDispatcher(String dispatcher) { + this.dispatcher = dispatcher; + } + + public String getDispatcherRules() { + return dispatcherRules; + } + + public void setDispatcherRules(String dispatcherRules) { + this.dispatcherRules = dispatcherRules; + } + + public Long getDefaultDelay() { + return defaultDelay; + } + + public void setDefaultDelay(Long defaultDelay) { + this.defaultDelay = defaultDelay; + } + + public Set getParameterConstraints() { + return parameterConstraints; + } + + public void setParameterConstraints(Set parameterConstraints) { + this.parameterConstraints = parameterConstraints; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/TestCaseReturnDTO.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/TestCaseReturnDTO.java new file mode 100644 index 000000000..620cd3966 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/TestCaseReturnDTO.java @@ -0,0 +1,38 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.dto; + +import io.github.microcks.domain.TestReturn; + +import java.util.List; + +/** + * Data Transfer object for grouping base information to report a test case run (and thus create a TestCaseResult). + * @author laurent + */ +public class TestCaseReturnDTO { + + private String operationName; + private List testReturns; + + public String getOperationName() { + return operationName; + } + + public List getTestReturns() { + return testReturns; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/TestRequestDTO.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/TestRequestDTO.java new file mode 100644 index 000000000..7d48cc34e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/dto/TestRequestDTO.java @@ -0,0 +1,76 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.dto; + +import io.github.microcks.domain.OAuth2ClientContext; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * Data Transfer object for grouping base information to launch a test (and thus create a TestResult). + * @author laurent + */ +public class TestRequestDTO { + + private String serviceId; + private String testEndpoint; + private String runnerType; + private String secretName; + private Long timeout; + private List filteredOperations; + private Map> operationsHeaders; + @JsonProperty("oAuth2Context") + private OAuth2ClientContext oAuth2Context; + + public String getServiceId() { + return serviceId; + } + + public String getTestEndpoint() { + return testEndpoint; + } + + public String getRunnerType() { + return runnerType; + } + + public String getSecretName() { + return secretName; + } + + public Long getTimeout() { + return timeout; + } + + public List getFilteredOperations() { + return filteredOperations; + } + + public Map> getOperationsHeaders() { + return operationsHeaders; + } + + public OAuth2ClientContext getOAuth2Context() { + return oAuth2Context; + } + + public void setOAuth2Context(OAuth2ClientContext oAuth2Context) { + this.oAuth2Context = oAuth2Context; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/CorsFilter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/CorsFilter.java new file mode 100644 index 000000000..ea17cc003 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/CorsFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Simple filter for enabling CORS on http request. See https://spring.io/guides/gs/rest-service-cors/. + * @author laurent + */ +public class CorsFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse response = (HttpServletResponse) servletResponse; + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE"); + response.setHeader("Access-Control-Max-Age", "3600"); + response.setHeader("Access-Control-Allow-Headers", "*"); + chain.doFilter(servletRequest, servletResponse); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/DynamicCorsFilter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/DynamicCorsFilter.java new file mode 100644 index 000000000..8577bd602 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/DynamicCorsFilter.java @@ -0,0 +1,97 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; + +/** + * Servlet filter that set the response CORS headers accordingly the configuration and incoming request. + */ +public class DynamicCorsFilter implements Filter { + private final String corsAllowedOrigins; + private final Boolean corsAllowCredentials; + + /** + * Build a new DynamicCorsFilter. + * + * @param corsAllowedOrigins Allowed origin forced if nothing found in incoming request + * @param corsAllowCredentials Whether to set allow credentials + */ + public DynamicCorsFilter(String corsAllowedOrigins, Boolean corsAllowCredentials) { + this.corsAllowedOrigins = corsAllowedOrigins; + this.corsAllowCredentials = corsAllowCredentials; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse response = (HttpServletResponse) servletResponse; + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + String origin = httpRequest.getHeader("Origin"); + if (origin == null) { + origin = corsAllowedOrigins; + } + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE"); + response.setHeader("Access-Control-Max-Age", "3600"); + response.setHeader("Access-Control-Allow-Headers", getHeadersString(httpRequest)); + if (Boolean.TRUE.equals(corsAllowCredentials)) { + response.setHeader("Access-Control-Allow-Credentials", "true"); + } + chain.doFilter(servletRequest, servletResponse); + } + + /** + * Build a string with all the headers from the incoming request. + * + * This method will also add the headers from the "Access-Control-Request-Headers" header if present. + * + * @param httpRequest + * @return + */ + private String getHeadersString(HttpServletRequest httpRequest) { + var headerNamesList = new HashSet(); + Enumeration headerNamesEnumeration = httpRequest.getHeaderNames(); + + if (headerNamesEnumeration != null) { + headerNamesList.addAll(Collections.list(headerNamesEnumeration)); + } + + String accessControlRequestHeaders = httpRequest.getHeader("Access-Control-Request-Headers"); + if (accessControlRequestHeaders != null) { + List headerNames = Arrays.stream(accessControlRequestHeaders.split(",")).map(String::trim).toList(); + headerNamesList.addAll(headerNames); + } + + if (headerNamesList.isEmpty()) { + return "*"; + } + return String.join(", ", headerNamesList); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/StaticResourcesFilter.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/StaticResourcesFilter.java new file mode 100644 index 000000000..b42c9392b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/java/io/github/microcks/web/filter/StaticResourcesFilter.java @@ -0,0 +1,63 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.filter; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Servlet filter for adapting static resources access path when in production mode (cause everything is now into /dist + * directory !) + * @author laurent + */ +public class StaticResourcesFilter implements Filter { + + /** A simple logger for diagnostic messages. */ + private static Logger log = LoggerFactory.getLogger(StaticResourcesFilter.class); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + String contextPath = httpRequest.getContextPath(); + String requestURI = httpRequest.getRequestURI(); + requestURI = StringUtils.substringAfter(requestURI, contextPath); + if (StringUtils.equals("/", requestURI)) { + requestURI = "/index.html"; + } + String newURI = "/dist" + requestURI; + log.debug("Dispatching to URI: {}", newURI); + servletRequest.getRequestDispatcher(newURI).forward(servletRequest, servletResponse); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/application.properties b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/application.properties new file mode 100644 index 000000000..c8a0388d6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/application.properties @@ -0,0 +1,86 @@ +# Application configuration properties +spring.threads.virtual.enabled=true +spring.servlet.multipart.max-file-size=${MAX_UPLOAD_FILE_SIZE:2MB} +spring.jackson.serialization.write-dates-as-timestamps=true +spring.jackson.default-property-inclusion=non_null + +server.compression.enabled=true +server.forward-headers-strategy=NATIVE + +#graphql.parser.max-characters=100000000 +#graphql.parser.max-tokens=100000 + +management.endpoints.enabled-by-default=false +management.endpoints.jmx.exposure.exclude=* +management.endpoints.web.exposure.include=* +management.endpoint.metrics.enabled=true +management.endpoint.prometheus.enabled=true +management.metrics.export.prometheus.enabled=true +management.metrics.distribution.percentiles-histogram.http.server.requests=true +management.metrics.distribution.slo.http.server.requests=1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 2500ms, 5000ms, 10000ms + +tests-callback.url=${TEST_CALLBACK_URL:http://localhost:8080} +postman-runner.url=${POSTMAN_RUNNER_URL:http://localhost:3000} +async-minion.url=${ASYNC_MINION_URL:http://localhost:8081} +default-artifacts-repository.url=${DEFAULT_ARTIFACTS_REPOSITORY_URL:#{null}} + +network.proxyHost=${PROXY_HOST:} +network.proxyPort=${PROXY_PORT:} +network.proxyUsername=${PROXY_USERNAME:} +network.proxyPassword=${PROXY_PASSWORD:} +network.nonProxyHosts=${PROXY_EXCLUDE:localhost|127.0.0.1|*.svc.cluster.local} + +validation.resourceUrl=http://localhost:8080/api/resources/ +services.update.interval=${SERVICES_UPDATE_INTERVAL:0 0 0/2 * * *} + +mocks.enable-invocation-stats=${ENABLE_INVOCATION_STATS:true} +mocks.rest.enable-cors-policy=${ENABLE_CORS_POLICY:true} +mocks.rest.cors.allowedOrigins=${CORS_REST_ALLOWED_ORIGINS:*} +mocks.rest.cors.allowCredentials=${CORS_REST_ALLOW_CREDENTIALS:false} + + +# Spring Security adapter configuration properties +spring.security.oauth2.client.registration.keycloak.client-id=microcks-app +spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.keycloak.scope=openid,profile +spring.security.oauth2.client.provider.keycloak.issuer-uri=${KEYCLOAK_URL:http://localhost:8180}/realms/${keycloak.realm} +spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username +spring.security.oauth2.resourceserver.jwt.issuer-uri=${KEYCLOAK_URL:http://localhost:8180}/realms/${keycloak.realm} + +# Keycloak adapter configuration properties +keycloak.enabled=${KEYCLOAK_ENABLED:true} +keycloak.auth-server-url=${KEYCLOAK_URL:http://localhost:8180} +keycloak.realm=microcks +keycloak.resource=microcks-app +keycloak.use-resource-role-mappings=true +keycloak.bearer-only=true +keycloak.ssl-required=external + +# Keycloak access configuration properties +sso.public-url=${KEYCLOAK_PUBLIC_URL:${keycloak.auth-server-url}} + +# Async mocking support. +async-api.enabled=false +async-api.default-binding=KAFKA +async-api.default-frequency=3 + +# Kafka configuration properties +spring.kafka.producer.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVER:localhost:9092} +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=io.github.microcks.event.ServiceViewChangeEventSerializer + +# Grpc server properties +#grpc.server.certChainFilePath=/Users/lbroudou/Development/temp/grpc/localhost.crt +#grpc.server.privateKeyFilePath=/Users/lbroudou/Development/temp/grpc/localhost.key + +# Test conformance computation config +test-conformance.trend-size=3 +test-conformance.trend-history-size=10 + +# AI Copilot configuration properties +ai-copilot.enabled=false +ai-copilot.implementation=openai +ai-copilot.openai.api-key=sk-my-openai-api-key +#ai-copilot.openai.api-url=http://localhost:1234/ +ai-copilot.openai.timeout=30 +ai-copilot.openai.maxTokens=3000 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/features.properties b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/features.properties new file mode 100644 index 000000000..775d624f6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/features.properties @@ -0,0 +1,28 @@ +# Optional and additional features configuration +# Syntax: features.features..= + +features.feature.microcks-hub.enabled=true +features.feature.microcks-hub.endpoint=https://hub.microcks.io/api +features.feature.microcks-hub.allowed-roles=admin,manager,manager-any + +features.feature.repository-filter.enabled=true +features.feature.repository-filter.label-key=domain +features.feature.repository-filter.label-label=Domain +features.feature.repository-filter.label-list=domain,status + +features.feature.repository-tenancy.enabled=false +features.feature.repository-tenancy.artifact-import-allowed-roles=admin,manager,manager-any + +features.feature.async-api.enabled=false +features.feature.async-api.frequencies=3,10,30 +features.feature.async-api.default-binding=KAFKA +features.feature.async-api.endpoint-KAFKA=my-cluster-kafka-bootstrap.apps.try.microcks.io +features.feature.async-api.endpoint-MQTT=my-mqtt-broker.apps.try.microcks.io +features.feature.async-api.endpoint-NATS=localhost:4222 +features.feature.async-api.endpoint-WS=localhost:8081 +features.feature.async-api.endpoint-AMQP=localhost:5672 +features.feature.async-api.endpoint-GOOGLEPUBSUB=my-project +features.feature.async-api.endpoint-SQS=eu-west-3 +features.feature.async-api.endpoint-SNS=eu-west-3 + +features.feature.ai-copilot.enabled=false \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/version.properties b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/version.properties new file mode 100644 index 000000000..79e7ae543 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/config/version.properties @@ -0,0 +1,2 @@ +versionId=${project.version} +buildTimestamp=${maven.build.timestamp} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/logback.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/logback.xml new file mode 100644 index 000000000..3598d139f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/logback.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + utf-8 + %clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%10.10t]){faint} %clr(%-40.40logger{36}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/mapDailyStatisticForADay.js b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/mapDailyStatisticForADay.js new file mode 100644 index 000000000..161e95209 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/mapDailyStatisticForADay.js @@ -0,0 +1,3 @@ +function(){ + emit(this.day, this); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/reduceDailyStatisticForADay.js b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/reduceDailyStatisticForADay.js new file mode 100644 index 000000000..895e20a66 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/reduceDailyStatisticForADay.js @@ -0,0 +1,10 @@ +function(key, values){ + var stat={'dailyCount':0, 'hourlyCount':{'0':0,'1':0,'2':0,'3':0,'4':0,'5':0,'6':0,'7':0,'8':0,'9':0,'10':0,'11':0,'12':0,'13':0,'14':0,'15':0,'16':0,'17':0,'18':0,'19':0,'20':0,'21':0,'22':0,'23':0}}; + for (var i=0;i + + + + + Microcks-UI - API Documentation + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/asyncapi.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/asyncapi.html new file mode 100644 index 000000000..5c1e47969 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/asyncapi.html @@ -0,0 +1,28 @@ + + + + Microcks-UI - API Documentation + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/openapi-3.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/openapi-3.0.yaml new file mode 100644 index 000000000..0c8326c6d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/openapi-3.0.yaml @@ -0,0 +1,75 @@ +--- +openapi: 3.0.0 +info: + title: {service} + description: This is a generic API definition for manipulation of {resource} resources. It contains basic CRUD operations for {resource} resources. + version: {version} +paths: + /{resource}: + get: + summary: Retrieve {resource} resources. + description: Retrieve a bunch of {resource} resources. Specify example resource as body payload. + operationId: get{resource}List + responses: + 200: + description: Get an array of {resource} resources. + post: + summary: Create new {resource} resource. + description: Create a new {resource} resource. Specify payload within request body. + operationId: create{resource} + requestBody: + description: The payload of resource {resource} to create. + content: + application/json: + schema: + $ref: '#/components/schema/{resource}Type' + responses: + 201: + description: Get the newly created {resource} resource. + /{resource}/{id}: + get: + summary: Retrieve a {resource} resource. + description: Retrieve an already existing {resource} resource having the specified id. + operationId: get{resource}ById + responses: + 200: + description: {resource} resource having specified id. + content: + application/json: + schema: + $ref: '#/components/schema/{resource}Type' + put: + summary: Update a {resource} resource. + description: Update an already existing {resource} resource having the specified id. + operationId: update{resource}ById + requestBody: + description: The payload of resource {resource} to update. + content: + application/json: + schema: + $ref: '#/components/schema/{resource}Type' + responses: + 200: + description: Updated {resource} resource. + content: + application/json: + schema: + $ref: '#/components/schema/{resource}Type' + delete: + summary: Delete a {resource} resource. + description: Remove an existing {resource} resource having the specified id. + operationId: delete{resource}ById + responses: + 204: + description: Resource {resource} with specified id has been removed. + parameters: + - name: id + in: path + description: Id of resource + required: true + schema: + type: string +components: + schemas: + {resource}Type: + {resourceSchema} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/redoc.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/redoc.html new file mode 100644 index 000000000..fa466ece6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/redoc.html @@ -0,0 +1,21 @@ + + + + Microcks-UI - API Documentation + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/swagger-2.0.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/swagger-2.0.json new file mode 100644 index 000000000..89ed4c0f7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/swagger-2.0.json @@ -0,0 +1,98 @@ +{ + "swagger": "2.0", + "info": { + "title": "{service}", + "description": "This is a generic API definition for manipulation of {resource} resources. It contains basic CRUD operations for {resource} resources.", + "version": "{version}" + }, + "paths": { + "/{resource}": { + "get": { + "summary": "Retrieve {resource} resources", + "description": "Retrieve a bunch of {resource} resources. Specify example resource as body payload.", + "responses": { + "200": { + "description": "Get an array of {resource} resources" + } + } + }, + "post": { + "summary": "Create new {resource} resource", + "description": "Create a new {resource} resource. Specify payload within request body.", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/{resource}Type" + } + } + ], + "responses": { + "201": { + "description": "Get the newly created {resource} resource." + } + } + } + }, + "/{resource}/:id": { + "get": { + "summary": "Retrieve a {resource} resource.", + "description": "Retrieve an already existing {resource} resource having the specified id.", + "responses": { + "200": { + "description": "{resource} resource having specified id.", + "schema": { + "$ref": "#/definitions/{resource}Type" + } + }, + "404": { + "description": "No {resource} resource have the specified id." + } + } + }, + "put": { + "summary": "Update a {resource} resource.", + "description": "Update an already existing {resource} resource having the specified id.", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "The payload of resource {resource} to update.", + "schema": { + "$ref": "#/definitions/{resource}Type" + } + } + ], + "responses": { + "200": { + "description": "Updated resource {resource} having the specified id.", + "schema": { + "$ref": "#/definitions/{resource}Type" + } + } + } + }, + "delete": { + "summary": "Delete a {resource} resource.", + "description": "Remove an existing {resource} resource having the specified id.", + "responses": { + "204": { + "description": "Resource {resource} with specified id has been removed." + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Resource {resource} unique identifier.", + "type": "string" + } + ] + } + }, + "definitions": { + "{resource}Type": {} + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/swagger-2.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/swagger-2.0.yaml new file mode 100644 index 000000000..ceb2e0f8d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/resources/templates/swagger-2.0.yaml @@ -0,0 +1,69 @@ +--- +swagger: '2.0' +info: + title: {service} + description: This is a generic API definition for manipulation of {resource} resources. + It contains basic CRUD operations for {resource} resources. + version: {version} +paths: + /{resource}: + get: + summary: Retrieve {resource} resources + description: Retrieve a bunch of {resource} resources. Specify example resource + as body payload. + responses: + '200': + description: Get an array of {resource} resources + post: + summary: Create new {resource} resource + description: Create a new {resource} resource. Specify payload within request + body. + parameters: + - name: body + in: body + schema: + $ref: "#/definitions/{resource}Type" + responses: + '201': + description: Get the newly created {resource} resource. + /{resource}/:id: + get: + summary: Retrieve a {resource} resource. + description: Retrieve an already existing {resource} resource having the specified + id. + responses: + '200': + description: "{resource} resource having specified id." + schema: + "$ref": "#/definitions/{resource}Type" + '404': + description: No {resource} resource have the specified id. + put: + summary: Update a {resource} resource. + description: Update an already existing {resource} resource having the specified + id. + parameters: + - name: body + in: body + description: The payload of resource {resource} to update. + schema: + "$ref": "#/definitions/{resource}Type" + responses: + '200': + description: Updated resource {resource} having the specified id. + schema: + "$ref": "#/definitions/{resource}Type" + delete: + summary: Delete a {resource} resource. + description: Remove an existing {resource} resource having the specified id. + responses: + '204': + description: Resource {resource} with specified id has been removed. + parameters: + - name: id + in: path + description: Resource {resource} unique identifier. + type: string +definitions: + {resource}Type: + {resourceSchema} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/angular.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/angular.json new file mode 100644 index 000000000..6e99681e2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/angular.json @@ -0,0 +1,112 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "microcks-ui": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + }, + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", + "./node_modules/patternfly/dist/css/patternfly.min.css", + "./node_modules/patternfly/dist/css/patternfly-additions.min.css", + "./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css", + "./node_modules/highlight.js/styles/github.min.css", + "src/lib/patternfly-ng.css", + "src/styles.css" + ], + "scripts": [ + "./node_modules/jquery/dist/jquery.min.js", + "./node_modules/bootstrap/dist/js/bootstrap.min.js", + "./node_modules/patternfly/dist/js/patternfly.min.js" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "3MB", + "maximumError": "4MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "5kB", + "maximumError": "10kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + }, + "configurations": { + "production": { + "buildTarget": "microcks-ui:build:production" + }, + "development": { + "buildTarget": "microcks-ui:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/package-lock.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/package-lock.json new file mode 100644 index 000000000..8a2f566e8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/package-lock.json @@ -0,0 +1,16369 @@ +{ + "name": "microcks-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "microcks-ui", + "version": "1.0.0", + "dependencies": { + "@angular/animations": "^19.2.0", + "@angular/cdk": "^19.2.0", + "@angular/common": "^19.2.0", + "@angular/compiler": "^19.2.0", + "@angular/core": "^19.2.0", + "@angular/forms": "^19.2.0", + "@angular/material": "^19.2.1", + "@angular/platform-browser": "^19.2.0", + "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/router": "^19.2.0", + "bootstrap": "^5.3.3", + "c3": "^0.7.20", + "d3": "^7.9.0", + "jquery": "^3.7.1", + "keycloak-js": "26.0.0", + "lodash-es": "^4.17.21", + "markdown-it": "^14.1.0", + "ng2-file-upload": "^8.0.0", + "ngx-bootstrap": "^19.0.2", + "ngx-highlightjs": "^14.0.0", + "patternfly": "^3.59.5", + "rxjs": "~7.8.0", + "sanitize-html": "^2.15.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.2.0", + "@angular/cli": "^19.2.0", + "@angular/compiler-cli": "^19.2.0", + "@types/d3": "^7.4.3", + "@types/jasmine": "~5.1.0", + "@types/jquery": "^3.5.32", + "@types/lodash-es": "^4.17.12", + "@types/markdown-it": "^14.1.2", + "@types/sanitize-html": "^2.13.0", + "jasmine-core": "~5.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.7.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1902.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.5.tgz", + "integrity": "sha512-GdcTqwCZT0CTagUoTmq799hpnbQeICx53+eHsfs+lyKjkojk1ahC6ZOi4nNLDl/J2DIMFPHIG1ZgHPuhjKItAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.5", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.5.tgz", + "integrity": "sha512-PmLAaPuruTzEACsVe7MVyDuShQhyFdj83gWqvPKXVd8p2SIEE8SeVXyNRKNYf84cZdxqJB+IgjyvTPK7R7a+rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.5", + "@angular-devkit/build-webpack": "0.1902.5", + "@angular-devkit/core": "19.2.5", + "@angular/build": "19.2.5", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "19.2.5", + "@vitejs/plugin-basic-ssl": "1.2.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "css-loader": "7.1.2", + "esbuild-wasm": "0.25.1", + "fast-glob": "3.3.3", + "http-proxy-middleware": "3.0.3", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.2", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.2", + "open": "10.1.0", + "ora": "5.4.1", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "postcss": "8.5.2", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.85.0", + "sass-loader": "16.0.5", + "semver": "7.7.1", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.39.0", + "tree-kill": "1.2.2", + "tslib": "2.8.1", + "webpack": "5.98.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.2.0", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.25.1" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.5", + "@web/test-runner": "^0.20.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1902.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.5.tgz", + "integrity": "sha512-rXvUKRAgjhHTmBVr4HbZs+gS6sQ5EM+sv+Ygzl7oz7xC2+JOKBYiq+9B8Udk4GnW3Es9m6Dq7G4XbBMPzVia3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1902.5", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.5.tgz", + "integrity": "sha512-s5d6ZQmut5QO7pcxssIoDgeVhVEjoQKxWpBeqsSdYxMYjROMR+QnlNcyiSDLI6Wc7QR9mZINOpx8yoj6Nim1Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.5.tgz", + "integrity": "sha512-gfWnbwDOuKyRZK0biVyiNIhV6kmI1VmHg1LLbJm3QK6jDL0JgXD0NudgL8ILl5Ksd1sJOwQAuzTLM5iPfB3hDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.5", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/animations": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.3.tgz", + "integrity": "sha512-HQexOmwEJFX3sHLspOCi7dVOdPW5Ad4jH6tJsf+zABkF0GjgIVf4jewe1uE5ZLKgoflr9f9vpcpy39IWl00kWw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.3" + } + }, + "node_modules/@angular/build": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.5.tgz", + "integrity": "sha512-WtgdBHxFVMtbLzEYf1dYJqtld282aXxEbefsRi3RZWnLya8qO33bKMxpcd0V2iLIuIc1v/sUXPIzbWLO10mvTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.5", + "@babel/core": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.1.6", + "@vitejs/plugin-basic-ssl": "1.2.0", + "beasties": "0.2.0", + "browserslist": "^4.23.0", + "esbuild": "0.25.1", + "fast-glob": "3.3.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.17", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "rollup": "4.34.8", + "sass": "1.85.0", + "semver": "7.7.1", + "source-map-support": "0.5.21", + "vite": "6.2.3", + "watchpack": "2.4.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.2.6" + }, + "peerDependencies": { + "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.5", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/cdk": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.7.tgz", + "integrity": "sha512-+Dx1WGEWMO3OYDKr2w/Z5xOCsdjkRuG7Z18ve8eeBOHayRaC0KbYoXkvPxUiJo233CJWEzKQ/qF13C54GGWnng==", + "license": "MIT", + "dependencies": { + "parse5": "^7.1.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.5.tgz", + "integrity": "sha512-jiaYtbRdrGGgMQ+Qw68so7m4ZoSblz1Q27ucaFMdKZhzi9yLsWoo9bCpzIk2B7K3dG/VebbjvjLf5WOdKI8UWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1902.5", + "@angular-devkit/core": "19.2.5", + "@angular-devkit/schematics": "19.2.5", + "@inquirer/prompts": "7.3.2", + "@listr2/prompt-adapter-inquirer": "2.0.18", + "@schematics/angular": "19.2.5", + "@yarnpkg/lockfile": "1.1.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.2.5", + "npm-package-arg": "12.0.2", + "npm-pick-manifest": "10.0.0", + "pacote": "20.0.0", + "resolve": "1.22.10", + "semver": "7.7.1", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.3.tgz", + "integrity": "sha512-cYOMRXFb6Sjtg9BI3bE/Ave+xU234jQmHYj7hBxr3MiqRSVJL4niy10KiA/Jiz6y76V5QfZfS+0aE65VuoqAvg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.3.tgz", + "integrity": "sha512-TL/JIU7vzSWD+IrMq2PAiHZi7IUFSRhdHo8q6/WuZ8SQmbuXCK2pJvHZpTtUdLswdPeD/UVhkhTAhQzEyEdZVg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.3.tgz", + "integrity": "sha512-ePh/7A6eEDAyfVn8QgLcAvrxhXBAf6mTqB/3+HwQeXLaka1gtN6xvZ6cjLEegP4s6kcYGhdfdLwzCcy0kjsY5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.26.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "19.2.3", + "typescript": ">=5.5 <5.9" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/core": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.3.tgz", + "integrity": "sha512-uNDbQBDWdAfL8JhgG2l9eTEbikovZ+SthLUKERyR4fL7AVGSx85LjNySRuq4WAL4eiD1cRN1UUgu8o+WKqF/ow==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + } + }, + "node_modules/@angular/forms": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.3.tgz", + "integrity": "sha512-JEgNKiZd3taYBg9lsMvoana5cv1QGke8xkuryc9zesHPJjhw9QHllmDPOW2HyUuwPqXZ/YkHiuCMOk+4qPjsAw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.3", + "@angular/core": "19.2.3", + "@angular/platform-browser": "19.2.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.2.7.tgz", + "integrity": "sha512-6aFj4Rk3oa0XykCMjQ28KGVCMo7umd8M37bT9/FALMue6JEi2fPIDrbUDWb2GYL5rdHkgsr+dbEqlwAyHW/5cw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "19.2.7", + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "@angular/forms": "^19.0.0 || ^20.0.0", + "@angular/platform-browser": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.3.tgz", + "integrity": "sha512-bz5mvUkCS8SxaMInjPgi/2dD7vpWkZePQesvr/bBRNQbYSE4cGTbovXcVl3X5hIxs5JoC6Het0lS2IxDq7j6qg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "19.2.3", + "@angular/common": "19.2.3", + "@angular/core": "19.2.3" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.3.tgz", + "integrity": "sha512-PHmmtdGxSfe9HL8xR4g3PspnEaPqTEOGyzNviAHugfkZCgXCdSbYs36d3i0nPwhExMAeuIVXbbJyouDn2kyeOw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.3", + "@angular/compiler": "19.2.3", + "@angular/core": "19.2.3", + "@angular/platform-browser": "19.2.3" + } + }, + "node_modules/@angular/router": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.3.tgz", + "integrity": "sha512-yYVMT7CceKqE+fBXxkhkAqEQUEdY/BHtLQr1vP9rEnAf30vwKghDEresugfegY6Ch4IGKTBtDG/QGmxWszgUAQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.3", + "@angular/core": "19.2.3", + "@angular/platform-browser": "19.2.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz", + "integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.26.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.27.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.0.tgz", + "integrity": "sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.0.tgz", + "integrity": "sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", + "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.0.tgz", + "integrity": "sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", + "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz", + "integrity": "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz", + "integrity": "sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", + "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", + "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.9.tgz", + "integrity": "sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.11.tgz", + "integrity": "sha512-OZSUW4hFMW2TYvX/Sv+NnOZgO8CHT2TU1roUCUIF2T+wfw60XFRRp9MRUPCT06cRnKL+aemt2YmTWwt7rOrNEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.8.tgz", + "integrity": "sha512-WXJI16oOZ3/LiENCAxe8joniNp8MQxF6Wi5V+EBbVA0ZIOpFcL4I9e7f7cXse0HJeIPCWO8Lcgnk98juItCi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.11.tgz", + "integrity": "sha512-pQK68CsKOgwvU2eA53AG/4npRTH2pvs/pZ2bFvzpBhrznh8Mcwt19c+nMO7LHRr3Vreu1KPhNBF3vQAKrjIulw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.11.tgz", + "integrity": "sha512-dH6zLdv+HEv1nBs96Case6eppkRggMe8LoOTl30+Gq5Wf27AO/vHFgStTVz4aoevLdNXqwE23++IXGw4eiOXTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.11.tgz", + "integrity": "sha512-uAYtTx0IF/PqUAvsRrF3xvnxJV516wmR6YVONOmCWJbbt87HcDHLfL9wmBQFbNJRv5kCjdYKrZcavDkH3sVJPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz", + "integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.0.tgz", + "integrity": "sha512-z0a2fmgTSRN+YBuiK1ROfJ2Nvrpij5lVN3gPDkQGhavdvIVGHGW29LwYZfM/j42Ai2hUghTI/uoBuTbrJk42bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", + "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", + "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", + "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.6.tgz", + "integrity": "sha512-yF/ih9EJJZc72psFQbwnn8mExIWfTnzWJg+N02hnpXtDPETYLmQswIMBn7+V88lfCaFrMozJsUvcEQIkEPU0Gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.6.tgz", + "integrity": "sha512-5BbCumsFLbCi586Bb1lTWQFkekdQUw8/t8cy++Uq251cl3hbDIGEwD9HAwh8H6IS2F6QA9KdKmO136LmipRNkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.6.tgz", + "integrity": "sha512-+6XgLpMb7HBoWxXj+bLbiiB4s0mRRcDPElnRS3LpWRzdYSe+gFk5MT/4RrVNqd2MESUDmb53NUXw1+BP69bjiQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.6.tgz", + "integrity": "sha512-l5VmJamJ3nyMmeD1ANBQCQqy7do1ESaJQfKPSm2IG9/ADZryptTyCj8N6QaYgIWewqNUrcbdMkJajRQAt5Qjfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.6.tgz", + "integrity": "sha512-nDYT8qN9si5+onHYYaI4DiauDMx24OAiuZAUsEqrDy+ja/3EbpXPX/VAkMV8AEaQhy3xc4dRC+KcYIvOFefJ4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.6.tgz", + "integrity": "sha512-XlqVtILonQnG+9fH2N3Aytria7P/1fwDgDhl29rde96uH2sLB8CHORIf2PfuLVzFQJ7Uqp8py9AYwr3ZUCFfWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngtools/webpack": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.5.tgz", + "integrity": "sha512-rp9hRFJiUzRrlUBbM3c4BSt/zB93GLM1X9eb+JQOwBsoQhRL92VU9kkffGDpK14hf6uB4goQ00AvQ4lEnxlUag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "typescript": ">=5.5 <5.9", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", + "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.1.tgz", + "integrity": "sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", + "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.1.1.tgz", + "integrity": "sha512-3Hc2KGIkrvJWJqTbvueXzBeZlmvoOxc2jyX00yzr3+sNFquJg0N8hH4SAPLPVrkWIRQICVpVgjrss971awXVnA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", + "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.5.tgz", + "integrity": "sha512-LXzeWpW7vhW7zk48atwdR860hOp2xEyU+TqDUz4dcLk5sPI14x94fAJuAWch42+9/X6LnkFLB+W2CmyOY9ZD1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.5", + "@angular-devkit/schematics": "19.2.5", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", + "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.0.tgz", + "integrity": "sha512-o09cLSIq9EKyRXwryWDOJagkml9XgQCoCSRjHOnHLnvsivaW7Qznzz6yjfV7PHJHhIvyp8OH7OX8w0Dc5bQK7A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", + "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.0.tgz", + "integrity": "sha512-suVMQEA+sKdOz5hwP9qNcEjX6B45R+hFFr4LAWzbRc5O+U2IInwvay/bpG5a4s+qR35P/JK/PiKiRGjfuLy1IA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.0.tgz", + "integrity": "sha512-kAAM06ca4CzhvjIZdONAL9+MLppW3K48wOFy1TbuaWFW/OMfl8JuTgW0Bm02JB1WJGT/ET2eqav0KTEKmxqkIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/c3": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@types/c3/-/c3-0.6.4.tgz", + "integrity": "sha512-W7i7oSmHsXYhseZJsIYexelv9HitGsWdQhx3mcy4NWso+GedpCYr02ghpkNvnZ4oTIjNeISdrOnM70s7HiuV+g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3": "^4" + } + }, + "node_modules/@types/c3/node_modules/@types/d3": { + "version": "4.13.15", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-4.13.15.tgz", + "integrity": "sha512-D1yRBsDCC8BBUHfl7DHfEXAX1+RkwdmrwTSMB+dhCPuzIyj4dc3b+fkKnvMWj7tqx3YeoM/QsZnZ13IkkbhTUw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-array": "^1", + "@types/d3-axis": "^1", + "@types/d3-brush": "^1", + "@types/d3-chord": "^1", + "@types/d3-collection": "*", + "@types/d3-color": "^1", + "@types/d3-dispatch": "^1", + "@types/d3-drag": "^1", + "@types/d3-dsv": "^1", + "@types/d3-ease": "^1", + "@types/d3-force": "^1", + "@types/d3-format": "^1", + "@types/d3-geo": "^1", + "@types/d3-hierarchy": "^1", + "@types/d3-interpolate": "^1", + "@types/d3-path": "^1", + "@types/d3-polygon": "^1", + "@types/d3-quadtree": "^1", + "@types/d3-queue": "*", + "@types/d3-random": "^1", + "@types/d3-request": "*", + "@types/d3-scale": "^1", + "@types/d3-selection": "^1", + "@types/d3-shape": "^1", + "@types/d3-time": "^1", + "@types/d3-time-format": "^2", + "@types/d3-timer": "^1", + "@types/d3-transition": "^1", + "@types/d3-voronoi": "*", + "@types/d3-zoom": "^1" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-array": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.12.tgz", + "integrity": "sha512-zIq9wCg/JO7MGC6vq3HRDaVYkqgSPIDjpo3JhAQxl7PHYVPA5D9SMiBfjW/ZoAvPd2a+rkovqBg0nS0QOChsJQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-axis": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.19.tgz", + "integrity": "sha512-rXxE2jJYv6kar/6YWS8rM0weY+jjvnJvBxHKrIUqt3Yzomrfbf5tncpKG6jq6Aaw6TZyBcb1bxEWc0zGzcmbiw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-selection": "^1" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-brush": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-1.1.8.tgz", + "integrity": "sha512-tPVjYAjJt02fgazF9yiX/309sj6qhIiIopLuHhP4FFFq9VKqu9NQBeCK3ger0RHVZGs9RKaSBUWyPUzii5biGQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-selection": "^1" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-chord": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-1.0.14.tgz", + "integrity": "sha512-W9rCIbSAhwtmydW5iGg9dwTQIi3SGBOh68/T3ke3PyOgejuSLozmtAMaWNViGaGJCeuM4aFJHTUHQvMedl4ugA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-color": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.5.tgz", + "integrity": "sha512-5sNP3DmtSnSozxcjqmzQKsDOuVJXZkceo1KJScDc1982kk/TS9mTPc6lpli1gTu1MIBF1YWutpHpjucNWcIj5g==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-dispatch": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-1.0.12.tgz", + "integrity": "sha512-vrhleoVNhGJGx7GQZ4207lYGyMbW/yj/iJTSvLKyfAp8nXFF+19dnMpPN/nEVs6fudIsQc7ZelBFUMe3aJDmKw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-drag": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.2.8.tgz", + "integrity": "sha512-QM6H8E6r9/51BcE4NEluQ0f9dTECCTDEALJSQIWn183+Mtz/6KvEjOxW8VzKYSnhhL+qMljMKKA1WOUUf/4Qhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-selection": "^1" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-dsv": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.2.8.tgz", + "integrity": "sha512-x1m1s0lVstZQ5/Kzp4bVIMee3fFuDm+hphVnvrYA7wU16XqwgbCBfeVvHYZzVQQIy4jyi3MEtgduLVuwIRCKLQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-ease": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-1.0.13.tgz", + "integrity": "sha512-VAA4H8YNaNN0+UNIlpkwkLOj7xL5EGdyiQpdlAvOIRHckjGFCLK8eMoUd4+IMNEhQgweq0Yk/Dfzr70xhUo6hA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-force": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.7.tgz", + "integrity": "sha512-zySqZfnxn67RVEGWzpD9dQA0AbNIp4Rj0qGvAuUdUNfGLrwuGCbEGAGze5hEdNaHJKQT2gTqr6j+qAzncm11ew==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-geo": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.12.7.tgz", + "integrity": "sha512-QetZrWWjuMfCe0BHLjD+dOThlgk7YGZ2gj+yhFAbDN5TularNBEQiBs5/CIgX0+IBDjt7/fbkDd5V70J1LjjKA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-hierarchy": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.11.tgz", + "integrity": "sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-interpolate": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-1.4.5.tgz", + "integrity": "sha512-k9L18hXXv7OvK4PqW1kSFYIzasGOvfhPUWmHFkoZ8/ci99EAmY4HoF6zMefrHl0SGV7XYc7Qq2MNh8dK3edg5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-color": "^1" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-polygon": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-1.0.10.tgz", + "integrity": "sha512-+hbHsFdCMs23vk9p/SpvIkHkCpl0vxkP2qWR2vEk0wRi0BXODWgB/6aHnfrz/BeQnk20XzZiQJIZ+11TGxuYMQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-quadtree": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-1.0.13.tgz", + "integrity": "sha512-BAQD6gTHnXqmI7JRhXwM2pEYJJF27AT1f6zCC192BKAUhigzd5HZjdje5ufRXmYcUM/fr2IJ9KqVMeXaljmmOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-random": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-1.1.5.tgz", + "integrity": "sha512-gB5CR+7xYMj56pt5zmSyDBjTNMEy96PdfUb2qBaAT9bmPcf4P/YHfhhTI5y8JoiqaSRLJY+3mqtaE9loBgB6Ng==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-scale": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-1.0.22.tgz", + "integrity": "sha512-9XHVg/pVr+4qbowUNKHYNouFCXQUQ0ZZr1ppGgh10DVUaEb6nKuyPj0May0mmTiLhuDEaa9di1t0Hmg6lYTSFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-time": "^1" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-selection": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.7.tgz", + "integrity": "sha512-aLaTOjdOJEFPhij59NdNwppvpHBheZFlLbcb7cIZZYLC0he9Wmdd/u4+1NZxlr7ncK+mq1PLmowMPw1GONrIQg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-path": "^1" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-time": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.4.tgz", + "integrity": "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-time-format": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.4.tgz", + "integrity": "sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-timer": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-1.0.12.tgz", + "integrity": "sha512-Tv9tkA4y3UvGQnrHyYAQhf5x/297FuYwotS4UW2TpwLblvRahbyL8r9HFYTJLPfPRqS63hwlqRItjKGmKtJxNg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/c3/node_modules/@types/d3-transition": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-1.3.6.tgz", + "integrity": "sha512-Y8NwxuHV4ElbCkN7tJcuwENYKiAL+ktU6tNDLHqZ141YsaT3kwa5ZA5eqiJwHYWQzXMjF+FgL6/Sxo9IGSwmNQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-selection": "^1" + } + }, + "node_modules/@types/c3/node_modules/@types/d3-zoom": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-1.8.7.tgz", + "integrity": "sha512-HJWci3jXwFIuFKDqGn5PmuwrhZvuFdrnUmtSKCLXFAWyf2lAIUKMKh1/lHOkWBl/f4KVupGricJiqkQy+cVTog==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-interpolate": "^1", + "@types/d3-selection": "^1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-collection": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@types/d3-collection/-/d3-collection-1.0.13.tgz", + "integrity": "sha512-v0Rgw3IZebRyamcwVmtTDCZ8OmQcj4siaYjNc7wGMZT7PmdSHawGsCOQMxyLvZ7lWjfohYLK0oXtilMOMgfY8A==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-queue": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-queue/-/d3-queue-3.0.10.tgz", + "integrity": "sha512-kYb7UeXKaOWJIkPx1Rx79+D/3wx69XXpkQ8+MWctAu4CUTdVnSOF/AKqC9bgf42sDuL1Fj0eeQSyU62HRqRHWg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-request": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-request/-/d3-request-1.0.9.tgz", + "integrity": "sha512-gD2991YKzdQu5lJGhWHEjptxQvWRZKwZF3QdWqjnqrWfVd15e7/WuL6X2Pl/4sRyLKaXWbB2xuk1tSBPVLlNhg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-dsv": "^1" + } + }, + "node_modules/@types/d3-request/node_modules/@types/d3-dsv": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.2.8.tgz", + "integrity": "sha512-x1m1s0lVstZQ5/Kzp4bVIMee3fFuDm+hphVnvrYA7wU16XqwgbCBfeVvHYZzVQQIy4jyi3MEtgduLVuwIRCKLQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-voronoi": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz", + "integrity": "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.7.tgz", + "integrity": "sha512-DVOfk9FaClQfNFpSfaML15jjB5cjffDMvjtph525sroR5BEAW2uKnTOYUTqTFuZFjNvH0T5XMIydvIctnUKufw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jquery": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", + "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sizzle": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", + "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.0.tgz", + "integrity": "sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", + "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.4", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.4" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/beasties": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.2.0.tgz", + "integrity": "sha512-Ljqskqx/tbZagIglYoJIMzH5zgssyp+in9+9sAyh15N22AornBeIDnb8EZ6Rk+6ShfMxd92uO3gfpT0NtZbpow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^9.1.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/beasties/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-datepicker": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/bootstrap-datepicker/-/bootstrap-datepicker-1.10.0.tgz", + "integrity": "sha512-lWxtSYddAQOpbAO8UhYhHLcK6425eWoSjb5JDvZU3ePHEPF6A3eUr51WKaFy4PccU19JRxUG6wEU3KdhtKfvpg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "jquery": ">=3.4.0 <4.0.0" + } + }, + "node_modules/bootstrap-sass": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.4.3.tgz", + "integrity": "sha512-vPgFnGMp1jWZZupOND65WS6mkR8rxhJxndT/AcMbqcq1hHMdkcH4sMPhznLzzoHOHkSCrd6J9F8pWBriPCKP2Q==", + "license": "MIT", + "optional": true + }, + "node_modules/bootstrap-select": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/bootstrap-select/-/bootstrap-select-1.12.2.tgz", + "integrity": "sha512-Fj1VstB55LigEEYQb6ZOi/ok+uaqnslRxS8Qo9Q+F46WWDhhXAeNpjBhjEMlxQjPs9yqYZf2hf/mxVRWab8sow==", + "license": "MIT", + "optional": true, + "dependencies": { + "jquery": ">=1.8" + } + }, + "node_modules/bootstrap-slider": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/bootstrap-slider/-/bootstrap-slider-9.10.0.tgz", + "integrity": "sha512-a9MtENtt4r3ttPW5mpIpOFmCaIsm37EGukOgw5cfHlxKvsUSN8AN9JtwKrKuqgEnxs86kUSsMvMn8kqewMorKw==", + "license": "MIT", + "optional": true + }, + "node_modules/bootstrap-switch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/bootstrap-switch/-/bootstrap-switch-3.3.4.tgz", + "integrity": "sha512-7YQo+Ir6gCUqC36FFp1Zvec5dRF/+obq+1drMtdIMi9Xc84Kx63Evi0kdcp4HfiGsZpiz6IskyYDNlSKcxsL7w==", + "license": "Apache-2.0", + "optional": true, + "peerDependencies": { + "bootstrap": "^3.1.1", + "jquery": ">=1.9.0" + } + }, + "node_modules/bootstrap-touchspin": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bootstrap-touchspin/-/bootstrap-touchspin-3.1.1.tgz", + "integrity": "sha512-o5pgzdr8Ma5hQKS3JE1uNq/jkx8qCG+KhJXSlvYCmX2wTxva2sS2Kq3idGN+tP5e1bZJQgkbqwP9TdEEx+R+6Q==", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c3": { + "version": "0.7.20", + "resolved": "https://registry.npmjs.org/c3/-/c3-0.7.20.tgz", + "integrity": "sha512-QZg4q5M32x2TEgoiQPgc+G+rAuDErTjtG2AeLxS8s0ZLxHBoqsaRcraVvLBG8Zbmj8hOefz2DCWSlX3gaV/11g==", + "license": "MIT", + "dependencies": { + "d3": "^5.8.0" + } + }, + "node_modules/c3/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/c3/node_modules/d3": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "node_modules/c3/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/c3/node_modules/d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1", + "d3-path": "1" + } + }, + "node_modules/c3/node_modules/d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^1.1.1" + } + }, + "node_modules/c3/node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "node_modules/c3/node_modules/d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json", + "csv2tsv": "bin/dsv2dsv", + "dsv2dsv": "bin/dsv2dsv", + "dsv2json": "bin/dsv2json", + "json2csv": "bin/json2dsv", + "json2dsv": "bin/json2dsv", + "json2tsv": "bin/json2dsv", + "tsv2csv": "bin/dsv2dsv", + "tsv2json": "bin/dsv2json" + } + }, + "node_modules/c3/node_modules/d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dsv": "1" + } + }, + "node_modules/c3/node_modules/d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/c3/node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/c3/node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1" + } + }, + "node_modules/c3/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "node_modules/c3/node_modules/d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "node_modules/c3/node_modules/d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/c3/node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/c3/node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "license": "BSD-3-Clause" + }, + "node_modules/c3/node_modules/d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "node_modules/c3/node_modules/d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/c3/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", + "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/datatables.net": { + "version": "1.13.11", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.11.tgz", + "integrity": "sha512-AE6RkMXziRaqzPcu/pl3SJXeRa6fmXQG/fVjuRESujvkzqDCYEeKTTpPMuVJSGYJpPi32WGSphVNNY1G4nSN/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jquery": "1.8 - 4" + } + }, + "node_modules/datatables.net-bs": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/datatables.net-bs/-/datatables.net-bs-2.2.2.tgz", + "integrity": "sha512-jGTV+TivO8ETIdo2eFHfO3QpgEnJfEekvjNPS2tQAztwCAVvRT+ksDgCTIqOoZ465aXHyK2G1Z2ClnoL08TKAg==", + "license": "MIT", + "optional": true, + "dependencies": { + "datatables.net": "2.2.2", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-bs/node_modules/datatables.net": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.2.2.tgz", + "integrity": "sha512-gfODIKE3gpgbVeZy2QGj2Dq9roO6hy00S+k1knklrqlMyAMrh1wt0Q6ryBUM7gU96U77ysbq8dYhxFdmcC/oPQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-colreorder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/datatables.net-colreorder/-/datatables.net-colreorder-1.7.2.tgz", + "integrity": "sha512-F8TYMFXtWLtsjciwS7hkP/Fbp3XS6WHuHLc+iMFtQqiQmbMo/59GK7YSxKuxSoqTTJU/opaPXQYjODnIuNEc/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "datatables.net": "^1.13.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-colreorder-bs": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/datatables.net-colreorder-bs/-/datatables.net-colreorder-bs-1.3.3.tgz", + "integrity": "sha512-+DPim/DhbSIqr2rhRvYNrAMFNZgl372PiKEAv5YeyjYMzc8+6kX8Vinpb3Bg0PDgEdPqEWqJ6H18pBCKhXppgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "datatables.net-bs": ">=1.10.9", + "datatables.net-colreorder": ">=1.2.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-select": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/datatables.net-select/-/datatables.net-select-1.2.7.tgz", + "integrity": "sha512-C3XDi7wpruGjDXV36dc9hN/FrAX9GOFvBZ7+KfKJTBNkGFbbhdzHS91SMeGiwRXPYivAyxfPTcVVndVaO83uBQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "datatables.net": "^1.10.15", + "jquery": ">=1.7" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/drmonty-datatables-colvis": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/drmonty-datatables-colvis/-/drmonty-datatables-colvis-1.1.2.tgz", + "integrity": "sha512-1kL4fbsBEkQQTl83eQ8G/vRGcCiM6Hn3O8Tp473tG4YSsBDcxETDDSxb8qC+fQjHZ3jUCptWj3lG/L8rI6NBNw==", + "optional": true, + "dependencies": { + "jquery": ">=1.7.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.124", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.124.tgz", + "integrity": "sha512-riELkpDUqBi00gqreV3RIGoowxGrfueEKBd6zPdOk/I8lvuFpBGNkYoHof3zUHbiTBsIU8oxdIIL/WNrAG1/7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eonasdan-bootstrap-datetimepicker": { + "version": "4.17.49", + "resolved": "https://registry.npmjs.org/eonasdan-bootstrap-datetimepicker/-/eonasdan-bootstrap-datetimepicker-4.17.49.tgz", + "integrity": "sha512-7KZeDpkj+A6AtPR3XjX8gAnRPUkPSfW0OmMANG1dkUOPMtLSzbyoCjDIdEcfRtQPU5X0D9Gob7wWKn0h4QWy7A==", + "license": "MIT", + "optional": true, + "dependencies": { + "bootstrap": "^3.3", + "jquery": "^3.5.1", + "moment": "^2.10", + "moment-timezone": "^0.4.0" + }, + "peerDependencies": { + "bootstrap": "^3.3", + "jquery": "^1.8.3 || ^2.0 || ^3.0", + "moment": "^2.10", + "moment-timezone": "^0.4.0 || ^0.5.0" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.1.tgz", + "integrity": "sha512-dZxPeDHcDIQ6ilml/NzYxnPbNkoVsHSFH3JGLSobttc5qYYgExMo8lh2XcB+w+AfiqykVDGK5PWanGB0gWaAWw==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", + "license": "(OFL-1.1 AND MIT)", + "engines": { + "node": ">=0.10.3" + } + }, + "node_modules/font-awesome-sass": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome-sass/-/font-awesome-sass-4.7.0.tgz", + "integrity": "sha512-apO2Nw3XP/Zv7fLxa+MnPnvJ/GdkH6qWrLrtN5oQrFL7RPprzHKROjN94jgyoxM+T7PQBhY9F/SwOKbBaLyXxg==", + "license": "MIT", + "optional": true + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-code-prettify": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/google-code-prettify/-/google-code-prettify-1.0.5.tgz", + "integrity": "sha512-Y47Bw63zJKCuqTuhTZC1ct4e/0ADuMssxXhnrP8QHq71tE2aYBKG6wQwXr8zya0zIUd0mKN3XTlI5AME4qm6NQ==", + "optional": true + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hosted-git-info": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz", + "integrity": "sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/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/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", + "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", + "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", + "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.5.0.tgz", + "integrity": "sha512-NHOvoPO6o9gVR6pwqEACTEpbgcH+JJ6QDypyymGbSUIFIFsMMbBJ/xsFNud8MSClfnWclXd7RQlAZBz7yVo5TQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, + "node_modules/jquery-match-height": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/jquery-match-height/-/jquery-match-height-0.7.2.tgz", + "integrity": "sha512-qSyC0GBc4zUlgBcxfyyumJSVUm50T6XuJEIz59cKaI28VXMUT95mZ6KiIjhMIMbG8IiJhh65FtQO1XD42TAcwg==", + "license": "MIT", + "optional": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma/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/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/keycloak-js": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.0.0.tgz", + "integrity": "sha512-uUvoc6luDuAOQ74i4/wKm1tGduYcJ/2jSh29Krekkt+ytn1mLL+mHug27FfpwgxcTb6yx+uL0OcehXwl3WIRMQ==", + "license": "Apache-2.0" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/less": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", + "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.6.tgz", + "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.2.6", + "@lmdb/lmdb-darwin-x64": "3.2.6", + "@lmdb/lmdb-linux-arm": "3.2.6", + "@lmdb/lmdb-linux-arm64": "3.2.6", + "@lmdb/lmdb-linux-x64": "3.2.6", + "@lmdb/lmdb-win32-x64": "3.2.6" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "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/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minizlib/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minizlib/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.4.1.tgz", + "integrity": "sha512-5cNPVUwaVJDCe9JM8m/qz17f9SkaI8rpnRUyDJi2K5HAd6EwhuQ3n5nLclZkNC/qJnomKgQH2TIu70Gy2dxFKA==", + "license": "MIT", + "optional": true, + "dependencies": { + "moment": ">= 2.6.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ng2-file-upload": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ng2-file-upload/-/ng2-file-upload-8.0.0.tgz", + "integrity": "sha512-uforjhx9zHxVJvstOwILWnn7qIpFD+V7x9xtsBIczVxI4KYwGtek2GyEohgZTemv979nZtcQBZrFGqH981OroA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0" + } + }, + "node_modules/ngx-bootstrap": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-19.0.2.tgz", + "integrity": "sha512-DR5sA5e9k1bwtVeo8lQW4Yr8IgU7ba2pco5rlznnr7k91BJ3SNgrUSq+BQfigwARHGmgKCcw9lexu8ZWvefEXg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^19.0.1", + "@angular/common": "^19.0.1", + "@angular/core": "^19.0.1", + "@angular/forms": "^19.0.1", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/ngx-highlightjs": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/ngx-highlightjs/-/ngx-highlightjs-14.0.0.tgz", + "integrity": "sha512-G0dezuYyHhp/oixm22wvj38PukFcgk5MooFTIx1shpZTFfq+KnqER0ZMNbMSNEQZh1OPtX9zpyZvYDoKZV2Avg==", + "dependencies": { + "highlight.js": "^11.11.1", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=19.0.0", + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.1.0.tgz", + "integrity": "sha512-/+7TuHKnBpnMvUQnsYEb0JOozDZqarQbfNuSGLXIjhStMT0fbw7IdSqWgopOP5xhRZE+lsbIvAHcekddruPZgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-install-checks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", + "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-packlist": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", + "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", + "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patternfly": { + "version": "3.59.5", + "resolved": "https://registry.npmjs.org/patternfly/-/patternfly-3.59.5.tgz", + "integrity": "sha512-SMQynv9eFrWWG0Ujta5+jPjxHdQB3xkTLiDW5VP8XXc0nGUxXb4EnZh21qiMeGGJYaKpu9CzaPEpCvuBxgYWHQ==", + "license": "MIT", + "dependencies": { + "bootstrap": "~3.4.1", + "font-awesome": "^4.7.0", + "jquery": "~3.4.1" + }, + "optionalDependencies": { + "@types/c3": "^0.6.0", + "bootstrap-datepicker": "^1.7.1", + "bootstrap-sass": "^3.4.0", + "bootstrap-select": "1.12.2", + "bootstrap-slider": "^9.9.0", + "bootstrap-switch": "3.3.4", + "bootstrap-touchspin": "~3.1.1", + "c3": "~0.4.11", + "d3": "~3.5.17", + "datatables.net": "^1.10.15", + "datatables.net-colreorder": "^1.4.1", + "datatables.net-colreorder-bs": "~1.3.2", + "datatables.net-select": "~1.2.0", + "drmonty-datatables-colvis": "~1.1.2", + "eonasdan-bootstrap-datetimepicker": "^4.17.47", + "font-awesome-sass": "^4.7.0", + "google-code-prettify": "~1.0.5", + "jquery-match-height": "^0.7.2", + "moment": "^2.19.1", + "moment-timezone": "^0.4.1", + "patternfly-bootstrap-combobox": "~1.1.7", + "patternfly-bootstrap-treeview": "~2.1.10" + } + }, + "node_modules/patternfly-bootstrap-combobox": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/patternfly-bootstrap-combobox/-/patternfly-bootstrap-combobox-1.1.7.tgz", + "integrity": "sha512-6KptS6UnS8jOwLuqsjokiNUYjOf3G4bSahiSHhkQMdfvG0b4sZkUgOFWdMJ8zBXaZGVe8T324GQoXqiJdJxMuw==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/patternfly-bootstrap-treeview": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/patternfly-bootstrap-treeview/-/patternfly-bootstrap-treeview-2.1.10.tgz", + "integrity": "sha512-P9+iFu34CwX+R5Fd7/EWbxTug0q9mDj53PnZIIh5ie54KX2kD0+54lCWtpD9SVylDwDtDv3n3A6gbFVkx7HsuA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bootstrap": "^3.4.1", + "jquery": "^3.4.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/patternfly/node_modules/c3": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/c3/-/c3-0.4.24.tgz", + "integrity": "sha512-mVCFtN5ZWUT5UE7ilFQ7KBQ7TUCdKIq6KsDt1hH/1m6gC1tBjvzFTO7fqhaiWHfhNOjjM7makschdhg6DkWQMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "d3": "~3.5.0" + } + }, + "node_modules/patternfly/node_modules/d3": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", + "integrity": "sha512-yFk/2idb8OHPKkbAL8QaOaqENNoMhIaSHZerk3oQsECwkObkCpJyjYwCe+OHiq6UEdhe1m8ZGARRRO3ljFjlKg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", + "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "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/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-html": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.15.0.tgz", + "integrity": "sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sass": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", + "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", + "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.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": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", + "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", + "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", + "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webpack": { + "version": "5.98.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", + "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.7", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/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/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", + "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==", + "license": "MIT" + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/package.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/package.json new file mode 100644 index 000000000..b53432e93 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/package.json @@ -0,0 +1,62 @@ +{ + "name": "microcks-ui", + "version": "1.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^19.2.0", + "@angular/cdk": "^19.2.0", + "@angular/common": "^19.2.0", + "@angular/compiler": "^19.2.0", + "@angular/core": "^19.2.0", + "@angular/forms": "^19.2.0", + "@angular/material": "^19.2.1", + "@angular/platform-browser": "^19.2.0", + "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/router": "^19.2.0", + "bootstrap": "^5.3.3", + "c3": "^0.7.20", + "d3": "^7.9.0", + "jquery": "^3.7.1", + "keycloak-js": "26.0.0", + "lodash-es": "^4.17.21", + "markdown-it": "^14.1.0", + "ng2-file-upload": "^8.0.0", + "ngx-bootstrap": "^19.0.2", + "ngx-highlightjs": "^14.0.0", + "patternfly": "^3.59.5", + "rxjs": "~7.8.0", + "sanitize-html": "^2.15.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.2.0", + "@angular/cli": "^19.2.0", + "@angular/compiler-cli": "^19.2.0", + "@types/d3": "^7.4.3", + "@types/jasmine": "~5.1.0", + "@types/jquery": "^3.5.32", + "@types/lodash-es": "^4.17.12", + "@types/markdown-it": "^14.1.2", + "@types/sanitize-html": "^2.13.0", + "jasmine-core": "~5.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.7.2" + }, + "overrides": { + "bootstrap": "^5.3.3", + "d3-color": "^3.1.0", + "jquery": "^3.7.1" + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/proxy.conf.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/proxy.conf.json new file mode 100644 index 000000000..85107439b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/proxy.conf.json @@ -0,0 +1,6 @@ +{ + "/api": { + "target": "http://localhost:8080", + "secure": false + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.html new file mode 100644 index 000000000..0b48e53be --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.html @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.spec.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.spec.ts new file mode 100644 index 000000000..089164d7d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.spec.ts @@ -0,0 +1,29 @@ +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'webapp-new' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('webapp-new'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, webapp-new'); + }); +}); diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.ts new file mode 100644 index 000000000..fa786cc6c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.component.ts @@ -0,0 +1,14 @@ +import { Component, Provider } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +import { VerticalNavComponent } from './components/vertical-nav/vertical-nav.component'; + +@Component({ + selector: 'app-root', + imports: [RouterOutlet, VerticalNavComponent], + templateUrl: './app.component.html', + styleUrl: './app.component.css' +}) +export class AppComponent { + title = 'webapp-new'; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.config.ts new file mode 100644 index 000000000..dcd6410c4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.config.ts @@ -0,0 +1,48 @@ +import { ApplicationConfig, inject, provideAppInitializer, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter, withDebugTracing, withHashLocation } from '@angular/router'; +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideAnimations } from '@angular/platform-browser/animations'; + +import { BsModalService } from 'ngx-bootstrap/modal'; +import { provideHighlightOptions } from 'ngx-highlightjs'; + +import { NotificationService } from './components/patternfly-ng/notification'; + +import { routes } from './app.routes'; +import { AuthenticationServiceProvider } from './services/auth.service.provider'; +import { ConfigService } from './services/config.service'; +import { AuthenticationHttpInterceptor } from './services/auth.http-interceptor'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes, withHashLocation()), + //[provideRouter(routes), withDebugTracing()], + provideHttpClient( + // DI-based interceptors must be explicitly enabled. + withInterceptorsFromDi(), + ), + provideAnimations(), + AuthenticationServiceProvider, + BsModalService, + NotificationService, + ConfigService, + provideAppInitializer(() => { + const configService = inject(ConfigService); + return configService.loadConfiguredFeatures() as Promise; + }), + + { provide: HTTP_INTERCEPTORS, useClass: AuthenticationHttpInterceptor, multi: true }, + + provideHighlightOptions({ + coreLibraryLoader: () => import('highlight.js/lib/core'), + languages: { + json: () => import('highlight.js/lib/languages/json'), + xml: () => import('highlight.js/lib/languages/xml'), + yaml: () => import('highlight.js/lib/languages/yaml'), + groovy: () => import('highlight.js/lib/languages/groovy'), + bash: () => import('highlight.js/lib/languages/bash'), + } + }) + ], +}; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.routes.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.routes.ts new file mode 100644 index 000000000..b1c365000 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/app.routes.ts @@ -0,0 +1,75 @@ +import { Routes } from '@angular/router'; + +import { DashboardPageComponent } from './pages/dashboard/dashboard.page'; +import { ServicesPageComponent } from './pages/services/services.page'; +import { ServiceDetailPageComponent } from './pages/services/{serviceId}/service-detail.page'; +import { OperationOverridePageComponent } from './pages/services/{serviceId}/operation/operation-override.page'; +import { TestsPageComponent } from './pages/tests/tests.page'; +import { TestCreatePageComponent } from './pages/tests/create/test-create.page'; +import { TestRunnerPageComponent } from './pages/tests/runner/test-runner.page'; +import { InvocationsServicePageComponent } from './pages/metrics/invocations/{serviceId}/invocations-service.page'; +import { TestDetailPageComponent } from './pages/tests/{testId}/test-detail.page'; +import { ImportersPageComponent } from './pages/importers/importers.page'; +import { HubPageComponent } from './pages/hub/hub.page'; +import { HubPackagePageComponent } from './pages/hub/package/package.page'; +import { HubAPIVersionPageComponent } from './pages/hub/package/apiVersion/apiVersion.page'; +import { AdminPageComponent } from './pages/admin/admin.page'; + +export const routes: Routes = [ + { + path: '', + component: DashboardPageComponent + }, + { + path: 'services', + component: ServicesPageComponent + }, + { + path: 'services/:serviceId', + component: ServiceDetailPageComponent + }, + { + path: 'services/:serviceId/operation/:name', + component: OperationOverridePageComponent + }, + { + path: 'tests/service/:serviceId', + component: TestsPageComponent + }, + { + path: 'tests/runner/:testId', + component: TestRunnerPageComponent + }, + { + path: 'tests/create', + component: TestCreatePageComponent + }, + { + path: 'tests/:testId', + component: TestDetailPageComponent + }, + { + path: 'metrics/invocations/:serviceName/:serviceVersion', + component: InvocationsServicePageComponent + }, + { + path: 'importers', + component: ImportersPageComponent + }, + { + path: 'hub', + component: HubPageComponent + }, + { + path: 'hub/package/:packageId', + component: HubPackagePageComponent + }, + { + path: 'hub/package/:packageId/api/:apiVersionId', + component: HubAPIVersionPageComponent + }, + { + path: 'admin', + component: AdminPageComponent + } +]; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/confirm-delete/confirm-delete.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/confirm-delete/confirm-delete.component.html new file mode 100644 index 000000000..3937af787 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/confirm-delete/confirm-delete.component.html @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/confirm-delete/confirm-delete.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/confirm-delete/confirm-delete.component.ts new file mode 100644 index 000000000..d6c1ff58b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/confirm-delete/confirm-delete.component.ts @@ -0,0 +1,82 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Output, EventEmitter, ViewChildren, QueryList, Provider } from '@angular/core'; +import { NgIf } from '@angular/common'; + +import { ModalDirective, ModalModule } from 'ngx-bootstrap/modal'; + +@Component({ + selector: 'app-confirm-delete-dialog', + templateUrl: 'confirm-delete.component.html', + imports: [NgIf, ModalModule] +}) +export class ConfirmDeleteDialogComponent { + + @Output() delete: EventEmitter = new EventEmitter(); + + @ViewChildren('confirmDeleteModal') confirmDeleteModal?: QueryList; + + private objectToDelete: any; + protected isOpenBF = false; + + set isOpen(isOpen: boolean) { + this.isOpenBF = isOpen; + } + + /** + * Returns true if the dialog is open. + */ + get isOpen() { + return this.isOpenBF; + } + + /** + * Called to open the dialog. + */ + public open(objectToDelete: any): void { + this.isOpen = true; + this.objectToDelete = objectToDelete; + this.confirmDeleteModal?.changes.subscribe(thing => { + if (this.confirmDeleteModal?.first) { + this.confirmDeleteModal.first.show(); + } + }); + } + + /** + * Called to close the dialog. + */ + public close(): void { + this.isOpen = false; + } + + /** + * Called when the user clicks "Yes". + */ + public handleDelete(): void { + this.delete.emit(this.objectToDelete); + this.cancel(); + } + + /** + * Called when the user clicks "cancel". + */ + public cancel(): void { + this.objectToDelete = false; + this.confirmDeleteModal?.first.hide(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/day-invocations-bar-chart/day-invocations-bar-chart.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/day-invocations-bar-chart/day-invocations-bar-chart.component.css new file mode 100644 index 000000000..6da4113f0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/day-invocations-bar-chart/day-invocations-bar-chart.component.css @@ -0,0 +1,19 @@ +.domain { + display: none; +} +.axis line { + stroke-width: 1px; + stroke: #eee; + shape-rendering: crispedges; +} +.axis text { + fill: #888; +} +rect { + color: #339cff; + fill: #339cff; + fill-opacity: 0.7; +} +rect:hover { + fill-opacity: 1; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/day-invocations-bar-chart/day-invocations-bar-chart.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/day-invocations-bar-chart/day-invocations-bar-chart.component.ts new file mode 100644 index 000000000..b50f2b3da --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/day-invocations-bar-chart/day-invocations-bar-chart.component.ts @@ -0,0 +1,129 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { DailyInvocations } from '../../models/metric.model'; + +import * as d3 from 'd3'; + +@Component({ + selector: 'app-day-invocations-bar-chart', + styleUrls: ['./day-invocations-bar-chart.component.css'], + template: `
`, +}) +export class DayInvocationsBarChartComponent implements OnInit { + @Input() + data!: Observable; + + @Output() + hourChange: EventEmitter = new EventEmitter(); + + constructor(private router: Router) {} + + ngOnInit() { + const width = parseInt(d3.select('#dayInvocationsBarChart').style('width')); + const height = 340; + const padt = 20; + const padr = 40; + const padb = 60; + const padl = 30; + + //const x = d3.scale.ordinal().rangeRoundBands([0, width - padl - padr], 0.1); + //const y = d3.scale.linear().range([height, 0]); + const x = d3.scaleBand().range([0, width - padl - padr]).padding(0.1); + const y = d3.scaleLinear().range([height, 0]); + + /* + const yAxis = d3.svg + .axis() + .scale(y) + .orient('left') + .tickSize(-width + padl + padr); + const xAxis = d3.svg.axis().scale(x).orient('bottom'); + */ + const yAxis = d3.axisLeft(y).tickSize(-width + padl + padr); + const xAxis = d3.axisBottom(x); + + this.data.subscribe((invocationsData) => { + const vis = d3 + .select('#dayInvocationsBarChart') + .append('svg') + .attr('width', width) + .attr('height', height + padt + padb) + .append('g') + .attr('transform', 'translate(' + padl + ',' + padt + ')'); + + // Clear the elements inside of the div. + vis.selectAll('*').remove(); + + //const max = d3.max(d3.map(invocationsData.hourlyCount).values()); + const max = d3.max(Object.values(invocationsData.hourlyCount)); + x.domain([ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', + '20', '21', '22', '23', + ]); + // y.domain([0, max]); + y.domain([0, max || 100]); + + // transform hourly object into an array of object(k, v) ascending sorted. + let hourlyData = Object.entries(invocationsData.hourlyCount) + .map(([key, value]) => ({ key, value, total: value })) + .sort((a, b) => d3.ascending(parseInt(a.key), parseInt(b.key))); + + vis.append('g').attr('class', 'y axis').call(yAxis); + + vis.append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + height + ')') + .call(xAxis) + .selectAll('.x.axis g') + .style('display', (d, i) => (i % 3 != 0 ? 'none' : 'block')); + + const bars = vis.selectAll('g.bar') + .data(hourlyData) + .enter() + .append('g') + .attr('class', 'invocations-bar') + .attr('transform', (d, i) => 'translate(' + x(i.toString()) + ', 0)'); + + const tooltip = d3.select('body') + .append('div') + .style('position', 'absolute') + .style('z-index', '10') + .style('visibility', 'hidden') + .style('padding', '0 6px') + .style('color', '#fff') + .style('background', '#292e34'); + + bars.append('rect') + .attr('width', function() { return x.bandwidth(); }) + .attr('height', function(this: SVGRectElement, d: { key: string; value: number; total?: number }) { return height - y(d.total || 0); }) + .attr('y', function(this: SVGRectElement, d: { key: string; value: number; total?: number }) { return y(d.total!); }) + .on('click', (e, d: { key: string; value: number; total?: number }) => this.hourChange.emit(parseInt(d.key)) ) + .on('mouseover', function(e, d: { key: string; value: any, total: any }) { + tooltip.text(d.total + ' hits'); + return tooltip.style('visibility', 'visible'); + }) + .on('mousemove', (event) => { + return tooltip.style('top', (event.pageY - 10) + 'px').style('left', (event.pageX + 10) + 'px'); + }) + .on('mouseout', function() { return tooltip.style('visibility', 'hidden'); }); + }); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels-dialog/edit-labels-dialog.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels-dialog/edit-labels-dialog.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels-dialog/edit-labels-dialog.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels-dialog/edit-labels-dialog.component.html new file mode 100644 index 000000000..89bd0babb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels-dialog/edit-labels-dialog.component.html @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels-dialog/edit-labels-dialog.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels-dialog/edit-labels-dialog.component.ts new file mode 100644 index 000000000..6e894f005 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels-dialog/edit-labels-dialog.component.ts @@ -0,0 +1,54 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { EditLabelsComponent } from '../edit-labels/edit-labels.component'; + +@Component({ + selector: 'app-edit-labels-dialog', + templateUrl: './edit-labels-dialog.component.html', + styleUrls: ['./edit-labels-dialog.component.css'], + imports: [ + EditLabelsComponent + ] +}) +export class EditLabelsDialogComponent implements OnInit { + @Output() saveLabelsAction = new EventEmitter>(); + + title?: string; + resourceType!: string; + resourceName!: string; + labels!: Map; + closeBtnName!: string; + + labelKV = ''; + + constructor(public bsModalRef: BsModalRef) {} + + ngOnInit() { + if (this.labels == null) { + this.labels = new Map(); + } + } + + saveLabels(): void { + // console.log("[EditLabelsDialogComponent saveLabels] with " + JSON.stringify(this.labels)); + this.saveLabelsAction.emit(this.labels); + this.bsModalRef.hide(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.css new file mode 100644 index 000000000..8b1fad639 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.css @@ -0,0 +1,24 @@ +.metadata-labels-input { + padding: 4px 9px; + min-height: 120px; + margin-bottom: 15px; +} + +.metadata-label { + background-color: #f5f5f5; + border: 1px solid #ededed; + border-radius: 12px; + display: inline-flex; + line-height: 20px; + margin: 0 3px 3px 0; + max-width: 100%; + padding: 0 8px; +} + +.labels-input { + background: transparent; + border: 1px solid transparent !important; + line-height: 20px; + outline: none; + padding: 0; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.html new file mode 100644 index 000000000..92057e5c3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.html @@ -0,0 +1,10 @@ +

+ Labels help you organize and select resources. Adding labels below will let you query for objects that have similar, overlapping or dissimilar labels. +

+
+ + +
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.ts new file mode 100644 index 000000000..f46ee5dd8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/edit-labels/edit-labels.component.ts @@ -0,0 +1,67 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, OnInit, Input, Output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-edit-labels', + templateUrl: './edit-labels.component.html', + styleUrls: ['./edit-labels.component.css'], + imports: [ + CommonModule, + FormsModule + ] +}) +export class EditLabelsComponent implements OnInit { + @Output() save = new EventEmitter>(); + + @Input() resourceType?: string; + @Input() resourceName!: string; + @Input() labels!: any; + + labelKV = ''; + + ngOnInit() { + if (this.labels == null) { + this.labels = new Map(); + } + } + + getKeys(map: any): string[] { + return Object.keys(map); + } + + removeLabel(labelK: string): void { + delete this.labels[labelK]; + this.labelKV = ''; + } + + addLabel(): void { + if (this.labelKV.indexOf('=') != -1) { + const kv: string[] = this.labelKV.split('='); + if (kv.length == 2) { + this.labels[kv[0]] = kv[1]; + } + } + this.labelKV = ''; + } + + saveLabels(): void { + // console.log("[EditLabelsComponent saveLabels] with " + JSON.stringify(this.labels)"); + this.save.emit(this.labels); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.css new file mode 100644 index 000000000..25b046d3a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.css @@ -0,0 +1,55 @@ +.grade-box-container { + display: flex; +} + +.grade-box.active:first-child { + border-left: none; +} +.grade-box:first-child { + margin-left: 0; +} +.grade-box { + background-color: #fff; + color: #6a6e73; + border-top: calc(1px * 2) solid #f5f5f5; +} +.grade-box:last-child { + border-right: calc(1px * 2) solid #f5f5f5; +} + +.grade-box.a { + --pfe-health-index--accent: #3f9c35; + --pfe-health-index--accent--text: var(--pfe-theme--color--text--on-dark, #fff); +} +.grade-box.b { + --pfe-health-index--accent: #92d400; + --pfe-health-index--accent--text: var(--pfe-theme--color--text--on-dark, #fff); +} +.grade-box.c { + --pfe-health-index--accent: #efaa00; + --pfe-health-index--accent--text: var(--pfe-theme--color--text--on-dark, #fff); +} +.grade-box.d { + --pfe-health-index--accent: #ec7a08; + --pfe-health-index--accent--text: var(--pfe-theme--color--text--on-dark, #fff); +} +.grade-box.e { + --pfe-health-index--accent: #cc0000; + --pfe-health-index--accent--text: var(--pfe-theme--color--text--on-dark, #fff); +} + +.grade-box .grade { + padding: 6px 10px; +} +.grade-box>.bottom { + height: 0.5em; + margin: 0 0.5px; +} +.grade-box.active .grade { + margin: calc(1px * -2) 0.5px 0 0.5px; + padding-top: 8px !important; +} +.grade-box.active .grade, .grade-box>.bottom { + background-color: var(--pfe-health-index--accent); + color: var(--pfe-health-index--accent--text); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.html new file mode 100644 index 000000000..6b12a3149 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.html @@ -0,0 +1,22 @@ +
+
+
A
+
+
+
+
B
+
+
+
+
C
+
+
+
+
D
+
+
+
+
E
+
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.ts new file mode 100644 index 000000000..f258c7c18 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/grade-index/grade-index.component.ts @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, OnInit, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TooltipModule } from 'ngx-bootstrap/tooltip'; + +@Component({ + selector: 'app-grade-index', + templateUrl: './grade-index.component.html', + styleUrls: ['./grade-index.component.css'], + imports: [ + CommonModule, + TooltipModule + ] +}) +export class GradeIndexComponent implements OnInit { + + @Input() score!: number; + + activeGrade!: string; + + ngOnInit() { + if (this.score >= 80) { + this.activeGrade = 'A'; + } else if (this.score >= 60 && this.score < 80) { + this.activeGrade = 'B'; + } else if (this.score >= 40 && this.score < 60) { + this.activeGrade = 'C'; + } else if (this.score >= 20 && this.score < 40) { + this.activeGrade = 'D'; + } else { + this.activeGrade = 'E'; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/help-dialog/help-dialog.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/help-dialog/help-dialog.component.html new file mode 100644 index 000000000..c8deb0490 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/help-dialog/help-dialog.component.html @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/help-dialog/help-dialog.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/help-dialog/help-dialog.component.ts new file mode 100644 index 000000000..56e65bcbe --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/help-dialog/help-dialog.component.ts @@ -0,0 +1,29 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component } from '@angular/core'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +@Component({ + selector: 'app-help-dialog', + templateUrl: 'help-dialog.component.html' +}) +export class HelpDialogComponent { + + protected isOpenBF = false; + + constructor(public bsModalRef: BsModalRef) {} +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/hour-invocations-bar-chart/hour-invocations-bar-chart.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/hour-invocations-bar-chart/hour-invocations-bar-chart.component.css new file mode 100644 index 000000000..1c437b860 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/hour-invocations-bar-chart/hour-invocations-bar-chart.component.css @@ -0,0 +1,18 @@ +#hourInvocationsBarChart >* .domain { + display: none; + } + #hourInvocationsBarChart >* .axis line { + stroke-width: 1px; + stroke: #eee; + shape-rendering: crispedges; + } + #hourInvocationsBarChart >* .axis text { + fill: #888; + } + .invocations-bar > rect { + fill: #0088ce; + fill-opacity: 0.7; + } + .invocations-bar > rect:hover { + fill-opacity: 1; + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/hour-invocations-bar-chart/hour-invocations-bar-chart.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/hour-invocations-bar-chart/hour-invocations-bar-chart.component.ts new file mode 100644 index 000000000..eee0985db --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/hour-invocations-bar-chart/hour-invocations-bar-chart.component.ts @@ -0,0 +1,164 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Component, + OnInit, + Input, + SimpleChanges, + OnChanges, +} from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { DailyInvocations } from '../../models/metric.model'; + +import * as d3 from 'd3'; + +const height = 340; +const padt = 20; +const padr = 20; +const padb = 60; +const padl = 40; + +@Component({ + selector: 'app-hour-invocations-bar-chart', + styleUrls: ['./hour-invocations-bar-chart.component.css'], + template: `
`, +}) +export class HourInvocationsBarChartComponent implements OnInit, OnChanges { + @Input() + data!: Observable; + + @Input() + hour!: number; + + resolvedData?: DailyInvocations; + + width: number = 100; + vis: any; + + constructor(private router: Router) {} + + ngOnInit() { + this.width = parseInt(d3.select('#hourInvocationsBarChart').style('width')); + + this.data.subscribe((invocationsData) => { + this.resolvedData = invocationsData; + this.vis = d3 + .select('#hourInvocationsBarChart') + .append('svg') + .attr('width', this.width) + .attr('height', height + padt + padb) + .append('g') + .attr('transform', 'translate(' + padl + ',' + padt + ')'); + }); + } + + ngOnChanges(changes: SimpleChanges) { + // TODO verify if needed? + if (changes['hour']) { + this.updateChart(changes['hour'].currentValue); + } + } + + updateChart(newHour: number) { + if (this.resolvedData) { + // Clear the elements inside of the div. + this.vis.selectAll('*').remove(); + + //const x = d3.scale.ordinal().rangeRoundBands([0, this.width - padl - padr], 0.1); + //const y = d3.scale.linear().range([height, 0]); + const x = d3.scaleBand().range([0, this.width - padl - padr]).padding(0.1); + const y = d3.scaleLinear().range([height, 0]); + + /* + const yAxis = d3.svg + .axis() + .scale(y) + .orient('left') + .tickSize(-this.width + padl + padr); + const xAxis = d3.svg.axis().scale(x).orient('bottom'); + */ + const yAxis = d3.axisLeft(y).tickSize(-this.width + padl + padr); + const xAxis = d3.axisBottom(x); + + // compute index for extracting stats + const startIndex = newHour * 60; + const endIndex = ((++newHour) * 60) - 1; + + // transform minute object into an array of object(k, v) ascending sorted. + /* + let minuteData = d3 + .entries(this.resolvedData.minuteCount) + .sort((a, b) => d3.ascending(parseInt(a.key), parseInt(b.key))); + minuteData = $.map(minuteData, (d, i) => ({ total: d.value })).slice( + startIndex, + endIndex + ); + */ + let minuteData = Object.entries(this.resolvedData.minuteCount) + .map(([key, value]) => ({ key, value, total: value })) + .sort((a, b) => d3.ascending(parseInt(a.key), parseInt(b.key))) + .slice(startIndex, endIndex); + + const max = d3.max(minuteData, (d: { key: string; value: any, total: any; }) => d.total); + x.domain(d3.range(60).map((v) => v.toString())); + y.domain([0, max || 100]); + + this.vis.append('g').attr('class', 'y axis').call(yAxis); + + this.vis + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + height + ')') + .call(xAxis) + .selectAll('.x.axis g') + .style('display', (d: any, i: number) => (i % 3 != 0 ? 'none' : 'block')); + + const bars = this.vis.selectAll('g.bar') + .data(minuteData) + .enter() + .append('g') + .attr('class', 'invocations-bar') + .attr('transform', (d: any, i: number) => 'translate(' + x(i.toString()) + ', 0)'); + + const tooltip = d3.select('body') + .append('div') + .style('position', 'absolute') + .style('z-index', '10') + .style('visibility', 'hidden') + .style('padding', '0 6px') + .style('color', '#fff') + .style('background', '#292e34'); + + bars + .append('rect') + .attr('width', () => x.bandwidth()) + .attr('height', (d: { key: string; value: number; total?: number }) => height - y(d.total || 0)) + .attr('y', (d: { key: string; value: number; total?: number }) => y(d.total!)) + .on('mouseover', (_: any, d: { key: string; value: any, total: any }) => { + tooltip.text(d.total + ' hits'); + return tooltip.style('visibility', 'visible'); + }) + .on('mousemove', (event: { pageY: number; pageX: number; }) => { + tooltip + .style('top', event.pageY - 10 + 'px') + .style('left', event.pageX + 10 + 'px') + }) + .on('mouseout', () => tooltip.style('visibility', 'hidden')); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.css new file mode 100644 index 000000000..d68d741e3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.css @@ -0,0 +1,10 @@ +.metadata-label { + background-color: #f5f5f5; + border: 1px solid #ededed; + border-radius: 12px; + display: inline-flex; + line-height: 20px; + margin: 0 3px 3px 0; + max-width: 100%; + padding: 0 8px; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.html new file mode 100644 index 000000000..323058aa4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.html @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.ts new file mode 100644 index 000000000..241e21ea5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/label-list/label-list.component.ts @@ -0,0 +1,84 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit, ViewEncapsulation, ChangeDetectionStrategy, Input} from '@angular/core'; +import { NgFor } from '@angular/common'; + +import { Metadata } from '../../../app/models/commons.model'; + + +@Component({ + selector: 'app-label-list', + encapsulation: ViewEncapsulation.None, + templateUrl: './label-list.component.html', + styleUrls: ['./label-list.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgFor] +}) +export class LabelListComponent implements OnInit { + + @Input() metadata: Metadata | null = null; + + @Input() filter: string | null = null; + + private labels: any = null; + + check() { + // Debug trace for diving into ChangeDetectionStrategy.OnPush issues... + console.log('[LabelListComponent] View checked'); + } + + ngOnInit() { + if (this.metadata) { + if (this.filter) { + this.labels = {}; + const filteredLabels = this.filter.split(','); + filteredLabels.forEach(label => { + if (this.metadata?.labels && this.metadata.labels[label]) { + this.labels[label] = this.metadata.labels[label]; + } + }); + } else { + this.labels = this.metadata.labels; + } + } + } + + getLabelsKeys(): string[] | null { + if (this.metadata) { + if (this.filter) { + this.labels = {}; + const filteredLabels = this.filter.split(','); + filteredLabels.forEach(label => { + if (this.metadata?.labels && this.metadata.labels[label]) { + this.labels[label] = this.metadata.labels[label]; + } + }); + } else { + this.labels = this.metadata.labels; + } + } + if (this.labels == null) { + return null; + } + return Object.keys(this.labels); + } + getLabelValue(label: string): string | null { + if (this.labels == null) { + return null; + } + return this.labels[label]; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/markdown.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/markdown.ts new file mode 100644 index 000000000..758add99a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/markdown.ts @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import sanitizeHtml from 'sanitize-html'; +import markdownit from 'markdown-it' + +export const markdownConverter = { + makeHtml: (markdown: any) => { + const md = markdownit({ + html: true, + linkify: true, + typographer: true, + breaks: true, + }) + const unsafeHtml = md.render(markdown); + + return sanitizeHtml(unsafeHtml, { + allowedTags: [ + 'b', + 'i', + 'strike', + 's', + 'del', + 'em', + 'strong', + 'a', + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'ul', + 'ol', + 'li', + 'code', + 'pre', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td' + ], + allowedAttributes: { + a: ['href', 'target', 'rel'] + }, + allowedSchemes: ['http', 'https', 'mailto'], + transformTags: { + a: sanitizeHtml.simpleTransform('a', { rel: 'noopener noreferrer' }, true) + } + }); + } +}; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action-config.ts new file mode 100644 index 000000000..e167340ef --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action-config.ts @@ -0,0 +1,37 @@ +import { Action } from './action'; + +/** + * An action config containing properties for primary and secondary actions such as + * multiple buttons and kebab menu options + */ +export class ActionConfig { + /** + * A list of secondary actions (e.g., menu options for a kebab) + */ + moreActions?: Action[]; + + /** + * Text announced to screen readers for the action config button + */ + moreActionsAriaLabel?: string; + + /** + * Set to true to disable secondary actions + */ + moreActionsDisabled!: boolean; + + /** + * Style class for the secondary actions container + */ + moreActionsStyleClass!: string; + + /** + * Set to false to hide secondary actions + */ + moreActionsVisible!: boolean; + + /** + * List of primary actions (e.g., for buttons) + */ + primaryActions!: Action[]; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.component.html new file mode 100644 index 000000000..33d9f87d4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.component.html @@ -0,0 +1,48 @@ + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.component.ts new file mode 100644 index 000000000..7d3b68ceb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.component.ts @@ -0,0 +1,180 @@ +import { + Component, + DoCheck, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + TemplateRef, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { clone, cloneDeep, defaults, isEqual } from 'lodash-es'; + +import { Action } from './action'; +import { ActionConfig } from './action-config'; + +/** + * List actions component. + * + * By default, buttons and kebab have no padding so they may inherit stying from components such as list and toolbar. + * + * Usage: + *
+ * // Individual module import
+ * import { ActionModule } from 'patternfly-ng/action';
+ * // Or
+ * import { ActionModule } from 'patternfly-ng';
+ *
+ * // NGX Bootstrap
+ * import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown';
+ *
+ * @NgModule({
+ *   imports: [ActionModule, BsDropdownModule.forRoot(),...],
+ *   providers: [BsDropdownConfig]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { Action, ActionConfig } from 'patternfly-ng/action';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-action', + templateUrl: './action.component.html', + imports: [ + CommonModule + ] +}) +export class ActionComponent implements DoCheck, OnInit { + /** + * The action config containing component properties + */ + @Input() config?: ActionConfig; + + /** + * Action template for custom actions + */ + @Input() template!: TemplateRef; + + /** + * The event emitted when an action has been selected + */ + @Output('onActionSelect') onActionSelect = new EventEmitter(); + + private defaultConfig = { + moreActionsDisabled: false, + moreActionsVisible: true + } as ActionConfig; + isMoreActionsDropup: boolean = false; + private prevConfig!: ActionConfig; + + /** + * The default constructor + * + * @param el The element reference for this component + */ + constructor(private el: ElementRef) { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + // lodash has issues deep cloning templates -- best seen with list component + this.prevConfig = clone(this.config); + } + + // Private + + handleAction(action: Action): void { + if (action && action.disabled !== true) { + this.onActionSelect.emit(action); + } + } + + /** + * Set flag indicating if kebab should be shown as a dropdown or dropup + * + * @param $event The MouseEvent triggering this function + */ + initMoreActionsDropup($event: MouseEvent): void { + window.requestAnimationFrame(() => { + let kebabContainer = this.closest($event.target, '.dropdown-kebab-pf.open', 'pfng-list-actions'); + let listContainer = this.closest(this.el.nativeElement, '.list-pf', 'pfng-list'); + if (kebabContainer === null || listContainer === null) { + return; + } + + let dropdownButton = kebabContainer.querySelector('.dropdown-toggle'); + let dropdownMenu = kebabContainer.querySelector('.dropdown-menu'); + if (dropdownButton === null || dropdownMenu === null) { + return; + } + let buttonRect = dropdownButton.getBoundingClientRect(); + let menuRect = dropdownMenu.getBoundingClientRect(); + let menuTop = buttonRect.top - menuRect.height; + let menuBottom = buttonRect.top + buttonRect.height + menuRect.height; + let parentRect = listContainer.getBoundingClientRect(); + + if ((menuBottom <= parentRect.top + parentRect.height) || (menuTop < parentRect.top)) { + this.isMoreActionsDropup = false; + } else { + this.isMoreActionsDropup = true; + } + }); + } + + // Utils + + /** + * Get the closest ancestor based on given selector + * + * @param el The HTML element to start searching for matching ancestor + * @param selector The selector to match + * @param stopSelector If this selector is matched, the search is stopped + * @returns {HTMLElement} The matching HTML element or null if not found + */ + private closest(el: any, selector: string, stopSelector: string): HTMLElement { + let retval = null; + while (el) { + if (el.matches(selector)) { + retval = el; + break; + } else if (stopSelector && el.matches(stopSelector)) { + break; + } + el = el.parentElement; + } + return retval; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.module.ts new file mode 100644 index 000000000..77996bf98 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { ActionComponent } from './action.component'; + +/** + * A module containing objects associated with action components + */ +@NgModule({ + imports: [ + ActionComponent, + BsDropdownModule.forRoot(), + CommonModule, + FormsModule, + ], + //declarations: [ActionComponent], + exports: [ActionComponent], + providers: [BsDropdownConfig] +}) +export class ActionModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.ts new file mode 100644 index 000000000..01bc37026 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/action.ts @@ -0,0 +1,46 @@ +import { TemplateRef } from '@angular/core'; + +/** + * An action containing common properties for buttons, kebabs, etc. + */ +export class Action { + /** + * Set to true to disable the action + */ + disabled?: boolean; + + /** + * Unique Id for the filter field, useful for comparisons + */ + id?: string; + + /** + * Set to true if this is a placehodler for a separator rather than an action + */ + separator?: boolean; + + /** + * Style class for button or kebab menu option + */ + styleClass?: string; + + /** + * Template name for including custom content (primary actions only) + */ + template?: TemplateRef; + + /** + * The title for the action, displayed as the button label or kebab menu option + */ + title?: string; + + /** + * A tooltip for the action + */ + tooltip?: string; + + /** + * Set to false if the button or kebab menu option should be hidden + */ + visible?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/index.ts new file mode 100644 index 000000000..6bffcc3a9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/action/index.ts @@ -0,0 +1,4 @@ +export { Action } from './action'; +export { ActionConfig } from './action-config'; +export { ActionComponent } from './action.component'; +export { ActionModule } from './action.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card-config.ts new file mode 100644 index 000000000..1a152de48 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card-config.ts @@ -0,0 +1,33 @@ +import { CardAction } from '../card-action/card-action'; +import { CardConfigBase } from '../card-config-base'; +import { CardFilter } from '../card-filter/card-filter'; + +/** + * A config containing properties for card + */ +export class CardConfig extends CardConfigBase { + /** + * An action shown in the footer + */ + action?: CardAction; + + /** + * The time frame filter position; "header" or "footer" + */ + filterPosition?: string; + + /** + * A list of time frame filters + */ + filters?: CardFilter[]; + + /** + * Omit padding for customization + */ + noPadding?: boolean; + + /** + * Sub-Title for the card + */ + subTitle?: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.html new file mode 100644 index 000000000..7d7c00e37 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.html @@ -0,0 +1,34 @@ +
+
+
+ + + + +
+ +
+

{{config.title}}

+
+
+ {{config.subTitle}} +
+ +
+ +
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.less new file mode 100644 index 000000000..e9ebd1a2f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.less @@ -0,0 +1,25 @@ +.pfng-card { + .card-pf-footer { + min-height: 60px; + } + &.pfng-card-no-padding { + &.card-pf { + padding-left: 0; + padding-right: 0; + } + .card-pf-body { + margin-top: 0; + padding-bottom: 0; + } + .card-pf-heading { + margin-bottom: 0; + margin-left: 0; + margin-right: 0; + } + } +} + +.pfng-card-heading-no-bottom { + margin: 0 -20px 0px; + padding: 0 20px; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.ts new file mode 100644 index 000000000..e23745508 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.component.ts @@ -0,0 +1,176 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { cloneDeep, defaults, isEqual } from 'lodash-es'; + +import { CardAction } from '../card-action/card-action'; +import { CardBase } from '../card-base'; +import { CardConfig } from './card-config'; +import { CardFilter } from '../card-filter/card-filter'; +import { CardFilterPosition } from '../card-filter/card-filter-position'; +import { CardFilterModule } from '../card-filter'; +import { CardActionModule } from '../card-action'; + +/** + * Card component + * + * For customization, use the templates named headerTemplate and footerTemplate. + * + * Usage: + *
+ * // Individual module import
+ * import { CardModule } from 'patternfly-ng/card';
+ * // Or
+ * import { CardModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [CardModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { CardAction, CardConfig, CardFilter, CardFilterPosition } from 'patternfly-ng/card';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-card', + templateUrl: './card.component.html', + imports: [ + CommonModule, + CardActionModule, + CardFilterModule + ] +}) +export class CardComponent extends CardBase implements DoCheck, OnInit { + /** + * The card config containing component properties + */ + @Input() config!: CardConfig; + + /** + * The event emitted when an action is selected + */ + @Output('onActionSelect') onActionSelect = new EventEmitter(); + + /** + * The event emitted when a filter is selected + */ + @Output('onFilterSelect') onFilterSelect = new EventEmitter(); + + private defaultConfig = { + filterPosition: CardFilterPosition.FOOTER, + noPadding: false, + titleBorder: true, + topBorder: true + } as CardConfig; + private prevConfig!: CardConfig; + + /** + * The default constructor + */ + constructor() { + super(); + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + this.prevConfig = cloneDeep(this.config); + } + + // Actions + + /** + * Handle the event emitted when an action is selected + * + * @param {CardAction} $event The emitted CardAction object + */ + protected handleActionSelect($event: CardAction): void { + this.onActionSelect.emit($event); + } + + /** + * Handle the event emitted when a filter is selected + * + * @param {CardFilter} $event The emitted CardFilter object + */ + protected handleFilterSelect($event: CardFilter): void { + this.onFilterSelect.emit($event); + } + + // Accessors + + /** + * Indicates that the footer should be shown in the footer + * + * @returns {boolean} True if the footer should be shown in the footer + */ + protected get showFilterInFooter(): boolean { + return (this.config.filters !== undefined && this.config.filterPosition !== undefined + && this.config.filterPosition === CardFilterPosition.FOOTER); + } + + /** + * Indicates that the footer should be shown in the header + * + * @returns {boolean} True if the footer should be shown in the header + */ + protected get showFilterInHeader(): boolean { + return (this.config.filters !== undefined && this.config.filterPosition !== undefined + && this.config.filterPosition === CardFilterPosition.HEADER); + } + + /** + * Indicates that the footer should be shown + * + * @returns {boolean} True if the footer should be shown + */ + get showFooter(): boolean { + return (this.footerTemplate !== undefined || this.showFilterInFooter || this.config.action !== undefined); + } + + /** + * Indicates that the header should be shown + * + * @returns {boolean} True if the header should be shown + */ + get showHeader(): boolean { + return (this.headerTemplate !== undefined || this.showFilterInHeader || this.config.title !== undefined); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.module.ts new file mode 100644 index 000000000..d1aeb0341 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/card.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { CardActionModule } from '../card-action/card-action.module'; +import { CardComponent } from '../basic-card/card.component'; +import { CardFilterModule } from '../card-filter/card-filter.module'; + +/** + * A module containing objects associated with basic card components + */ +@NgModule({ + imports: [ + CardActionModule, + CardComponent, + CardFilterModule, + CommonModule, + FormsModule + ], + //declarations: [CardComponent], + exports: [CardComponent] +}) +export class CardModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/index.ts new file mode 100644 index 000000000..fb214e4dc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/basic-card/index.ts @@ -0,0 +1,3 @@ +export { CardComponent } from './card.component'; +export { CardConfig } from './card-config'; +export { CardModule } from './card.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.component.html new file mode 100644 index 000000000..30e59219b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.component.html @@ -0,0 +1,19 @@ +
+ + + {{action?.hypertext}} + + + + + {{action?.hypertext}} + + + + {{action?.hypertext}} + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.component.ts new file mode 100644 index 000000000..7a313e4c1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.component.ts @@ -0,0 +1,72 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { CardAction } from './card-action'; + +/** + * Card action component + * + * Usage: + *
+ * // Individual module import
+ * import { CardActionModule } from 'patternfly-ng/card';
+ * // Or
+ * import { CardActionModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [CardActionModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { CardAction } from 'patternfly-ng/card';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-card-action', + templateUrl: './card-action.component.html', + imports: [ + CommonModule + ] +}) +export class CardActionComponent implements OnInit { + /** + * The card filters + */ + @Input() action?: CardAction; + + /** + * The event emitted when a filter is selected + */ + @Output('onActionSelect') onActionSelect = new EventEmitter(); + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + } + + // Actions + + protected select($event: MouseEvent): void { + this.onActionSelect.emit(this.action); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.module.ts new file mode 100644 index 000000000..d766938f4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { CardActionComponent } from './card-action.component'; + +/** + * A module containing objects associated with card action components + */ +@NgModule({ + imports: [ + CommonModule, + FormsModule, + CardActionComponent + ], + //declarations: [CardActionComponent], + exports: [CardActionComponent] +}) +export class CardActionModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.ts new file mode 100644 index 000000000..adbc547cd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/card-action.ts @@ -0,0 +1,29 @@ +/** + * An object containing card action properties + */ +export class CardAction { + /** + * Set to true to disable the action + */ + disabled?: boolean; + + /** + * The action link text + */ + hypertext!: string; + + /** + * The action id + */ + id?: string; + + /** + * Style class for main icon (e.g., 'fa fa-flag') + */ + iconStyleClass?: string; + + /** + * The action link URL + */ + url?: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/index.ts new file mode 100644 index 000000000..ef18bc9b6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-action/index.ts @@ -0,0 +1,3 @@ +export { CardAction } from './card-action'; +export { CardActionComponent } from './card-action.component'; +export { CardActionModule } from './card-action.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-base.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-base.ts new file mode 100644 index 000000000..9e9fa695a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-base.ts @@ -0,0 +1,32 @@ +import { + Component, + Input, + TemplateRef, +} from '@angular/core'; + +/** + * Card base component + * + * For customization, use the templates named headerTemplate and footerTemplate. + */ +@Component({ + selector: 'pfng-card-base', + template: '' +}) +export abstract class CardBase { + /** + * The name of the template containing footer layout + */ + @Input() footerTemplate?: TemplateRef; + + /** + * The name of the template containing header layout + */ + @Input() headerTemplate?: TemplateRef; + + /** + * The default constructor + */ + constructor() { + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-config-base.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-config-base.ts new file mode 100644 index 000000000..fb4c807fb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-config-base.ts @@ -0,0 +1,19 @@ +/** + * A config containing properties for cards + */ +export class CardConfigBase { + /** + * Title for the card + */ + title?: string; + + /** + * Show a border after the title and sub-title. + */ + titleBorder?: boolean; + + /** + * Show a border above the card title. + */ + topBorder?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter-position.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter-position.ts new file mode 100644 index 000000000..833e07b9e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter-position.ts @@ -0,0 +1,14 @@ +/* + * An object containing filter positions + */ +export class CardFilterPosition { + /** + * Footer position + */ + static readonly FOOTER: string = 'footer'; + + /** + * Header position + */ + static readonly HEADER: string = 'header'; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.component.html new file mode 100644 index 000000000..809752e76 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.component.html @@ -0,0 +1,15 @@ +
+ +
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.component.ts new file mode 100644 index 000000000..c1190b687 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.component.ts @@ -0,0 +1,107 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { CardFilter } from '../card-filter/card-filter'; + +/** + * Card filter component + * + * Usage: + *
+ * // Individual module import
+ * import { CardFilterModule } from 'patternfly-ng/card';
+ * // Or
+ * import { CardFilterModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [CardFilterModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { CardFilter, CardFilterPosition } from 'patternfly-ng/card';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-card-filter', + templateUrl: './card-filter.component.html', + imports: [ + CommonModule, + BsDropdownModule + ] +}) +export class CardFilterComponent implements OnInit { + /** + * The card filters + */ + @Input() filters?: CardFilter[]; + + /** + * The event emitted when a filter is selected + */ + @Output('onFilterSelect') onSelect = new EventEmitter(); + + private _currentFilter!: CardFilter; + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + if (this.filters !== undefined && this.filters.length > 0) { + this.currentFilter = this.filters[0]; + this.filters.forEach((filter) => { + if (filter.default === true) { + this.currentFilter = filter; + return; + } + }); + } + } + + // Actions + + protected select($event: CardFilter): void { + this.currentFilter = $event; + this.onSelect.emit($event); + } + + // Accessors + + /** + * Returns the current filter + * + * @returns {CardFilter} The current filter + */ + get currentFilter(): CardFilter { + return this._currentFilter; + } + + /** + * Sets the current filter + * + * @param {CardFilter} filter The current filter + */ + set currentFilter(filter: CardFilter) { + this._currentFilter = filter; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.module.ts new file mode 100644 index 000000000..2988fa435 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { CardFilterComponent } from './card-filter.component'; + +/** + * A module containing objects associated with card filter components + */ +@NgModule({ + imports: [ + BsDropdownModule.forRoot(), + CommonModule, + FormsModule, + CardFilterComponent + ], + //declarations: [CardFilterComponent], + exports: [CardFilterComponent], + providers: [BsDropdownConfig] +}) +export class CardFilterModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.ts new file mode 100644 index 000000000..f067944d3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/card-filter.ts @@ -0,0 +1,24 @@ +/** + * An object containing card filter properties + */ +export class CardFilter { + /** + * The filter id + */ + id?: string; + + /** + * True if this filter should be selected by default + */ + default?: boolean; + + /** + * The title to display for the filter + */ + title!: string; + + /** + * Filter value + */ + value!: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/index.ts new file mode 100644 index 000000000..9453673f5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/card-filter/index.ts @@ -0,0 +1,4 @@ +export { CardFilter } from './card-filter'; +export { CardFilterComponent } from './card-filter.component'; +export { CardFilterPosition } from './card-filter-position'; +export { CardFilterModule } from './card-filter.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/index.ts new file mode 100644 index 000000000..1672e9ba9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/index.ts @@ -0,0 +1,7 @@ +export { CardBase } from './card-base'; +export { CardConfigBase } from './card-config-base'; + +export * from './card-action/index'; +export * from './basic-card/index'; +export * from './card-filter/index'; +export * from './info-status-card/index'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/index.ts new file mode 100644 index 000000000..9b9e2f31e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/index.ts @@ -0,0 +1,3 @@ +export { InfoStatusCardComponent } from './info-status-card.component'; +export { InfoStatusCardConfig } from './info-status-card-config'; +export { InfoStatusCardModule } from './info-status-card.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card-config.ts new file mode 100644 index 000000000..c7d4906dd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card-config.ts @@ -0,0 +1,39 @@ +/** + * A config containing properties for the info status card + */ +export class InfoStatusCardConfig { + /** + * Flag to allow parsing of HTML content within the info options + */ + htmlContent!: boolean; + + /** + * Show/hide the top border, true shows top border, false (default) hides top border + */ + showTopBorder?: boolean; + + /** + * The main title of the info status card + */ + title?: string; + + /** + * The href to navigate to if one clicks on the title or count + */ + href?: string; + + /** + * An icon to display to the left of the count + */ + iconStyleClass?: string; + + /** + * An image to display to the left of Infrastructure + */ + iconImageSrc?: string; + + /** + * An array of strings to display, each element in the array is on a new line, accepts HTML content + */ + info?: string[]; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.html new file mode 100644 index 000000000..a34a4d071 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.html @@ -0,0 +1,27 @@ +
+
+ + +
+
+

+ + {{config.title}} + + + {{config.title}} + +

+ +
+ +
+
+ +
+ {{item}} +
+
+
+
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.less new file mode 100644 index 000000000..cfcc74441 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.less @@ -0,0 +1,24 @@ +.pfng-card-info-status { + display: flex; + margin: 0 10px; + .pfng-card-info-image { + display: flex; + align-items: center; + justify-content: center; + flex-direction:column; + margin-right: 15px; + .info-icon { + font-size: 50px; + } + .info-img { + max-height: 50px; + } + } + .pfng-card-info-content { + margin: 10px 0; + .pfng-card-title { + margin-top: 10px; + margin-bottom: 15px; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.ts new file mode 100644 index 000000000..5435452ba --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.component.ts @@ -0,0 +1,91 @@ +import { + Component, + DoCheck, + Input, + OnInit, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { cloneDeep, defaults, isEqual } from 'lodash-es'; + +import { InfoStatusCardConfig } from './info-status-card-config'; + +/** + * Info Status Card Component + * + * Usage: + *
+ * // Individual module import
+ * import { InfoStatusCardModule } from 'patternfly-ng/card';
+ * // Or
+ * import { InfoStatusCardModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [InfoStatusCardModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { InfoStatusCardConfig } from 'patternfly-ng/card';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-info-status-card', + templateUrl: './info-status-card.component.html', + imports: [ + CommonModule + ] +}) +export class InfoStatusCardComponent implements OnInit, DoCheck { + + /** + * The config object containing component properties + */ + @Input('config') config!: InfoStatusCardConfig; + + private defaultConfig: InfoStatusCardConfig = { + showTopBorder: false, + htmlContent: true + }; + + private prevConfig!: InfoStatusCardConfig; + + /** + * The default constructor + */ + constructor() {} + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if any component config props have changed + */ + ngDoCheck(): void { + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + this.prevConfig = cloneDeep(this.config); + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.module.ts new file mode 100644 index 000000000..51cf6b986 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/card/info-status-card/info-status-card.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { InfoStatusCardComponent } from './info-status-card.component'; + +/** + * A module containing objects associated with info status card components + */ +@NgModule({ + imports: [ + CommonModule, + FormsModule, + InfoStatusCardComponent + ], + //declarations: [InfoStatusCardComponent], + exports: [InfoStatusCardComponent] +}) +export class InfoStatusCardModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-base.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-base.ts new file mode 100644 index 000000000..f8e4ed0da --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-base.ts @@ -0,0 +1,54 @@ +import { Component, EventEmitter, Output } from '@angular/core'; + +import { cloneDeep } from 'lodash-es'; +import * as c3 from 'c3'; + +import { ChartConfigBase } from './chart-config-base'; + +/** + * Chart base + * + * Note: In order to use charts, please include the following JavaScript file from patternfly. For example: + * require('patternfly/dist/js/patternfly-settings'); + */ +@Component({ + selector: 'pfng-chart-base', + template: '' +}) +export abstract class ChartBase { + /** + * Event emitted with the chart reference after load is complete + * @type {EventEmitter} + */ + @Output() chartLoaded: EventEmitter = new EventEmitter(); + + // Store the chart object + private chart: any; + + /** + * Default constructor + */ + constructor() {} + + /** + * Protected method called when configuration or data changes by any class that inherits from this + * + * @param config The config for the c3 chart + * @param reload True to reload + */ + protected generateChart(config: ChartConfigBase, reload?: boolean): void { + setTimeout(() => { + let c3Config: any = cloneDeep(config); + c3Config.bindto = '#' + config.chartId; + + // Note: Always re-generate donut pct chart because it's colors change based on data and thresholds + if (this.chart === undefined || reload === true) { + this.chart = c3.generate(c3Config); + } else { + // if chart is already created, then we only need to re-load data + this.chart.load(c3Config.data); + } + this.chartLoaded.emit(this.chart); + }); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-config-base.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-config-base.ts new file mode 100644 index 000000000..4325c7206 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-config-base.ts @@ -0,0 +1,16 @@ +/** + * A base config containing properties for charts + */ +export abstract class ChartConfigBase { + /** + * The id of the chart used in the markup + */ + chartId?: string; + + /** + * C3 inherited configuration for data + * + * See: http://c3js.org/reference.html#data + */ + data?: any; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-defaults.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-defaults.ts new file mode 100644 index 000000000..f062d20bc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/chart-defaults.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class ChartDefaults { + + protected patternflyDefaults: any = (window as any).patternfly.c3ChartDefaults(); + + public getDefaultColors = this.patternflyDefaults.getDefaultColors; + public getDefaultDonut = this.patternflyDefaults.getDefaultDonut; + public getDefaultDonutSize = this.patternflyDefaults.getDefaultDonutSize; + public getDefaultDonutColors = this.patternflyDefaults.getDefaultDonutColors; + public getDefaultRelationshipDonutColors = this.patternflyDefaults.getDefaultRelationshipDonutColors; + public getDefaultDonutLegend = this.patternflyDefaults.getDefaultDonutLegend; + public getDefaultDonutTooltip = this.patternflyDefaults.getDefaultDonutTooltip; + public getDefaultDonutConfig = this.patternflyDefaults.getDefaultDonutConfig; + public getDefaultSparklineArea = this.patternflyDefaults.getDefaultSparklineArea; + public getDefaultSparklineSize = this.patternflyDefaults.getDefaultSparklineSize; + public getDefaultSparklineAxis = this.patternflyDefaults.getDefaultSparklineAxis; + public getDefaultSparklineColor = this.patternflyDefaults.getDefaultColors; + public getDefaultSparklineLegend = this.patternflyDefaults.getDefaultSparklineLegend; + public getDefaultSparklinePoint = this.patternflyDefaults.getDefaultSparklinePoint; + public getDefaultSparklineTooltip = this.patternflyDefaults.getDefaultSparklineTooltip; + public getDefaultSparklineConfig = this.patternflyDefaults.getDefaultSparklineConfig; + public getDefaultLineConfig = this.patternflyDefaults.getDefaultLineConfig; + + constructor() { + } +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart-config.ts new file mode 100644 index 000000000..948272b79 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart-config.ts @@ -0,0 +1,7 @@ +import { DonutChartBaseConfig } from '../donut-chart-base-config'; + +/** + * A config containing properties for the sparkline chart + */ +export class DonutChartConfig extends DonutChartBaseConfig { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.component.html new file mode 100644 index 000000000..f93d5c690 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.component.html @@ -0,0 +1 @@ +
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.component.ts new file mode 100644 index 000000000..d52bed2d4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.component.ts @@ -0,0 +1,48 @@ +import { + Component, + ViewEncapsulation +} from '@angular/core'; + +import { ChartDefaults } from '../../chart-defaults'; +import { DonutChartBaseComponent } from '../donut-chart-base.component'; +import { WindowReference } from '../../../utilities/window.reference'; + +/** + * Donut chart component. + * + * Note: In order to use charts, please include the following JavaScript file from PatternFly. + *
+ * require('patternfly/dist/js/patternfly-settings');
+ * 
+ * + * Usage: + *
+ * // Individual module import
+ * import { DonutChartModule } from 'patternfly-ng/chart';
+ * // Or
+ * import { DonutChartModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [DonutChartModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { DonutChartConfig } from 'patternfly-ng/chart';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-donut-chart', + templateUrl: './donut-chart.component.html' +}) +export class DonutChartComponent extends DonutChartBaseComponent { + /** + * Default constructor + */ + constructor(protected override chartDefaults: ChartDefaults, protected override windowRef: WindowReference) { + super(chartDefaults, windowRef); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.module.ts new file mode 100644 index 000000000..bdb4b577d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/donut-chart.module.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { ChartDefaults } from '../../chart-defaults'; + +import { DonutChartComponent } from './donut-chart.component'; +import { WindowReference } from '../../../utilities/window.reference'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + DonutChartComponent + ], + //declarations: [DonutChartComponent], + exports: [DonutChartComponent], + providers: [ChartDefaults, WindowReference] +}) +export class DonutChartModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/index.ts new file mode 100644 index 000000000..c9b1e906b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/basic-donut-chart/index.ts @@ -0,0 +1,3 @@ +export { DonutChartComponent } from './donut-chart.component'; +export { DonutChartConfig } from './donut-chart-config'; +export { DonutChartModule } from './donut-chart.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/donut-chart-base-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/donut-chart-base-config.ts new file mode 100644 index 000000000..39878d92f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/donut-chart-base-config.ts @@ -0,0 +1,46 @@ +import { ChartConfigBase } from '../chart-config-base'; + +/** + * A config containing properties for the sparkline chart + */ +export class DonutChartBaseConfig extends ChartConfigBase { + /** + * Text for the donut chart center label (optional) + */ + centerLabel?: any; + + /** + * The height of the donut chart (optional) + */ + chartHeight?: number; + + /** + * C3 inherited configuration for colors + * + * colors : { + * Cats: '#0088ce', + * Hamsters: '#3f9c35', + * } + */ + colors?: any; + + /** + * C3 inherited donut configuration + */ + donut?: any; + + /** + * C3 inherited legend configuration + */ + legend?: any; + + /** + * C3 inherited configuration for size + */ + size?: any; + + /** + * C3 inherited configuration for tooltip + */ + tooltip?: any; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/donut-chart-base.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/donut-chart-base.component.ts new file mode 100644 index 000000000..d69ac1692 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/donut-chart-base.component.ts @@ -0,0 +1,186 @@ +import { + Component, + DoCheck, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { Subscription } from 'rxjs'; + +import { cloneDeep, defaultsDeep, isEqual, merge, uniqueId } from 'lodash-es'; + +import * as d3 from 'd3'; + +import { ChartDefaults } from '../chart-defaults'; +import { ChartBase } from '../chart-base'; +import { DonutChartBaseConfig } from './donut-chart-base-config'; +import { WindowReference } from '../../utilities/window.reference'; + +/** + * Donut base + */ +@Component({ + selector: 'pfng-donut-chart-base', + template: '' +}) +export abstract class DonutChartBaseComponent extends ChartBase implements DoCheck, OnDestroy, OnInit { + /** + * An array containing key value pairs: + * + * key - string representing an arc within the donut chart + * value - number representing the value of the arc + */ + @Input() chartData!: any[]; + + /** + * Configuration object containing details about how to render the chart + */ + @Input() config!: DonutChartBaseConfig; + + private defaultConfig!: DonutChartBaseConfig; + private prevChartData!: any[]; + private prevConfig!: DonutChartBaseConfig; + private subscriptions: Subscription[] = []; + + /** + * Default constructor + * @param chartDefaults + */ + constructor(protected chartDefaults: ChartDefaults, protected windowRef: WindowReference) { + super(); + this.subscriptions.push(this.chartLoaded.subscribe({ + next: (chart: any) => { + this.chartAvailable(chart); + } + })); + } + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfigDefaults(); + this.setupConfig(); + this.generateChart(this.config); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + const dataChanged = !isEqual(this.chartData, this.prevChartData); + if (dataChanged || !isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + this.generateChart(this.config, !dataChanged); + } + } + + /** + * Clean up subscriptions + */ + ngOnDestroy(): void { + this.subscriptions.forEach(sub => sub.unsubscribe); + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaultsDeep(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + + if (this.config.chartHeight !== undefined) { + this.config.size.height = this.config.chartHeight; + } + this.config.data = merge(this.config.data, this.getChartData()); + this.prevConfig = cloneDeep(this.config); + this.prevChartData = cloneDeep(this.chartData); + } + + /** + * Set up default config + */ + protected setupConfigDefaults(): void { + this.defaultConfig = this.chartDefaults.getDefaultDonutConfig(); + this.defaultConfig.chartId = uniqueId(); + this.defaultConfig.data = { + type: 'donut', + order: null + }; + this.defaultConfig.donut = this.chartDefaults.getDefaultDonut(); + this.defaultConfig.tooltip = { contents: (this.windowRef.nativeWindow).patternfly.pfDonutTooltipContents }; + } + + /** + * Convert chartData to C3 data property + */ + protected getChartData(): any { + return { + columns: this.chartData, + colors: this.config.colors + }; + } + + /** + * Returns an object containing center label properties + * @returns {any} + */ + getCenterLabelText(): any { + // Public for testing + let centerLabelText = { + title: this.getTotal(), + subTitle: this.config.donut.title + }; + if (this.config.centerLabel) { + centerLabelText.title = this.config.centerLabel; + centerLabelText.subTitle = ''; + } + return centerLabelText; + } + + // Private + + private chartAvailable(chart: any): void { + this.setupDonutChartTitle(chart); + } + + private getTotal(): number { + let total = 0; + if (this.config.data !== undefined && this.config.data.columns !== undefined) { + this.config.data.columns.forEach((element: any) => { + if (!isNaN(element[1])) { + total += Number(element[1]); + } + }); + } + return total; + } + + private setupDonutChartTitle(chart: any): void { + let donutChartTitle, centerLabelText; + + if (chart === undefined) { + return; + } + + donutChartTitle = d3.select(chart.element).select('text.c3-chart-arcs-title'); + if (donutChartTitle === undefined) { + return; + } + + centerLabelText = this.getCenterLabelText(); + + donutChartTitle.text(''); + if (centerLabelText.title && !centerLabelText.subTitle) { + donutChartTitle.text(centerLabelText.title); + } else { + donutChartTitle.insert('tspan').text(centerLabelText.title) + .classed('donut-title-big-pf', true).attr('dy', 0).attr('x', 0); + donutChartTitle.insert('tspan').text(centerLabelText.subTitle). + classed('donut-title-small-pf', true).attr('dy', 20).attr('x', 0); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/index.ts new file mode 100644 index 000000000..c2ae6edb6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/donut-chart/index.ts @@ -0,0 +1,5 @@ +export { DonutChartBaseComponent } from './donut-chart-base.component'; +export { DonutChartBaseConfig } from './donut-chart-base-config'; + +export * from './basic-donut-chart/index'; +//export * from './utilization-donut-chart/index'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/index.ts new file mode 100644 index 000000000..56d2d5e92 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/index.ts @@ -0,0 +1,8 @@ +export { ChartBase } from './chart-base'; +export { ChartConfigBase } from './chart-config-base'; +export { ChartDefaults } from './chart-defaults'; + +export * from './donut-chart/index'; +export * from './donut-chart/basic-donut-chart/index'; +export * from './sparkline-chart/index'; +//export * from './donut-chart/utilization-donut-chart/index'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/index.ts new file mode 100644 index 000000000..13f0754ae --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/index.ts @@ -0,0 +1,4 @@ +export { SparklineChartComponent } from './sparkline-chart.component'; +export { SparklineChartConfig } from './sparkline-chart-config'; +export { SparklineChartData } from './sparkline-chart-data'; +export { SparklineChartModule } from './sparkline-chart.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart-config.ts new file mode 100644 index 000000000..761b8f99f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart-config.ts @@ -0,0 +1,50 @@ +import { ChartConfigBase } from '../chart-config-base'; + +/** + * A config containing properties for the sparkline chart + */ +export class SparklineChartConfig extends ChartConfigBase { + /** + * C3 inherited configuration for axis + */ + axis?: any; + + /** + * The height of the chart + */ + chartHeight?: number; + + /** + * Boolean to indicate whether or not to show the x axis + */ + showXAxis?: boolean; + + /** + * Boolean to indicate whether or not to show the y axis + */ + showYAxis?: boolean; + + /** + * C3 inherited configuration for size object + * + * See: http://c3js.org/reference.html#size + */ + size?: any; + + /** + * C3 inherited configuration for tooltip + * + * See: http://c3js.org/reference.html#tooltip + */ + tooltip?: any; + + /** + * Options include usagePerDay, valuePerDay, percentage or default + */ + tooltipType?: string; + + /** + * The unit of measure for the chart + */ + units?: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart-data.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart-data.ts new file mode 100644 index 000000000..1d4babb5f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart-data.ts @@ -0,0 +1,24 @@ +/** + * A base config containing properties for chart data + */ +export abstract class SparklineChartData { + /** + * True is data is available + */ + dataAvailable?: boolean; + + /** + * The Total amount, used when determining percentages + */ + total?: number; + + /** + * X values for the data points, first element must be the name of the data + */ + xData?: any[]; + + /** + * Y Values for the data points, first element must be the name of the data + */ + yData?: any[]; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.component.html new file mode 100644 index 000000000..f93d5c690 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.component.html @@ -0,0 +1 @@ +
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.component.ts new file mode 100644 index 000000000..0664c7c9c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.component.ts @@ -0,0 +1,263 @@ +import { + Component, + DoCheck, + Input, + OnInit, + ViewEncapsulation +} from '@angular/core'; + +import { cloneDeep, defaultsDeep, isEqual, merge, uniqueId } from 'lodash-es'; + +import { ChartBase } from '../chart-base'; +import { ChartDefaults } from '../chart-defaults'; +import { SparklineChartConfig } from './sparkline-chart-config'; +import { SparklineChartData } from './sparkline-chart-data'; + +/** + * Sparkline chart component + * + * Note: In order to use charts, please include the following JavaScript file from PatternFly. + *
+ * require('patternfly/dist/js/patternfly-settings');
+ * 
+ * + * Usage: + *
+ * // Individual module import
+ * import { SparklineChartModule } from 'patternfly-ng/chart';
+ * // Or
+ * import { SparklineChartModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [SparklineChartModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { SparklineChartConfig, SparklineChartData } from 'patternfly-ng/chart';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-sparkline-chart', + templateUrl: './sparkline-chart.component.html' +}) +export class SparklineChartComponent extends ChartBase implements DoCheck, OnInit { + /** + * Chart data + */ + @Input() chartData!: SparklineChartData; + + /** + * Configuration object containing details about how to render the chart + */ + @Input() config!: SparklineChartConfig; + + private defaultConfig!: SparklineChartConfig; + private prevChartData!: SparklineChartData; + private prevConfig!: SparklineChartConfig; + + /** + * Default constructor + * @param chartDefaults + */ + constructor(protected chartDefaults: ChartDefaults) { + super(); + } + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfigDefaults(); + this.setupConfig(); + this.generateChart(this.config, true); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + const dataChanged = !isEqual(this.chartData, this.prevChartData); + if (dataChanged || !isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + this.generateChart(this.config, !dataChanged); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaultsDeep(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + + /* + * Setup Axis options. Default is to not show either axis. This can be overridden in two ways: + * 1) in the config, setting showAxis to true will show both axes + * 2) in the attributes showXAxis and showYAxis will override the config if set + * + * By default only line and the tick marks are shown, no labels. This is a sparkline and should be used + * only to show a brief idea of trending. This can be overridden by setting the config.axis options per C3 + */ + if (this.config.axis !== undefined) { + this.config.axis.x.show = this.config.showXAxis === true; + this.config.axis.y.show = this.config.showYAxis === true; + } + if (this.config.chartHeight !== undefined) { + this.config.size.height = this.config.chartHeight; + } + this.config.data = merge(this.config.data, this.getChartData()); + this.prevConfig = cloneDeep(this.config); + this.prevChartData = cloneDeep(this.chartData); + } + + /** + * Set up config defaults + */ + protected setupConfigDefaults(): void { + this.defaultConfig = this.chartDefaults.getDefaultSparklineConfig(); + + this.defaultConfig.axis = { + x: { + show: this.config.showXAxis === true, + type: 'timeseries', + tick: { + format: () => { + return ''; // change to lambda ? + } + } + }, + y: { + show: this.config.showYAxis === true, + tick: { + format: () => { + return ''; // change to lambda ? + } + } + } + }; + this.defaultConfig.chartId = uniqueId(this.config.chartId); + this.defaultConfig.data = { type: 'area' }; + this.defaultConfig.tooltip = this.tooltip(); + this.defaultConfig.units = ''; + } + + // Chart + + /** + * Convert chartData to C3 data property + */ + protected getChartData(): any { + let data: any = {}; + + if (this.chartData && this.chartData.dataAvailable !== false && this.chartData.xData && this.chartData.yData) { + data.x = this.chartData.xData[0]; + data.columns = [ + this.chartData.xData, + this.chartData.yData + ]; + } + return data; + } + + /** + * Tooltip function for sparklines + * + * @returns {{contents: ((d:any)=>string), position: ((data:any, width:number, + * height:number, element:any)=>{top: number, left: number})}} + */ + tooltip(): any { + return { + contents: (d: any) => { + let tipRows; + let percentUsed = 0; + + switch (this.config.tooltipType) { + case 'usagePerDay': + if (this.chartData && this.chartData.dataAvailable !== false && (this.chartData.total || 0) > 0) { + percentUsed = Math.round(d[0].value / (this.chartData.total || 0) * 100.0); + } + tipRows = + '' + + ' ' + d[0].x.toLocaleDateString() + '' + + '' + + '' + + ' ' + percentUsed + '%:' + '' + + ' ' + d[0].value + ' ' + + (this.config.units ? this.config.units + ' ' : '') + d[0].name + '' + + ''; + break; + case 'valuePerDay': + tipRows = + '' + + ' ' + d[0].x.toLocaleDateString() + '' + + ' ' + d[0].value + ' ' + d[0].name + '' + + ''; + break; + case 'percentage': + percentUsed = Math.round(d[0].value / (this.chartData.total || 0) * 100.0); + tipRows = + '' + + ' ' + percentUsed + '%' + '' + + ''; + break; + default: + tipRows = this.chartDefaults.getDefaultSparklineTooltip().contents(d); + } + return this.getTooltipTableHTML(tipRows); + }, + position: (data: any, width: number, height: number, element: any) => { + let center; + let top; + let chartBox; + let graphOffsetX; + let x; + + try { + center = parseInt(element.getAttribute('x'), 10); + top = parseInt(element.getAttribute('y'), 10); + const chartElement = document.querySelector('#' + this.config.chartId); + const axisYElement = document.querySelector('#' + this.config.chartId + ' g.c3-axis-y'); + + if (chartElement && axisYElement) { + chartBox = chartElement.getBoundingClientRect(); + graphOffsetX = axisYElement.getBoundingClientRect().right; + + x = Math.max(0, center + graphOffsetX - chartBox.left - Math.floor(width / 2)); + + // As width is now always the same, we can't use it to calculate the position. + // Math.min() below will always return 0 so we just put a fix padding.... + return { + top: top - height + 10, + left: Math.min(x, chartBox.width - width) + 10 + }; + } + + throw new Error('Error getting chart element'); + } catch (e) { + console.log('Error getting tooltip position', e); + } + + return { top: 0, left: 0 } + } + }; + } + + // Private + + private getTooltipTableHTML(tipRows: any): string { + return '
' + + ' ' + + ' ' + + tipRows + + ' ' + + '
' + + '
'; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.module.ts new file mode 100644 index 000000000..cfd1250ab --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/chart/sparkline-chart/sparkline-chart.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { ChartDefaults } from '../chart-defaults'; +import { SparklineChartComponent } from './sparkline-chart.component'; +import { WindowReference } from '../../utilities/window.reference'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + SparklineChartComponent + ], + //declarations: [SparklineChartComponent], + exports: [SparklineChartComponent], + providers: [ChartDefaults, WindowReference] +}) +export class SparklineChartModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state-config.ts new file mode 100644 index 000000000..5171eaa54 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state-config.ts @@ -0,0 +1,46 @@ +import { ActionConfig } from '../action/action-config'; + +/** + * An empty state config containing component properties + */ +export class EmptyStateConfig { + /** + * The action config containing button properties + */ + actions?: ActionConfig; + + /** + * Config properties for the help link + */ + helpLink?: { + /** + * Help link text + */ + hypertext: string; + + /** + * Help link description + */ + text?: string; + + /** + * Help link URL + */ + url: string; + }; + + /** + * Style class for main icon (e.g., 'pficon pficon-add-circle-o') + */ + iconStyleClass?: string; + + /** + * Text for the main informational paragraph + */ + info?: string; + + /** + * Text for the main title + */ + title!: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.html new file mode 100644 index 000000000..1fac366dd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.html @@ -0,0 +1,39 @@ +
+
+ +
+

+ {{config.title}} +

+

+ {{config.info}} +

+ +
+ +
+
+ +
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.less new file mode 100644 index 000000000..7eaa17753 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.less @@ -0,0 +1,6 @@ +.blank-slate-pf { + margin-bottom: 0; + button { + margin-right: 4px; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.ts new file mode 100644 index 000000000..9f2d95bee --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.component.ts @@ -0,0 +1,106 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; + +import { cloneDeep, defaults, isEqual } from 'lodash-es'; + +import { Action } from '../action/action'; +import { EmptyStateConfig } from './empty-state-config'; +import { CommonModule } from '@angular/common'; + +/** + * Component for rendering an empty state. + * + * Usage: + *
+ * // Individual module import
+ * import { EmptyStateModule } from 'patternfly-ng/empty-state';
+ * // Or
+ * import { EmptyStateModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [EmptyStateModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { EmptyStateConfig } from 'patternfly-ng/empty-state';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-empty-state', + templateUrl: './empty-state.component.html', + imports: [ + CommonModule + ] +}) +export class EmptyStateComponent implements DoCheck, OnInit { + /** + * The empty state config containing component properties + */ + @Input() config!: EmptyStateConfig; + + /** + * The event emitted when an action is selected + */ + @Output('onActionSelect') onActionSelect = new EventEmitter(); + + private defaultConfig = { + title: 'No Items Available' + } as EmptyStateConfig; + private prevConfig!: EmptyStateConfig; + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + this.prevConfig = cloneDeep(this.config); + } + + // Private + + protected handleAction(action: Action): void { + if (action && action.disabled !== true) { + this.onActionSelect.emit(action); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.module.ts new file mode 100644 index 000000000..cfe61217e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/empty-state.module.ts @@ -0,0 +1,17 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { EmptyStateComponent } from './empty-state.component'; + +/** + * A module containing objects associated with the empty state component + */ +@NgModule({ + imports: [ + CommonModule, + EmptyStateComponent + ], + //declarations: [EmptyStateComponent], + exports: [EmptyStateComponent] +}) +export class EmptyStateModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/index.ts new file mode 100644 index 000000000..d2083f947 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/empty-state/index.ts @@ -0,0 +1,3 @@ +export { EmptyStateComponent } from './empty-state.component'; +export { EmptyStateConfig } from './empty-state-config'; +export { EmptyStateModule } from './empty-state.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-config.ts new file mode 100644 index 000000000..08171aa31 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-config.ts @@ -0,0 +1,47 @@ +import { Filter } from './filter'; +import { FilterField } from './filter-field'; + +/** + * A config containing properties for filters + */ +export class FilterConfig { + /** + * A list of the currently applied filters + */ + appliedFilters?: Filter[]; + + /** + * A flag indicating the component is disabled + */ + disabled?: boolean; + + /** + * A list of filterable fields + */ + fields!: FilterField[]; + + /** + * The number of results returned after the current applied filters have been applied + */ + resultsCount?: number; + + /** + * The number selected items + */ + selectedCount?: number; + + /** + * Show the control to save the currently applied filter + */ + showSaveFilter?: boolean; + + /** + * The total number of items before any filters have been applied + */ + totalCount?: number; + + /** + * The tooltip placement (e.g., bottom, left, top, right) + */ + tooltipPlacement?: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-event.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-event.ts new file mode 100644 index 000000000..579d438be --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-event.ts @@ -0,0 +1,28 @@ +import { Filter } from './filter'; +import { FilterField } from './filter-field'; +import { FilterQuery } from './filter-query'; + +/** + * An object containing properties for filter events + */ +export class FilterEvent { + /** + * A list of the currently applied filters + */ + appliedFilters?: Filter[]; + + /** + * The currently selected filter field + */ + field?: FilterField; + + /** + * The currently selected filter query, if applicable + */ + query?: FilterQuery; + + /** + * The filter input field value, if applicable + */ + value?: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-field.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-field.ts new file mode 100644 index 000000000..f7fdf02f4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-field.ts @@ -0,0 +1,41 @@ +import { FilterQuery } from './filter-query'; + +/** + * An object containing properties for a filterable field, used to select categories of filters + */ +export class FilterField { + /** + * A unique Id for the filter field + */ + id?: string; + + /** + * Text to display when no filter value has been entered + */ + placeholder?: string; + + /** + * A list of filter queries used when filterType is 'select' + */ + queries?: FilterQuery[]; + + /** + * The title to display for the filter field + */ + title?: string; + + /** + * The filter input field type (e.g., 'select' for a select box, 'typeahead' to filter queries) + */ + type?: string; + + /** + * Set to true when a separator should be shown instead of a menu option + */ + separator?: boolean; + + /** + * A flag indicating the field is disabled + */ + disabled?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.html new file mode 100644 index 000000000..741be5ba8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.html @@ -0,0 +1,111 @@ +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.less new file mode 100644 index 000000000..4e514947a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.less @@ -0,0 +1,129 @@ +.filter-pf { + a { cursor: pointer; } + &.filter-fields { + .form-group { + padding-left: 0; + width: 275px; + } + .tooltip { + white-space: nowrap; + } + .typeahead-input-container { + &.disabled { + width: 100%; + .caret { + top: 8px; + } + } + position: relative; + padding-right: 0; + .caret { + color: @color-pf-black-500; + font-style: italic; + position: absolute; + top: 10px; + right: 12px; + z-index: 2; + } + } + } +} + +.filter-select { + .btn-default { + background-color: @color-pf-white; + background-image: none; + .placeholder { + color: @color-pf-black-500; + font-style: italic; + font-weight: 400; + } + } + .avatar { + height: 20px; + margin-right: 5px; + } +} + +/* stylelint-disable */ +.input-group { + .input-group-btn { + .dropdown-menu>.selected>a { + background-color: @color-pf-blue !important; + border-color: @color-pf-blue-400 !important; // was #0076b7 + color: @color-pf-white; + } + } +} +/* styleint-enable */ + +.pfng-filter-delete { + font-size: 12px; + opacity: 0; + padding-top: 4px; + a { + color: @color-pf-black; + opacity: .7; + &:hover { + opacity: 1; + } + } + .dropdown-item:hover & { + opacity: 1; + transition-duration: .5s; + transition-property: opacity; + transition-timing-function: ease-in; + } +} + +.pfng-filter-delete-confirm { + font-size: 12px; + padding-bottom: 5px; + padding-right: 5px; + padding-top: 5px; + a { + color: @color-pf-white; + opacity: .9; + .fa:before { + color: @color-pf-white; + } + &:hover { + opacity: 1; + } + } +} + +.pfng-filter-delete-slide { + background-color: @color-pf-red; + right: -100%; + position: absolute; + transition-duration: 1s; + width: 100%; + z-index: 1000; + &.slide-in { + right: 0; + .close { + opacity: .9; + transition-delay: .5s; + transition-duration: .5s; + transition-property: opacity; + transition-timing-function: ease-in; + } + } +} + +.pfng-filter-delete-text { + color: @color-pf-white; + padding-left: 10px; + padding-top: 2px; + position: absolute; +} + +.pfng-filter-delete-wrapper { + position: relative; + overflow: hidden; +} + +.table-view-pf-select-results { + padding-top: 10px; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.ts new file mode 100644 index 000000000..89063c113 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-fields.component.ts @@ -0,0 +1,299 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { TooltipDirective } from 'ngx-bootstrap/tooltip'; + +import { cloneDeep, defaults, find, isEqual } from 'lodash-es'; + +import { FilterConfig } from './filter-config'; +import { FilterEvent } from './filter-event'; +import { FilterField } from './filter-field'; +import { FilterQuery } from './filter-query'; +import { SearchHighlightPipeModule, TruncatePipeModule } from '../pipe'; + +/** + * Helper component for the filter query field and filter query dropdown + */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-filter-fields', + templateUrl: './filter-fields.component.html', + imports: [ + BsDropdownModule, + CommonModule, + FormsModule, + SearchHighlightPipeModule, + TooltipDirective, + TruncatePipeModule + ] +}) +export class FilterFieldsComponent implements DoCheck, OnInit { + /** + * The filter config containing component properties + */ + @Input() config?: FilterConfig; + + /** + * The event emitted when a filter has been added + */ + @Output('onAdd') onAdd = new EventEmitter(); + + /** + * The event emitted when a saved filter has been deleted + */ + @Output('onDelete') onDelete = new EventEmitter(); + + /** + * The event emitted when a field menu option is selected + */ + @Output('onFieldSelect') onFieldSelect = new EventEmitter(); + + /** + * The event emitted when the user types ahead in the query input field + */ + @Output('onTypeAhead') onTypeAhead = new EventEmitter(); + + private _currentField?: FilterField; + private _currentValue?: string | null; + private defaultConfig = { + disabled: false + } as FilterConfig; + private prevConfig?: FilterConfig; + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + + if (this.config && this.config.fields === undefined) { + this.config.fields = []; + } + if (this.config && this.config.tooltipPlacement === undefined) { + this.config.tooltipPlacement = 'top'; + } + this.initCurrentField(); + this.prevConfig = cloneDeep(this.config); + } + + /** + * Initialize current field and value + */ + protected initCurrentField(): void { + let fieldFound: boolean = false; + if (this._currentField !== undefined) { + this.config?.fields.forEach((nextField) => { + if (nextField.id === this._currentField?.id) { + fieldFound = true; + return; + } + }); + } + if (!fieldFound) { + this._currentField = this.config?.fields[0]; + this._currentValue = null; + } else if (this._currentField?.type === 'select' || this._currentField?.type === 'typeahead') { + // clear dropdown if there is no applied filter for it + if (!this.getAppliedFilterByField(this._currentField)) { + this._currentValue = null; + } + } + if (this._currentValue === undefined) { + this._currentValue = null; + } + } + + protected getAppliedFilterByField(field: any): any { + let foundFilter = find(this.config?.appliedFilters, { + field: field + }); + return foundFilter; + } + + /** + * Reset current field and value + */ + reset(): void { + this._currentField = undefined; + this.initCurrentField(); + } + + // Accessors + + /** + * Get the current filter field + * + * @returns {FilterField} The current filter field + */ + get currentField(): FilterField { + return this._currentField!; + } + + /** + * Get the current filter field value + * + * @returns {string} The current filter field value + */ + protected get currentValue(): string { + return this._currentValue!; + } + + /** + * Set the current filter field value + * + * @param val The current filter field value + */ + protected set currentValue(val: string) { + this._currentValue = val; + } + + // Private + + deleteQuery($event: MouseEvent, filterQuery: FilterQuery, el: HTMLElement): void { + // Unset focus + if (el !== undefined) { + el.blur(); + } + + // Close previous open confirmation + this.hideDeleteConfirm(false); + + // Show delete query confirmation + (filterQuery as any).showDeleteConfirm = true; + + // Menu should remain open + $event.stopPropagation(); + } + + deleteQueryCancel($event: MouseEvent, filterQuery: FilterQuery): void { + // Hide delete query confirmation + (filterQuery as any).showDeleteConfirm = false; + + // Menu should remain open + $event.stopPropagation(); + } + + deleteQueryConfirm($event: MouseEvent, filterQuery: FilterQuery): void { + // Hide delete query confirmation + (filterQuery as any).showDeleteConfirm = false; + + // Menu should remain open + if (this._currentField && this._currentField.queries && this._currentField.queries.length > 1) { + $event.stopPropagation(); + } + this.onDelete.emit({ + field: this._currentField, + query: filterQuery, + value: filterQuery.value + } as FilterEvent); + this._currentValue = null; + } + + fieldInputKeyPress($event: KeyboardEvent): void { + if ($event.which === 13 && this._currentValue && this._currentValue.length > 0) { + this.onAdd.emit({ + field: this._currentField, + value: this._currentValue + } as FilterEvent); + this._currentValue = undefined; + } + } + + // Hide all delete confirm + hideDeleteConfirm($isOpen: boolean): void { + this._currentField?.queries?.forEach(query => { + (query as any).showDeleteConfirm = false; + }); + } + + isFieldDisabled(field: FilterField): boolean { + if (field.disabled) { + return true; + } + if (field.type === undefined || field.type === 'text') { + return false; + } + return (field.queries === undefined || field.queries.length === 0); + } + + queryInputChange(value: string) { + this.onTypeAhead.emit({ + field: this._currentField, + value: this._currentValue + } as FilterEvent); + } + + selectField(field: FilterField): void { + this._currentField = field; + if (this._currentField.type === 'select' || this._currentField.type === 'typeahead') { + // Restore selected value for dropdown if there is an applied filter for it + let filterField: any = this.getAppliedFilterByField(this._currentField); + this._currentValue = filterField ? filterField.value : null; + } else { + this._currentValue = null; + } + this.onFieldSelect.emit({ + field: this._currentField, + value: this._currentValue + } as FilterEvent); + } + + selectQuery(filterQuery: FilterQuery): void { + this.onAdd.emit({ + field: this._currentField, + query: filterQuery, + value: filterQuery.value + } as FilterEvent); + this._currentValue = filterQuery.value; + } + + showDelete(): boolean { + let result = false; + this._currentField?.queries?.forEach(query => { + if (query.showDelete === true) { + result = true; + return; + } + }); + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-query.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-query.ts new file mode 100644 index 000000000..073191e64 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-query.ts @@ -0,0 +1,39 @@ +/** + * An object containing properties for a filterable query, used when filterType is 'select' + */ +export class FilterQuery { + /** + * A unique Id for the filter query + */ + id?: string; + + /** + * Filter query value used when filterType is 'select' + */ + value!: string; + + /** + * The URL used to show an image + */ + imageUrl?: string; + + /** + * Style class used to show an icon (e.g., 'fa fa-bookmark') + */ + iconStyleClass?: string; + + /** + * Set to true when a separator should be shown instead of a menu option + */ + separator?: boolean; + + /** + * Show the control to delete a filter query + */ + showDelete?: boolean; + + /** + * Show the control to delete a filter query confirmation + */ + showDeleteConfirm?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.html new file mode 100644 index 000000000..d618b5a51 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.html @@ -0,0 +1,55 @@ +
+
+
+
{{config.resultsCount}} Results
+

Active filters:

+
    +
  • + + {{filter.field.title}}: {{filter.value}} + + +
  • +
+

+ Clear All Filters +

+

+ + + + + +

+ +
+
+ + + + Save Filter + + Save Filter +

+
+
+ {{config.selectedCount}} of {{config.totalCount}} selected +
+
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.less new file mode 100644 index 000000000..3f2e822eb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.less @@ -0,0 +1,33 @@ +.filter-pf { + a { cursor: pointer; } + .pficon-close {cursor: pointer; } +} + +.pfng-save-filter-close { + font-size: 12px; + padding-top: 3px; +} + +.pfng-save-filter { + input { + display: inline; + @media (min-width: @screen-xs-min) { width: 10em; } + @media (min-width: @screen-sm-min) { width: 15em; } + } + .popover { + max-width: initial; + } +} + +.pfng-save-filter-divider { + border-top: 1px solid @color-pf-black-300; + margin-left: -15px; + margin-right: -15px; + margin-top: 10px; +} + +.pfng-save-filter-footer { + float: right; + margin-bottom: 10px; + margin-top: 10px; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.ts new file mode 100644 index 000000000..141931004 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-results.component.ts @@ -0,0 +1,136 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { PopoverModule } from 'ngx-bootstrap/popover'; + +import { clone, cloneDeep, defaults, isEqual } from 'lodash-es'; + +import { Filter } from './filter'; +import { FilterConfig } from './filter-config'; +import { FilterEvent } from './filter-event'; + +/** + * Helper component for the filter results + */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-filter-results', + templateUrl: './filter-results.component.html', + imports: [ + CommonModule, + FormsModule, + PopoverModule + ] +}) +export class FilterResultsComponent implements DoCheck, OnInit { + /** + * The filter config containing component properties + */ + @Input() config?: FilterConfig; + + /** + * The event emitted when the clear action is selected + */ + @Output('onClear') onClear = new EventEmitter(); + + /** + * The event emitted when the save action is selected + */ + @Output('onSave') onSave = new EventEmitter(); + + private defaultConfig = { + disabled: false + } as FilterConfig; + private prevConfig: FilterConfig = new FilterConfig; + saveFilterName!: string; + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + + if (this.config && this.config.appliedFilters === undefined) { + this.config.appliedFilters = []; + } + if (this.config && this.config.resultsCount === undefined) { + this.config.resultsCount = 0; + } + if (this.config && this.config.selectedCount === undefined) { + this.config.selectedCount = 0; + } + if (this.config && this.config.totalCount === undefined) { + this.config.totalCount = 0; + } + this.prevConfig = cloneDeep(this.config); + } + + // Private + + clearFilter(filter: Filter): void { + let newFilters: Filter[] = []; + this.config?.appliedFilters?.forEach((appliedFilter) => { + if (appliedFilter.field.title !== filter.field.title + || appliedFilter.value !== filter.value) { + newFilters.push(appliedFilter); + } + }); + if (this.config) { + this.config.appliedFilters = newFilters; + } + if (this.config) { + this.onClear.emit(this.config.appliedFilters); + } + } + + clearAllFilters(): void { + this.config!.appliedFilters = []; + this.onClear.emit(this.config!.appliedFilters); + } + + saveAllFilters(): void { + this.onSave.emit({ + appliedFilters: this.config?.appliedFilters, + value: clone(this.saveFilterName) + } as FilterEvent); + this.saveFilterName = ''; // Reset + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-type.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-type.ts new file mode 100644 index 000000000..30a7bcda8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter-type.ts @@ -0,0 +1,19 @@ +/* + * An object containing properties for filter types + */ +export class FilterType { + /** + * Select type + */ + static readonly SELECT: string = 'select'; + + /** + * Text type + */ + static readonly TEXT: string = 'text'; + + /** + * Type ahead type + */ + static readonly TYPEAHEAD: string = 'typeahead'; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.html new file mode 100644 index 000000000..243171f69 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.html @@ -0,0 +1,11 @@ +
+ + +
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.less new file mode 100644 index 000000000..782add571 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.less @@ -0,0 +1,10 @@ +.filter-pf { + a { cursor: pointer; } +} +/* Fixes issue #130 */ +.filter-select { + .btn { + border-left: 0; + } +} +.dropdown-menu { min-width: 176px; } diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.ts new file mode 100644 index 000000000..fd02ede87 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.component.ts @@ -0,0 +1,236 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; + +import { cloneDeep, defaults, find, isEqual, remove } from 'lodash-es'; + +import { Filter } from './filter'; +import { FilterConfig } from './filter-config'; +import { FilterEvent } from './filter-event'; +import { FilterFieldsComponent } from './filter-fields.component'; +import { FilterType } from './filter-type'; +import { FilterResultsComponent } from './filter-results.component'; + +/** + * Filter component + * + * Usage: + *
+ * // Individual module import
+ * import { FilterModule } from 'patternfly-ng/filter';
+ * // Or
+ * import { FilterModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [FilterModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import {
+ *   Filter,
+ *   FilterConfig,
+ *   FilterField,
+ *   FilterEvent,
+ *   FilterType
+ * } from 'patternfly-ng/filter';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-filter', + templateUrl: './filter.component.html', + imports: [ + FilterFieldsComponent, + FilterResultsComponent + ] +}) +export class FilterComponent implements DoCheck, OnInit { + /** + * The filter config containing component properties + */ + @Input() config!: FilterConfig; + + /** + * The event emitted when a filter has been changed + */ + @Output('onChange') onChange = new EventEmitter(); + + /** + * The event emitted when a query (i.e., saved filter) has been deleted + */ + @Output('onDelete') onDelete = new EventEmitter(); + + /** + * The event emitted when a field menu option is selected + */ + @Output('onFieldSelect') onFilterSelect = new EventEmitter(); + + /** + * The event emitted when a filter has been changed + */ + @Output('onSave') onSave = new EventEmitter(); + + /** + * The event emitted when the user types ahead in the query input field + */ + @Output('onTypeAhead') onTypeAhead = new EventEmitter(); + + /** + * A reference to the underlying filter fields component + */ + @ViewChild('filterFields') private filterFields!: FilterFieldsComponent; + + private defaultConfig = { + disabled: false + } as FilterConfig; + private prevConfig: FilterConfig = new FilterConfig; + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + + if (this.config && this.config.appliedFilters === undefined) { + this.config.appliedFilters = []; + } + this.prevConfig = cloneDeep(this.config); + } + + // Actions + + /** + * Handle add filter event + * + * @param $event The FilterEvent contining properties for this event + */ + addFilter($event: FilterEvent): void { + let newFilter = { + field: $event.field, + query: $event.query, + value: $event.value + } as Filter; + + if (!this.filterExists(newFilter)) { + if (newFilter.field.type === FilterType.SELECT || newFilter.field.type === FilterType.TYPEAHEAD) { + this.enforceSingleSelect(newFilter); + } + if (this.config.appliedFilters) { + this.config.appliedFilters.push(newFilter); + } + $event.appliedFilters = this.config.appliedFilters; + this.onChange.emit($event); + } + } + + /** + * Handle clear filter event + * + * @param $event An array of current Filter objects + */ + clearFilter($event: Filter[]): void { + this.config.appliedFilters = $event; + this.onChange.emit({ + appliedFilters: $event + } as FilterEvent); + } + + /** + * Handle delete query (i.e., saved filter) event + * + * @param $event The FilterEvent contining properties for this event + */ + deleteQuery($event: FilterEvent): void { + this.onDelete.emit($event); + } + + /** + * Handle filter field selected event + * + * @param $event The FilterEvent contining properties for this event + */ + fieldSelected($event: FilterEvent): void { + this.onFilterSelect.emit($event); + } + + /** + * Reset current field + */ + resetCurrentField(): void { + this.filterFields.reset(); + } + + /** + * Handle save filter event + * + * @param $event An array of current Filter objects + */ + saveFilter($event: FilterEvent): void { + this.onSave.emit($event); + } + + /** + * Handle type ahead event + * + * @param $event The FilterEvent contining properties for this event + */ + typeAhead($event: FilterEvent) { + this.onTypeAhead.emit($event); + } + + // Private + + private enforceSingleSelect(filter: Filter): void { + const filterField = { title: filter.field.title }; + if (this.config.appliedFilters) { + remove(this.config.appliedFilters, { field: filterField }); + } + } + + private filterExists(filter: Filter): boolean { + let foundFilter = find(this.config.appliedFilters, { + field: filter.field, + value: filter.value + }); + return foundFilter !== undefined; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.module.ts new file mode 100644 index 000000000..e12956f29 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.module.ts @@ -0,0 +1,35 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { PopoverModule } from 'ngx-bootstrap/popover'; +import { TooltipConfig, TooltipModule } from 'ngx-bootstrap/tooltip'; + +import { FilterComponent } from './filter.component'; +import { FilterFieldsComponent } from './filter-fields.component'; +import { FilterResultsComponent } from './filter-results.component'; +import { SearchHighlightPipeModule } from '../pipe/search-highlight/search-highlight.pipe.module'; +import { TruncatePipeModule } from '../pipe/truncate/truncate.pipe.module'; + +/** + * A module containing objects associated with filter components + */ +@NgModule({ + imports: [ + BsDropdownModule.forRoot(), + CommonModule, + FilterComponent, FilterFieldsComponent, FilterResultsComponent, + FormsModule, + PopoverModule.forRoot(), + SearchHighlightPipeModule, + TooltipModule.forRoot(), + TruncatePipeModule + ], + declarations: [ + //FilterComponent, FilterFieldsComponent, FilterResultsComponent + ], + exports: [FilterComponent, FilterFieldsComponent, FilterResultsComponent], + providers: [BsDropdownConfig, TooltipConfig] +}) +export class FilterModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.ts new file mode 100644 index 000000000..000bab0d9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/filter.ts @@ -0,0 +1,22 @@ +import { FilterField } from './filter-field'; +import { FilterQuery } from './filter-query'; + +/** + * An object containing filter properties + */ +export class Filter { + /** + * A filterable field, used to select categories of filters + */ + field!: FilterField; + + /** + * A filterable query, if applicable + */ + query?: FilterQuery; + + /** + * Filter value + */ + value!: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/index.ts new file mode 100644 index 000000000..cbb6a9926 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/filter/index.ts @@ -0,0 +1,10 @@ +export { Filter } from './filter'; +export { FilterComponent } from './filter.component'; +export { FilterConfig } from './filter-config'; +export { FilterEvent } from './filter-event'; +export { FilterField } from './filter-field'; +export { FilterFieldsComponent } from './filter-fields.component'; +export { FilterModule } from './filter.module'; +export { FilterResultsComponent } from './filter-results.component'; +export { FilterQuery } from './filter-query'; +export { FilterType } from './filter-type'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/index.ts new file mode 100644 index 000000000..0d10109dd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/index.ts @@ -0,0 +1,4 @@ +export { ListComponent } from './list.component'; +export { ListConfig } from './list-config'; +//export { ListExpandToggleComponent } from './list-expand-toggle.component'; +export { ListModule } from './list.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list-config.ts new file mode 100644 index 000000000..af8c18d3e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list-config.ts @@ -0,0 +1,21 @@ +import { ListConfigBase } from '../list-config-base'; + +/** + * A config containing properties for list view + */ +export class ListConfig extends ListConfigBase { + /** + * Set to true to hide the close button in the expansion area. Default is false + */ + hideClose?: boolean; + + /** + * Allow expansion for each list item + */ + useExpandItems?: boolean; + + /** + * Show pinned items + */ + usePinItems?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.html new file mode 100644 index 000000000..49e5552da --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.html @@ -0,0 +1,111 @@ +
+ +
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+ + +
+ +
+ + +
+
+
+
+ +
+
+ +
+
+
+ + + +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+
+
+
+ +
+ +
+
+
+
+
+ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.less new file mode 100644 index 000000000..6c89b3de4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.less @@ -0,0 +1,122 @@ +// Checkbox placeholder for heading +.pfng-list-cb-placeholder { + width: 12px; +} + +// Container for item heading +.pfng-list-heading { + display: flex; + flex-grow: 1; +} + +// Row expansion toggle placeholder for heading and hidden toggles +.pfng-list-expand-placeholder { + width: 8px; +} + +.pfng-list-expand { + .fa-angle-right { + padding-left: 5px; + &.fa-angle-down { + padding-left: 0; + } + } +} + +.list-pf-container { + // Add hover style and modify cursor + .list-pf-chevron { + cursor: pointer; + &:hover { + color: @color-pf-blue-400; + } + } +} + +// For displaying close button in expansion area +.pfng-list-expansion { + position: relative; + .list-pf-content { + flex-grow: 1; + } +} + +// For displaying a heading above the list +.pfng-list-heading { + // Hide heading for small screens + @media (max-width: 992px) { + display: none; + } + // Heading should not be clickable + pointer-events: none; + + // Heading should not highlight on mouse hover + &:hover { + background-color: @color-pf-white; + } + + // Allow info icons to generate events + i { + pointer-events: auto; + } + + // Override top border for heading + &.list-pf-item { + border-top: none; + } + + // Override font for normal heading text + .list-pf-title { + font-size: inherit; + font-weight: normal; + } + + // Remove the divider line for heading + .list-pf-chevron, .list-pf-select { + + .list-pf-content { + border-left: none; + } + } +} + +// Pin +.pfng-list-pin { + align-items: center; + align-self: stretch; + background-color: @color-pf-black-150; + box-shadow: -3px 1px 4px 0 @color-pf-black-200 inset; + display: flex; + margin-bottom: -20px; + margin-left: -20px; + margin-right: 20px; + margin-top: -20px; + padding-bottom: 20px; + padding-left: 5px; + padding-right: 5px; + padding-top: 20px; + &.multi-ctrls { + margin-right: 10px; + } + a { + color: @color-pf-black; + opacity: .7; + &:hover { + opacity: 1; + } + } +} + +// Pin constainer to hide/show feature +.pfng-list-pin-container { + align-self: stretch; + display: flex; +} + +// Pin placeholder for heading and non-pinned items +.pfng-list-pin-placeholder { + margin-left: -20px; + width: 39px; + &.multi-ctrls { + width: 28px; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.ts new file mode 100644 index 000000000..712e7219f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.component.ts @@ -0,0 +1,218 @@ +import { + Component, + DoCheck, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + TemplateRef, + TrackByFunction, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { cloneDeep, defaults, isEqual, uniqueId } from 'lodash-es'; + +import { ListBase } from '../list-base'; +import { ListConfig } from './list-config'; +import { ListEvent } from '../list-event'; +import { EmptyStateModule } from '../../empty-state'; +import { SortArrayPipe } from '../../pipe'; + +/** + * List component + * + * For items, use a template named itemTemplate to contain content for each item. For each item in the items array, the + * expansion can be disabled by setting disabled to true on the item. If using actions, use a template named + * actionTemplate to contain expandable content for the actions of each item. If using expand items, use a template + * named itemExpandedTemplate to contain expandable content for each item. + * + * Cannot use both multi-select and double click selection at the same time + * Cannot use both checkbox and click selection at the same time + * + * Unique IDs are generated for each list item, which can be overridden by providing an id for the pfng-list tag. + * + * Usage: + *
+ * // Individual module import
+ * import { ListModule } from 'patternfly-ng/list';
+ * // Or
+ * import { ListModule } from 'patternfly-ng';
+ *
+ * // NGX Bootstrap
+ * import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown';
+ * import { TooltipConfig, TooltipModule } from 'ngx-bootstrap/tooltip';
+ *
+ * @NgModule({
+ *   imports: [ListModule, BsDropdownModule.forRoot(), TooltipModule.forRoot(),...],
+ *   providers: [BsDropdownConfig, TooltipConfig]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { ListConfig, ListEvent } from 'patternfly-ng/list';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-list', + templateUrl: './list.component.html', + imports: [ + CommonModule, + EmptyStateModule, + FormsModule, + SortArrayPipe, + ] +}) +export class ListComponent extends ListBase implements DoCheck, OnInit { + /** + * The name of the template containing action heading layout + */ + @Input() actionHeadingTemplate?: TemplateRef; + + /** + * The list config containing component properties + */ + @Input() config!: ListConfig; + + /** + * The name of the template used to contain expandable content for each item + */ + @Input() expandTemplate!: TemplateRef; + + /** + * The name of the template containing item heading layout + */ + @Input() itemHeadingTemplate?: TemplateRef; + + /** + * The function to pass to the underlying ngFor trackBy property + */ + @Input() trackBy!: TrackByFunction; + + /** + * The event emitted when an item pin has been changed + */ + @Output('onPinChange') onPinChange = new EventEmitter(); + + private defaultConfig = { + dblClick: false, + hideClose: false, + multiSelect: false, + selectedItems: [], + selectionMatchProp: 'uuid', + selectItems: false, + showCheckbox: false, + showRadioButton: false, + useExpandItems: false + } as ListConfig; + private id: string = uniqueId('pfng-list'); + private prevConfig!: ListConfig; + + /** + * The default constructor + */ + constructor(private el: ElementRef) { + super(); + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected override setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + super.setupConfig(); + this.prevConfig = cloneDeep(this.config); + } + + /** + * Return component config + * + * @returns {} ListConfig The component config + */ + protected getConfig(): ListConfig { + return this.config; + } + + /** + * Return an ID for the given element prefix and index (e.g., 'pfng-list1-item0') + * + * Note: The ID prefix can be overridden by providing an id for the pfng-list tag. + * + * @param {string} suffix The element suffix (e.g., 'item') + * @param {number} index The current item index + * @returns {string} + */ + protected getId(suffix: string, index: number): string { + let result = this.id; + if (this.el.nativeElement.id !== undefined && this.el.nativeElement.id.length > 0) { + result = this.el.nativeElement.id; + } + return result + '-' + suffix + index; + } + + // Toggle + + protected closeExpandItem(item: any): void { + item.expandId = undefined; + item.expanded = false; + } + + /** + * Toggle expand item open/close + * + * @param {MouseEvent} $event The event emitted when an item has been clicked + * @param {Object} item The object associated with the current row + */ + protected toggleExpandItem($event: MouseEvent, item: any): void { + // Do nothing if item expansion is disabled + if (!this.config.useExpandItems) { + return; + } + // Do not trigger for child items, only on the DOM element to which the event is attached + if ($event.target !== $event.currentTarget) { + return; + } + // Item may already be open due to compound expansion + if (item.expanded && item.expandId !== undefined) { + item.expandId = undefined; + return; + } + item.expandId = undefined; + item.expanded = !item.expanded; + } + + protected togglePin($event: MouseEvent, item: any): void { + item.showPin = (item.showPin === undefined) ? true : !item.showPin; + this.onPinChange.emit({ + item: item + } as ListEvent); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.module.ts new file mode 100644 index 000000000..92d6673de --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/basic-list/list.module.ts @@ -0,0 +1,28 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { EmptyStateModule } from '../../empty-state/empty-state.module'; +import { ListComponent } from './list.component'; +//import { ListExpandToggleComponent } from './list-expand-toggle.component'; +import { SortArrayPipeModule } from '../../pipe/sort-array/sort-array.pipe.module'; + +/** + * A module containing objects associated with basic list components + */ +@NgModule({ + imports: [ + CommonModule, + EmptyStateModule, + FormsModule, + ListComponent, + //ListExpandToggleComponent, + SortArrayPipeModule + ], + //declarations: [ListComponent, ListExpandToggleComponent], + exports: [ + ListComponent, + //ListExpandToggleComponent + ] +}) +export class ListModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/index.ts new file mode 100644 index 000000000..2b867d9ef --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/index.ts @@ -0,0 +1,6 @@ +export { ListBase } from './list-base'; +export { ListConfigBase } from './list-config-base'; +export { ListEvent } from './list-event'; + +export * from './basic-list/index'; +// export * from './tree-list/index'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-base.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-base.ts new file mode 100644 index 000000000..3ab229487 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-base.ts @@ -0,0 +1,306 @@ +import { + EventEmitter, + Input, + Output, + TemplateRef, +} from '@angular/core'; + +import { Action } from '../action/action'; +import { ListConfigBase } from './list-config-base'; +import { ListEvent } from './list-event'; + +/** + * List base + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'pfng-list-base', + template: '' +}) +export abstract class ListBase { + /** + * The name of the template containing actions for each item + */ + @Input() actionTemplate!: TemplateRef; + + /** + * An array of items to display in the list + */ + @Input() items!: any[]; + + /** + * The name of the template containing item layout + */ + @Input() itemTemplate!: TemplateRef; + + /** + * The event emitted when an action (e.g., button, kebab, etc.) has been selected + */ + @Output('onActionSelect') onActionSelect = new EventEmitter(); + + /** + * The event emitted when an item has been clicked + */ + @Output('onClick') onClick = new EventEmitter(); + + /** + * The event emitted when an item is double clicked + */ + @Output('onDblClick') onDblClick = new EventEmitter(); + + /** + * The event emitted when an item selection has been changed + */ + @Output('onSelectionChange') onSelectionChange = new EventEmitter(); + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Set up default config + */ + protected setupConfig(): void { + let config = this.getConfig(); + if (config.multiSelect === undefined || config.multiSelect === false) { + let selectedItems = this.getSelectedItems(this.items); + if (selectedItems.length > 0) { + this.selectSingleItem(selectedItems[0]); + } + } + if (config.multiSelect && config.dblClick) { + throw new Error('ListComponent - Illegal use: ' + + 'Cannot use both multi-select and double click selection at the same time.'); + } + if (config.selectItems && config.showCheckbox) { + throw new Error('ListComponent - Illegal use: ' + + 'Cannot use both checkbox and click selection at the same time.'); + } + if (config.selectItems && config.showRadioButton) { + throw new Error('ListComponent - Illegal use: ' + + 'Cannot use both radio button and single row selection at the same time.'); + } + if (config.showRadioButton && config.showCheckbox) { + throw new Error('ListComponent - Illegal use: ' + + 'Cannot use both radio button and checkbox at the same time.'); + } + } + + /** + * Return component config + * + * @returns {ListConfigBase} The component config + */ + protected abstract getConfig(): ListConfigBase; + + // Accessors + + get item(): any { + return this.items[0]; + } + + /** + * Get the flag indicating list has no items + * + * @returns {boolean} The flag indicating list has no items + */ + get itemsEmpty(): boolean { + return !(this.items !== undefined && this.items.length > 0); + } + + // Actions + + /** + * Helper to generate action select event + * + * @param {Action} action The selected action + */ + protected handleAction(action: Action): void { + if (action && action.disabled !== true) { + this.onActionSelect.emit(action); + } + } + + // Selection + + /** + * Helper to generate selection change event + * + * @param item The selected item + */ + protected checkboxChange(item: any): void { + this.onSelectionChange.emit({ + item: item, + selectedItems: this.getSelectedItems(this.items) + } as ListEvent); + } + + /** + * Helper to generate double click event + * + * @param {MouseEvent} $event The triggered event + * @param item The double clicked item + */ + protected dblClick($event: MouseEvent, item: any): void { + let config = this.getConfig(); + if (config.dblClick === true) { + this.onDblClick.emit({ + item: item + } as ListEvent); + } + } + + /** + * Helper to deselect given items items and children + * + * @param {any[]} items The items to be deselected + */ + protected deselectItems(items: any[]): void { + if (items !== undefined) { + for (let i = 0; i < items.length; i++) { + items[i].selected = false; + if (Array.isArray(items[i].children)) { + this.deselectItems(items[i].children); + } + } + } + } + + /** + * Helper to retrieve selected items + * + * @param {any[]} items The items containing possible selections + * @returns {any[]} A list of selected items + */ + protected getSelectedItems(items: any[]): any[] { + let selectedItems = []; + if (items !== undefined) { + for (let i = 0; i < items.length; i++) { + if (items[i].selected) { + selectedItems.push(items[i]); + } + if (Array.isArray(items[i].children)) { + let selectedChildren = this.getSelectedItems(items[i].children); + selectedItems = selectedItems.concat(selectedChildren); + } + } + } + return selectedItems; + } + + /** + * Helper to generate selection change event + * + * @param item The selected item + */ + protected radioButtonChange(item: any): void { + let selected = item.selected; + + this.deselectItems(this.items); + if (!selected) { + this.selectSingleItem(item); + } + + this.onSelectionChange.emit({ + item: item, + selectedItems: this.getSelectedItems(this.items) + } as ListEvent); + } + + /** + * Helper to select a single item and deselect all others + * + * @param item The item to select + */ + protected selectSingleItem(item: any): void { + this.deselectItems(this.items); + item.selected = true; + } + + /** + * Select or deselect an item + * + * @param item The item to select or deselect + * @param {boolean} selected True if item should be selected + */ + selectItem(item: any, selected: boolean): void { + let config = this.getConfig(); + + // Are we using checkboxes or radiobuttons? + if (config.showCheckbox) { + item.selected = selected; + return; + } + if (config.showRadioButton) { + this.deselectItems(this.items); + this.selectSingleItem(item); + return; + } + + // Multiple item selection + if (config.multiSelect && !config.dblClick) { + item.selected = selected; + } else { + // Single item selection + this.deselectItems(this.items); + this.selectSingleItem(item); + } + } + + /** + * Helper to toggle item selection + * + * @param {MouseEvent} $event The triggered event + * @param item The item to select + */ + protected toggleSelection($event: MouseEvent, item: any): void { + let config = this.getConfig(); + let selectionChanged = false; + + // Always emit click event + this.onClick.emit({ + item: item + } as ListEvent); + + // Go no further if click selection isn't enabled + if (!config.selectItems) { + return; + } + + // Multiple item selection + if (config.multiSelect && !config.dblClick) { + // Item's 'selected' prop may be undefined initially + if (item.selected === true) { + item.selected = false; + } else { + item.selected = true; + } + selectionChanged = true; + } else { + // Single item selection + if (item.selected === true) { + // Avoid accidentally deselecting by dblClick + if (!config.dblClick) { + this.deselectItems(this.items); + selectionChanged = true; + } + } else { + this.selectSingleItem(item); + selectionChanged = true; + } + } + + // Emit event only if selection changed + if (selectionChanged === true) { + this.onSelectionChange.emit({ + item: item, + selectedItems: this.getSelectedItems(this.items) + } as ListEvent); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-config-base.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-config-base.ts new file mode 100644 index 000000000..ad0baeae7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-config-base.ts @@ -0,0 +1,38 @@ +import { EmptyStateConfig } from '../empty-state/empty-state-config'; + +/** + * A config containing properties for tree list + */ +export class ListConfigBase { + /** + * Handle double clicking (item remains selected on a double click). Default is false + */ + dblClick?: boolean; + + /** + * A config containing properties for empty state when no items are available + */ + emptyStateConfig?: EmptyStateConfig; + + /** + * Allow multiple item selections + * + * Not applicable when dblClick is true. Default is false + */ + multiSelect?: boolean; + + /** + * Allow row item selection. Default is false + */ + selectItems?: boolean; + + /** + * Show checkbox for selecting items. Default is false + */ + showCheckbox?: boolean; + + /** + * Show radio button for selecting items. Default is false + */ + showRadioButton?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-event.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-event.ts new file mode 100644 index 000000000..972929777 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/list/list-event.ts @@ -0,0 +1,14 @@ +/** + * An object containing properties for list events + */ +export class ListEvent { + /** + * The object associated with the current row + */ + item: any; + + /** + * The currently selected items, if applicable + */ + selectedItems?: any[]; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal-config.ts new file mode 100644 index 000000000..85b6ccc80 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal-config.ts @@ -0,0 +1,43 @@ +/** + * A config containing properties for about modal + */ +export class AboutModalConfig { + /** + * Text explaining the version or copyright + */ + additionalInfo?: string; + + /** + * Product copyright information + */ + copyright?: string; + + /** + * The alt text for the corner graphic + */ + logoImageAlt?: string; + + /** + * The source for the corner graphic + */ + logoImageSrc?: string; + + /** + * data for the modal: + * .product - the product label + * .version - the product version + */ + productInfo?: ProductInfo[]; + + /** + * The product title for the modal + */ + title?: string; +} + +export type ProductInfo = { + name: string; + value: string; +} + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal-event.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal-event.ts new file mode 100644 index 000000000..9840aa829 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal-event.ts @@ -0,0 +1,9 @@ +/** + * An object containing events for about modal events + */ +export class AboutModalEvent { + /** + * Flag indicating Modal is open + */ + close?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.component.html new file mode 100644 index 000000000..d080d25e5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.component.html @@ -0,0 +1,25 @@ +
+ + + +
+ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.component.ts new file mode 100644 index 000000000..9ace6be03 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.component.ts @@ -0,0 +1,109 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { cloneDeep, defaults, isEqual } from 'lodash-es'; + +import { AboutModalConfig } from './about-modal-config'; +import { AboutModalEvent } from './about-modal-event'; + +/** + * About Modal component + * + * Usage: + *
+ * // Individual module import
+ * import { AboutModalModule } from 'patternfly-ng/modal';
+ * // Or
+ * import { AboutModalModule } from 'patternfly-ng';
+ *
+ * // NGX Bootstrap
+ * import { ModalModule } from 'ngx-bootstrap/modal';
+ *
+ * @NgModule({
+ *   imports: [AboutModalModule, ModalModule.forRoot(),...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { AboutModalConfig, AboutModalEvent } from 'patternfly-ng/modal';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-about-modal', + templateUrl: './about-modal.component.html', + imports: [ + CommonModule + ] +}) +export class AboutModalComponent implements DoCheck, OnInit { + /** + * The AboutModal config contaning component properties + */ + @Input() config!: AboutModalConfig; + + /** + * The Event is emitted when modal is closed + */ + @Output('onCancel') onCancel = new EventEmitter(); + + private defaultConfig = {} as AboutModalConfig; + private prevConfig!: AboutModalConfig; + + /** + * The default contructor + */ + constructor() {} + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Setup default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + this.prevConfig = cloneDeep(this.config); + } + + /** + * Close the Modal + * @param $event MouseEvent to emit + */ + close(): void { + this.onCancel.emit({ + close: true + } as AboutModalEvent); + } +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.module.ts new file mode 100644 index 000000000..75c639922 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/about-modal.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { AboutModalComponent } from './about-modal.component'; + +@NgModule({ + imports: [ + CommonModule, + AboutModalComponent + ], + //declarations: [AboutModalComponent], + exports: [AboutModalComponent] +}) +export class AboutModalModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/index.ts new file mode 100644 index 000000000..047e705e7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/about-modal/index.ts @@ -0,0 +1,4 @@ +export { AboutModalConfig } from './about-modal-config'; +export { AboutModalComponent } from './about-modal.component'; +export { AboutModalEvent } from './about-modal-event'; +export { AboutModalModule } from './about-modal.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/index.ts new file mode 100644 index 000000000..cdbe44457 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/modal/index.ts @@ -0,0 +1 @@ +export * from './about-modal/index'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/index.ts new file mode 100644 index 000000000..364b944d3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/index.ts @@ -0,0 +1,10 @@ +export { Notification } from './notification'; +export { NotificationEvent } from './notification-event'; +//export { NotificationGroup, NotificaitonGroup } from './notification-group'; +export { NotificationType } from './notification-type'; + +export * from './inline-notification/index'; +//export * from './notification-drawer/index'; +export * from './notification-service/index'; +export * from './toast-notification/index'; +export * from './toast-notification-list/index'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/index.ts new file mode 100644 index 000000000..7ae2a6fd7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/index.ts @@ -0,0 +1,2 @@ +export { InlineNotificationComponent } from './inline-notification.component'; +export { InlineNotificationModule } from './inline-notification.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.component.html new file mode 100644 index 000000000..3c992bb1a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.component.html @@ -0,0 +1,14 @@ +
+ + + + + + + {{header}} {{message}} +
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.component.ts new file mode 100644 index 000000000..ad64d7a02 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.component.ts @@ -0,0 +1,92 @@ +import { + Component, + EventEmitter, + Input, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { NotificationType } from '../notification-type'; + +/** + * Inline notifications can be used to provide notifications to user that can persist on the page + * they are also optionally dismissable by the user + * + * Usage: + *
+ * // Individual module import
+ * import { InlineNotificationModule } from 'patternfly-ng/notification';
+ * // Or
+ * import { InlineNotificationModule } from 'patternfly-ng';
+ *
+ * // NGX Bootstrap
+ * import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown';
+ *
+ * @NgModule({
+ *   imports: [InlineNotificationModule, BsDropdownModule.forRoot(),...],
+ *   providers: [BsDropdownConfig]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { NotificationType } from 'patternfly-ng/notification';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-inline-notification', + templateUrl: './inline-notification.component.html', + imports: [ + CommonModule, + ] +}) +export class InlineNotificationComponent { + /** + * The notification type (e.g., NotificationType.SUCCESS, NotificationType.INFO, etc.) + */ + @Input() type!: NotificationType; + + /** + * The message to display within the notification + */ + @Input() message!: string; + + /** + * The notification header + */ + @Input() header!: string; + + /** + * Boolean to indicate whether or not notification can be dismissed + */ + @Input() dismissable?: boolean; + + /** + * Indicates whether or not the notification is currently hidden + */ + @Input() hidden: boolean = false; + + /** + * The event emitted when the mouse hovers over and leaves a notification + */ + @Output('hiddenChange') hiddenChange = new EventEmitter(); + + + /** + * The default constructor + */ + constructor() { + } + + /** + * Function called from the view when the notification is removed + */ + public notificationRemove(): void { + this.hidden = true; + this.hiddenChange.emit(this.hidden); + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.module.ts new file mode 100644 index 000000000..eb67e3729 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/inline-notification/inline-notification.module.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { InlineNotificationComponent } from './inline-notification.component'; + +/** + * A module containing objects associated with inline notifications + */ +@NgModule({ + imports: [ + CommonModule, + FormsModule, + InlineNotificationComponent + ], + //declarations: [InlineNotificationComponent], + exports: [ + InlineNotificationComponent + ] +}) +export class InlineNotificationModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-event.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-event.ts new file mode 100644 index 000000000..7b5ebf54b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-event.ts @@ -0,0 +1,22 @@ +import { Action } from '../action/action'; +import { Notification } from './notification'; + +/** + * An object containing properties for notification events + */ +export class NotificationEvent { + /** + * Configuration properties for notification actions + */ + action?: Action; + + /** + * Configuration properties for a notification message + */ + notification!: Notification; + + /** + * Flag indicating user is actively viewing notification + */ + isViewing?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-service/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-service/index.ts new file mode 100644 index 000000000..cf3ca996b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-service/index.ts @@ -0,0 +1 @@ +export { NotificationService } from './notification.service'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-service/notification.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-service/notification.service.ts new file mode 100644 index 000000000..6303c9275 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-service/notification.service.ts @@ -0,0 +1,205 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; + +import { Action } from '../../action/action'; +import { Notification } from '../notification'; +import { NotificationType } from '../notification-type'; + +/** + * Notification service used to notify user about important events in the application. + * + * You may configure the service with: setDelay, setVerbose and setPersist. + * + * Usage: + *
+ * // Individual module import
+ * import { NotificationServiceModule } from 'patternfly-ng/notification';
+ * // Or
+ * import { NotificationServiceModule } from 'patternfly-ng';
+ * 
+ * + * Optional: + *
+ * import { Notification, NotificationType } from 'patternfly-ng/notification';
+ * 
+ */ +@Injectable() +export class NotificationService { + + // time (in ms) the notifications are shown + private delay: number = 8000; + private modes: any = {}; + private notifications: any = {}; + private persist: any = { 'error': true, 'httpError': true }; + private verbose: boolean = false; + private _notificationsSubject: Subject = new Subject(); + + /** + * The default constructor + */ + constructor() { + this.notifications.data = [] as Notification[]; + this.modes = [ + { info: { type: NotificationType.INFO, header: 'Info!', log: 'info' } }, + { success: { type: NotificationType.SUCCESS, header: 'Success!', log: 'info' } }, + { error: { type: NotificationType.DANGER, header: 'Error!', log: 'error' } }, + { warn: { type: NotificationType.WARNING, header: 'Warning!', log: 'warn' } } + ]; + this.modes.forEach((mode: any, index: number) => { + this.notifications[index] = this.createNotifyMethod(index); + }); + } + + /** + * Get all notifications + */ + getNotifications(): Notification[] { + return this.notifications.data; + } + + /** + * Allows for interacting with a stream of notifications + */ + get getNotificationsObserver(): Observable { + return this._notificationsSubject.asObservable(); + } + + /** + * Generate a notification for the given HTTP Response + * + * @param message The notification message + * @param httpResponse The HTTP Response + */ + httpError(message: string, httpResponse: any): void { + message += ' (' + (httpResponse.data.message || httpResponse.data.cause + || httpResponse.data.cause || httpResponse.data.errorMessage) + ')'; + this.message('danger', 'Error!', message, this.persist.httpError, undefined, undefined); + if (this.verbose) { + console.log(message); + } + } + + /** + * Generate a notification message + * + * @param type The notification type + * @param header The notification header + * @param message The notification message + * @param isPersistent True if the notification should be persistent + * @param primaryAction The primary action for the notifiaction + * @param moreActions More actions for the kebab + */ + message( + type: string, + header: string, + message: string, + isPersistent: boolean, + primaryAction?: Action, + moreActions?: Action[]): void { + let notification = { + header: header, + isPersistent: isPersistent, + isViewing: false, + message: message, + moreActions: moreActions, + primaryAction: primaryAction, + showClose: false, + type: type, + visible: true + } as Notification; + + this.notifications.data.push(notification); + this.updateNotificationsStream(); + + if (notification.isPersistent !== true) { + notification.isViewing = false; + setTimeout(() => { + notification.visible = false; + if (!notification.isViewing) { + this.remove(notification); + } + }, this.delay); + } + } + + /** + * Remove notification + * + * @param notification The notification to remove + */ + remove(notification: Notification): void { + let index = this.notifications.data.indexOf(notification); + if (index !== -1) { + this.removeIndex(index); + this.updateNotificationsStream(); + } + } + + /** + * Set the delay after which the notification is dismissed. The argument of this method expects miliseconds. Default + * delay is 8000 ms. + * + * @param delay The delay in ms + */ + setDelay(delay: number): void { + this.delay = delay; + } + + /** + * Sets persist option for particular modes. Notification with persistent mode won't be dismissed after delay, but has + * to be closed manually with the close button. By default, the "error" and "httpError" modes are set to persistent. + * + * @param persist Set to true to persist notifications + */ + setPersist(persist: boolean): void { + this.persist = persist; + } + + /** + * Set the verbose mode to on (default) or off. During the verbose mode, each notification is printed in the console. + * + * @param verbose Set to true for verbose mode + */ + setVerbose(verbose: boolean): void { + this.verbose = verbose; + } + + /** + * Set a flag indicating user is viewing the given notification + * + * @param notification The notification currently being viewed + * @param isViewing True if the notification is being viewed + */ + setViewing(notification: Notification, isViewing: boolean): void { + notification.isViewing = isViewing; + if (isViewing !== true && notification.visible !== true) { + this.remove(notification); + } + } + + // Private + + private createNotifyMethod(index: number): any { + return (message: string, header: string, persistent: boolean, primaryAction: Action, moreActions: Action[]) => { + if (header !== undefined) { + header = this.modes[index].header; + } + if (persistent !== undefined) { + persistent = this.persist[index]; + } + this.notifications.message(this.modes[index].type, header, message, persistent, primaryAction, moreActions); + if (this.verbose) { + console.log(message); + } + }; + } + + private removeIndex(index: number): void { + this.notifications.data.splice(index, 1); + } + + private updateNotificationsStream(): void { + this._notificationsSubject.next(this.getNotifications()); + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-type.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-type.ts new file mode 100644 index 000000000..18311ac63 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification-type.ts @@ -0,0 +1,24 @@ +/* + * An object containing properties for a notification type + */ +export class NotificationType { + /** + * Danger notification type + */ + static readonly DANGER: string = 'danger'; + + /** + * Information notification type + */ + static readonly INFO: string = 'info'; + + /** + * Success notification type + */ + static readonly SUCCESS: string = 'success'; + + /** + * Warning notification type + */ + static readonly WARNING: string = 'warning'; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification.ts new file mode 100644 index 000000000..cbabaf522 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/notification.ts @@ -0,0 +1,56 @@ +import { Action } from '../action/action'; + +/** + * An object containing properties for notification messages + */ +export class Notification { + /** + * The header to display for the notification + */ + header?: string; + + /** + * Flag to show close button for the notification even if showClose is false + */ + isPersistent?: boolean; + + /** + * Flag indicating user is actively viewing notification + */ + isViewing?: boolean; + + /** + * The main text message of the notification + */ + message!: string; + + /** + * More actions to show in a kebab menu + */ + moreActions?: Action[]; + + /** + * The primary action for the notification + */ + primaryAction?: Action; + + /** + * Flag to show the close button on all notifications (not shown with menu actions) + */ + showClose?: boolean; + + /** + * The type of the notification message (e.g., 'success', 'info', 'danger', 'warning') + */ + type!: string; + + /** + * Flag indicating notification should be visible + */ + visible?: boolean; + + /** + * Only for notification drawer module, to dislay time stamp + */ + timeStamp?: number; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/index.ts new file mode 100644 index 000000000..82337ab60 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/index.ts @@ -0,0 +1,2 @@ +export { ToastNotificationListComponent } from './toast-notification-list.component'; +export { ToastNotificationListModule } from './toast-notification-list.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.component.html new file mode 100644 index 000000000..4e68a6b12 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.component.html @@ -0,0 +1,16 @@ +
+
+ + +
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.component.ts new file mode 100644 index 000000000..0ff6b15c1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.component.ts @@ -0,0 +1,105 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { Notification } from '../notification'; +import { NotificationEvent } from '../notification-event'; +import { ToastNotificationComponent } from '../toast-notification/toast-notification.component'; + +/** + * Component to display a list of toast notifications + * + * Usage: + *
+ * // Individual module import
+ * import { ToastNotificationListModule } from 'patternfly-ng/notification';
+ * // Or
+ * import { ToastNotificationListModule } from 'patternfly-ng';
+ *
+ * // NGX Bootstrap
+ * import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown';
+ *
+ * @NgModule({
+ *   imports: [ToastNotificationListModule, BsDropdownModule.forRoot(),...],
+ *   providers: [BsDropdownConfig]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { Notification, NotificationEvent, NotificationType } from 'patternfly-ng/notification';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-toast-notification-list', + templateUrl: './toast-notification-list.component.html', + imports: [ + CommonModule, + ToastNotificationComponent + ] +}) +export class ToastNotificationListComponent implements OnInit { + /** + * A list of notifiactions to display + */ + @Input() notifications!: Notification[]; + + /** + * Set to true to show close button + */ + @Input() showClose!: boolean; + + /** + * The event emitted when an action has been selected + */ + @Output('onActionSelect') onActionSelect = new EventEmitter(); + + /** + * The event emitted when the close button has been selected + */ + @Output('onCloseSelect') onCloseSelect = new EventEmitter(); + + /** + * The event emitted when the mouse hovers over and leaves a notification + */ + @Output('onViewingChange') onViewingChange = new EventEmitter(); + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + } + + // Actions + + /** + * Check if the component config has changed + */ + protected handleAction($event: NotificationEvent): void { + this.onActionSelect.emit($event); + } + + protected handleClose($event: NotificationEvent): void { + this.onCloseSelect.emit($event); + } + + protected handleViewingChange($event: NotificationEvent) { + this.onViewingChange.emit($event); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.module.ts new file mode 100644 index 000000000..bba484872 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification-list/toast-notification-list.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { ToastNotificationListComponent } from './toast-notification-list.component'; +import { ToastNotificationModule } from '../toast-notification'; + +/** + * A module containing objects associated with toast notification lists + */ +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ToastNotificationListComponent, + ToastNotificationModule + ], + //declarations: [ToastNotificationListComponent], + exports: [ + ToastNotificationListComponent + ] +}) +export class ToastNotificationListModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/index.ts new file mode 100644 index 000000000..13e03ca77 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/index.ts @@ -0,0 +1,2 @@ +export { ToastNotificationComponent } from './toast-notification.component'; +export { ToastNotificationModule } from './toast-notification.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.html new file mode 100644 index 000000000..3c2b1fad3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.html @@ -0,0 +1,56 @@ +
+ + + + + + + + + {{header}} {{message}} + + + {{message}} + +
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.less new file mode 100644 index 000000000..41a8c4da8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.less @@ -0,0 +1,5 @@ +.toast-pf-action > a { cursor: pointer; } + +.toast-pf { + .dropdown-menu > li > a { cursor: pointer; } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.ts new file mode 100644 index 000000000..3e25a6bbf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.component.ts @@ -0,0 +1,171 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { Action } from '../../action/action'; +import { Notification } from '../notification'; +import { NotificationEvent } from '../notification-event'; + +/** + * Toast notifications are used to notify users of a system occurrence. Toast notifications should be transient and stay + * on the screen for 8 seconds, so that they do not block the information behind them for too long, but allows the user + * to read the message. The ToastNotification component allows status, header, message, primary action and menu actions + * for the notification. The notification can also allow the user to close the notification. + * + * Note: Using the kebab menu (more actions) with the close button is not currently supported. If both are specified the + * close button will not be shown. Add a close menu item if you want to have both capabilities. + * + * Usage: + *
+ * // Individual module import
+ * import { ToastNotificationModule } from 'patternfly-ng/notification';
+ * // Or
+ * import { ToastNotificationModule } from 'patternfly-ng';
+ *
+ * // NGX Bootstrap
+ * import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown';
+ *
+ * @NgModule({
+ *   imports: [ToastNotificationModule, BsDropdownModule.forRoot(),...],
+ *   providers: [BsDropdownConfig]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { Notification, NotificationEvent, NotificationType } from 'patternfly-ng/notification';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-toast-notification', + templateUrl: './toast-notification.component.html', + imports: [ + CommonModule, + ] +}) +export class ToastNotificationComponent implements DoCheck, OnInit { + /** + * The notification header + */ + @Input() header?: string; + + /** + * The notification message + */ + @Input() message!: string; + + /** + * The notification kebab actions + */ + @Input() moreActions?: Action[]; + + /** + * An object containing notifications properties + */ + @Input() notification!: Notification; + + /** + * The primary action + */ + @Input() primaryAction?: Action; + + /** + * Set to true to show close button + */ + @Input() showClose?: boolean; + + /** + * The notification type (e.g., NotificationType.SUCCESS, NotificationType.INFO, etc.) + */ + @Input() type!: string; + + /** + * The event emitted when an action has been selected + */ + @Output('onActionSelect') onActionSelect = new EventEmitter(); + + /** + * The event emitted when the close button has been selected + */ + @Output('onCloseSelect') onCloseSelect = new EventEmitter(); + + /** + * The event emitted when the mouse hovers over and leaves a notification + */ + @Output('onViewingChange') onViewingChange = new EventEmitter(); + + private _showCloseButton: boolean = false; + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + this._showCloseButton = (this.showClose === true) + && (this.moreActions === undefined || this.moreActions === null || this.moreActions.length === 0); + } + + // Accessors + + /** + * Get the flag indicating that the close button should be shown + * + * @returns {FilterField} The flag indicating that the close button should be shown + */ + get showCloseButton(): boolean { + return this._showCloseButton; + } + + // Actions + + handleEnter($event: MouseEvent): void { + this.onViewingChange.emit({ + notification: this.notification, + isViewing: true + } as NotificationEvent); + } + + handleLeave($event: MouseEvent): void { + this.onViewingChange.emit({ + notification: this.notification, + isViewing: false + } as NotificationEvent); + } + + // Private + + protected handleAction(action: Action): void { + if (action && action.disabled !== true) { + this.onActionSelect.emit({ + action: action, + notification: this.notification + } as NotificationEvent); + } + } + + protected handleClose($event: MouseEvent): void { + this.onCloseSelect.emit({ notification: this.notification } as NotificationEvent); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.module.ts new file mode 100644 index 000000000..65070d84b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/notification/toast-notification/toast-notification.module.ts @@ -0,0 +1,25 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { ToastNotificationComponent } from './toast-notification.component'; + +/** + * A module containing objects associated with toast notifications + */ +@NgModule({ + imports: [ + BsDropdownModule.forRoot(), + CommonModule, + FormsModule, + ToastNotificationComponent + ], + //declarations: [ToastNotificationComponent], + exports: [ToastNotificationComponent], + providers: [ + BsDropdownConfig + ] +}) +export class ToastNotificationModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/index.ts new file mode 100644 index 000000000..9d0ef9107 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/index.ts @@ -0,0 +1,4 @@ +export { PaginationComponent } from './pagination.component'; +export { PaginationConfig } from './pagination-config'; +export { PaginationEvent } from './pagination-event'; +export { PaginationModule } from './pagination.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination-config.ts new file mode 100644 index 000000000..f0885e9d8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination-config.ts @@ -0,0 +1,24 @@ +/** + * A config containing properties for Pagination + */ +export class PaginationConfig { + /** + * The current page number + */ + pageNumber?: number; + + /** + * The total number of items in the data set. + */ + totalItems!: number; + + /** + * Page size increments for the 'per page' dropdown + */ + pageSizeIncrements?: Array; + + /** + * The initial page size to use + */ + pageSize?: number; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination-event.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination-event.ts new file mode 100644 index 000000000..85bf91ed6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination-event.ts @@ -0,0 +1,14 @@ +/** + * An object containing properties for pagination events + */ +export class PaginationEvent { + /** + * The current page number + */ + pageNumber?: number; + + /** + * The initial page size to use + */ + pageSize?: number; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.component.html new file mode 100644 index 000000000..a0d677721 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.component.html @@ -0,0 +1,54 @@ +
+
+
+ +
+ per page +
+
+ + {{getCurrentPage()}} of  + {{config.totalItems}} + + + + of {{lastPageNumber}} + +
+
+ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.component.ts new file mode 100644 index 000000000..6fd4ef028 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.component.ts @@ -0,0 +1,274 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { cloneDeep, defaults, isEqual } from 'lodash-es'; + +import { PaginationConfig } from './pagination-config'; +import { PaginationEvent } from './pagination-event'; + +/** + * Component for rendering pagination + * + * Usage: + *
+ * // Individual module import
+ * import { PaginationModule } from 'patternfly-ng/pagination';
+ * // Or
+ * import { PaginationModule } from 'patternfly-ng';
+ *
+ * // NGX Bootstrap
+ * import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown';
+ *
+ * @NgModule({
+ *   imports: [PaginationModule, BsDropdownModule.forRoot(),...],
+ *   providers: [BsDropdownConfig]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { PaginationConfig, PaginationEvent } from 'patternfly-ng/pagination';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-pagination', + templateUrl: './pagination.component.html', + imports: [ + CommonModule, + FormsModule + ] +}) +export class PaginationComponent implements DoCheck, OnInit { + /** + * The Pagination config contaning component properties + */ + @Input() config!: PaginationConfig; + + /** + * The Event is emitted when Page Size is changed + */ + @Output('onPageSizeChange') onPageSizeChange = new EventEmitter(); + + /** + * The Event is emitted when Page Number is Changed + */ + @Output('onPageNumberChange') onPageNumberChange = new EventEmitter(); + + private defaultConfig = { + pageNumber: 1, + pageSizeIncrements: [5, 10, 20, 40, 80, 100], + pageSize: 5 + } as PaginationConfig; + private _pageNumber: number = 1; + private prevConfig!: PaginationConfig; + private _lastPageNumber: number = 1; + + /** + * The default constructor + */ + constructor() {} + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + this.lastPageNumber = this.getLastPageNumber(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Setup default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + this.lastPageNumber = this.getLastPageNumber(); + this.pageNumber = this.config.pageNumber ?? 1; + this.prevConfig = cloneDeep(this.config); + } + + // Accessors + + get pageNumber(): number { + return (this.config.totalItems !== undefined && this.config.totalItems > 0) ? this._pageNumber : 0; + } + + set pageNumber(pageNumber: number) { + this._pageNumber = pageNumber; + } + + /** + * Return last page number + */ + get lastPageNumber(): number { + return (this.config.totalItems !== undefined && this.config.totalItems > 0) ? this._lastPageNumber : 0; + } + + /** + * Update Last page Number + */ + set lastPageNumber(value: number) { + this._lastPageNumber = value; + } + + // Actions + + /** + * Jump to First Page + */ + gotoFirstPage(): void { + if (this.config.pageNumber !== 1) { + this.updatePageNumber(1); + } + } + + /** + * Go to Previous Page + */ + gotoPreviousPage(): void { + if (this.config.pageNumber !== 1) { + if (this.config && this.config.pageNumber !== undefined) { + this.updatePageNumber(this.config.pageNumber - 1); + } + } + } + + /** + * Go to Next Page + */ + gotoNextPage(): void { + if (this.config && this.config.pageNumber !== undefined && this.config.pageNumber < this.lastPageNumber) { + this.updatePageNumber(this.config.pageNumber + 1); + } + } + + /** + * Jump to Last Page + */ + gotoLastPage(): void { + if (this.config && this.config.pageNumber !== undefined && this.config.pageNumber < this.lastPageNumber) { + this.updatePageNumber(this.lastPageNumber); + } + } + + /** + * Return start index and end index of current page + */ + getCurrentPage() { + return this.getStartIndex() + ' - ' + this.getEndIndex(); + } + + /** + * Start Index of Current Page + */ + protected getStartIndex(): number { + return (this.config.totalItems !== undefined && this.config.totalItems > 0) + ? (this.config.pageSize ?? 5) * ((this.config.pageNumber ?? 1) - 1) + 1 : 0; + } + + /** + * End Index of Current Page + */ + protected getEndIndex(): number { + let numFullPages = Math.floor(this.config.totalItems / (this.config.pageSize ?? 5)); + let numItemsOnLastPage = this.config.totalItems - (numFullPages * (this.config.pageSize ?? 5)) || this.config.pageSize; + let numItemsOnPage = this.isLastPage() ? numItemsOnLastPage : (this.config.pageSize ?? 5); + return (this.config.totalItems !== undefined && this.config.totalItems > 0) + ? (this.getStartIndex() + (numItemsOnPage ?? 5) - 1) : 0; + } + + /** + * Page number is changed via input field's focus event + */ + onPageNumberBlur($event: FocusEvent) { + let newPageNumber: number = parseInt(String(this.pageNumber), 10); + if (isNaN(newPageNumber)) { + newPageNumber = this.pageNumber = this.config.pageNumber ?? 1; + } + + if (newPageNumber > this.lastPageNumber) { + this.updatePageNumber(this.lastPageNumber); + } else if (newPageNumber < 1) { + this.updatePageNumber(1); + } else { + this.updatePageNumber(newPageNumber); + } + } + + /** + * Page number is changed via input field's keyboard event + */ + onPageNumberKeyup($event: any): void { + let keycode = $event.keyCode ? $event.keyCode : $event.which; + if (keycode === 13) { + this.onPageNumberBlur(new FocusEvent('blur')); + } + } + + // Private + + /** + * Page size is changed + * @param newPageSize new page size + */ + protected onPageSizeUpdate($event: Event, newPageSize: number): void { + this.config.pageSize = newPageSize; + this.lastPageNumber = this.getLastPageNumber(); + this.gotoFirstPage(); + this.onPageSizeChange.emit({ + pageSize: newPageSize + } as PaginationEvent); + } + + /** + * Update the Page Number + * @param newPageNumber new page number + */ + protected updatePageNumber(newPageNumber: number): void { + this.config.pageNumber = this.pageNumber = newPageNumber; + this.onPageNumberChange.emit({ + pageNumber: newPageNumber + } as PaginationEvent); + } + + /** + * Get Last Page Number + */ + private getLastPageNumber(): number { + return Math.ceil(this.config.totalItems / (this.config.pageSize ?? 5)); + } + + /** + * Check if current Page is Last Page + */ + private isLastPage(): boolean { + return (this.config.pageNumber === this.lastPageNumber); + } +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.module.ts new file mode 100644 index 000000000..c9a896d20 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pagination/pagination.module.ts @@ -0,0 +1,30 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; + +import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { PaginationConfig } from './pagination-config'; +import { PaginationComponent } from './pagination.component'; +import { PaginationEvent } from './pagination-event'; + +export { + PaginationConfig, + PaginationEvent +}; + +/** + * A module containing objects associated with notification components + */ +@NgModule({ + imports: [ + BsDropdownModule.forRoot(), + CommonModule, + FormsModule, + PaginationComponent + ], + //declarations: [PaginationComponent], + exports: [PaginationComponent], + providers: [BsDropdownConfig] +}) +export class PaginationModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/index.ts new file mode 100644 index 000000000..2951a1795 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/index.ts @@ -0,0 +1,3 @@ +export * from './search-highlight/index'; +export * from './sort-array/index'; +export * from './truncate/index'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/index.ts new file mode 100644 index 000000000..00ff49ba7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/index.ts @@ -0,0 +1,2 @@ +export { SearchHighlightPipeModule } from './search-highlight.pipe.module'; +export { SearchHighlightPipe } from './search-highlight.pipe'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/search-highlight.pipe.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/search-highlight.pipe.module.ts new file mode 100644 index 000000000..97f4ea15c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/search-highlight.pipe.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { SearchHighlightPipe } from './search-highlight.pipe'; + +/** + * A module containing objects associated with the search highlight pipe + */ +@NgModule({ + imports: [ + SearchHighlightPipe + ], + declarations: [ + //SearchHighlightPipe + ], + exports: [ + SearchHighlightPipe + ] +}) +export class SearchHighlightPipeModule { } diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/search-highlight.pipe.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/search-highlight.pipe.ts new file mode 100644 index 000000000..55dc609eb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/search-highlight/search-highlight.pipe.ts @@ -0,0 +1,60 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/** + * Search highlight pipe + * + * This is currently used with the type ahead feature of the filter fields component + * + * Usage: + *
+ * // Individual module import
+ * import { SearchHighlightPipeModule } from 'patternfly-ng/pipe';
+ * // Or
+ * import { SearchHighlightPipeModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [SearchHighlightPipeModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ */ +@Pipe({ name: 'searchHighlight' }) +export class SearchHighlightPipe implements PipeTransform { + /** + * Transform the substring matching the given search + * + * @param {string} val The string to highlight + * @param {string} search The text to search for + * @returns {any} The given string with highlighted text + */ + transform(val: string, search: string): any { + if (search !== undefined && search.length > 0) { + let lowerVal = val.toLowerCase(); + search = search.toLowerCase(); + if (!lowerVal) return ''; + else return this.convertToOriginal(lowerVal.split(search).join('' + search + ''), val); + } else { + return val; + } + } + + private convertToOriginal(str: string, original: string): string { + let output = ''; + let inTag = false; + let j = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '<') { + inTag = true; + output += str[i]; + } else if (str[i] === '>') { + inTag = false; + output += str[i]; + } else if (!inTag) { + output += original[j++]; + } else { + output += str[i]; + } + } + return output; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/index.ts new file mode 100644 index 000000000..617a80466 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/index.ts @@ -0,0 +1,2 @@ +export { SortArrayPipeModule } from './sort-array.pipe.module'; +export { SortArrayPipe } from './sort-array.pipe'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/sort-array.pipe.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/sort-array.pipe.module.ts new file mode 100644 index 000000000..ef62f9ed7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/sort-array.pipe.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { SortArrayPipe } from './sort-array.pipe'; + +/** + * A module containing objects associated with the sort array pipe + */ +@NgModule({ + imports: [ + SortArrayPipe + ], + declarations: [ + //SortArrayPipe + ], + exports: [ + SortArrayPipe + ] +}) +export class SortArrayPipeModule { } diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/sort-array.pipe.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/sort-array.pipe.ts new file mode 100644 index 000000000..637badb8f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/sort-array/sort-array.pipe.ts @@ -0,0 +1,44 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { orderBy } from 'lodash-es'; + +/** + * Sort array pipe + * + * This is currently used with the pin feature of the list component + * + * Example: + *
+ * + * Usage: + *
+ * // Individual module import
+ * import { SortArrayPipeModule } from 'patternfly-ng/pipe';
+ * // Or
+ * import { SortArrayPipeModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [SortArrayPipeModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ */ +@Pipe({ name: 'sortArray'}) +export class SortArrayPipe implements PipeTransform { + /** + * Sort array by property + * + * @param {Array} arr Array to sort + * @param prop Property name to sort by + * @param {boolean} descending True to sort descending + * @returns {any} Returns sorted array + */ + transform(arr: Array, prop: any, descending: boolean = false): any { + if (arr === undefined) { + return arr; + } + const sortOrder = descending ? 'desc' : 'asc'; + const sortedArray = orderBy(arr, [prop], [sortOrder]); + return sortedArray; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/index.ts new file mode 100644 index 000000000..9ae3fd85a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/index.ts @@ -0,0 +1,2 @@ +export { TruncatePipeModule } from './truncate.pipe.module'; +export { TruncatePipe } from './truncate.pipe'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/truncate.pipe.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/truncate.pipe.module.ts new file mode 100644 index 000000000..e18649746 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/truncate.pipe.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { TruncatePipe } from './truncate.pipe'; + +/** + * A module containing objects associated with the truncate pipe + */ +@NgModule({ + imports: [ + TruncatePipe + ], + declarations: [ + //TruncatePipe + ], + exports: [ + TruncatePipe + ] +}) +export class TruncatePipeModule { } diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/truncate.pipe.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/truncate.pipe.ts new file mode 100644 index 000000000..39b7a984c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/pipe/truncate/truncate.pipe.ts @@ -0,0 +1,34 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/** + * Truncate pipe + * + * This is currently used with the save filter feature of the filter fields component + * + * Usage: + *
+ * // Individual module import
+ * import { TruncatePipeModule } from 'patternfly-ng/pipe';
+ * // Or
+ * import { TruncatePipeModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [TruncatePipeModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ */ +@Pipe({ name: 'truncate'}) +export class TruncatePipe implements PipeTransform { + /** + * Truncate given string + * + * @param {string} value The string to truncate + * @param {string} limit The number of characters to truncate the string at + * @param {string} trail The trailing characters representing truncation + * @returns {string} The truncated string + */ + transform(value: string, limit: number = 10, trail: string = '...'): string { + return (value.length > limit) ? value.substring(0, limit) + trail : value; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/index.ts new file mode 100644 index 000000000..42fcb77ce --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/index.ts @@ -0,0 +1,5 @@ +export { SortComponent } from './sort.component'; +export { SortConfig } from './sort-config'; +export { SortEvent } from './sort-event'; +export { SortField } from './sort-field'; +export { SortModule } from './sort.module'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-config.ts new file mode 100644 index 000000000..d87ef0258 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-config.ts @@ -0,0 +1,26 @@ +import { SortField } from './sort-field'; + +/** + * A config containing properties for sort + */ +export class SortConfig { + /** + * A flag indicating the component is disabled + */ + disabled?: boolean; + + /** + * A list of sortable fields + */ + fields?: SortField[]; + + /** + * True if sort is ascending + */ + isAscending?: boolean; + + /** + * True if sort should be shown + */ + visible?: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-event.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-event.ts new file mode 100644 index 000000000..524435607 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-event.ts @@ -0,0 +1,16 @@ +import { SortField } from './sort-field'; + +/** + * An object containing properties for sort events + */ +export class SortEvent { + /** + * The currently selected filterable field + */ + field!: SortField; + + /** + * True if sort is ascending + */ + isAscending!: boolean; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-field.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-field.ts new file mode 100644 index 000000000..88dff13c0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort-field.ts @@ -0,0 +1,19 @@ +/** + * An object containing properties for a sortable field, used to select categories of sorting + */ +export class SortField { + /** + * A unique Id for the sort field + */ + id?: string; + + /** + * The title to display for the sort field + */ + title?: string; + + /** + * The sort field type (e.g., 'alpha' or 'numeric' ) + */ + sortType!: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.html new file mode 100644 index 000000000..b68513ce7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.html @@ -0,0 +1,21 @@ +
+ + +
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.less new file mode 100644 index 000000000..72ec26ef2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.less @@ -0,0 +1,11 @@ +.sort-pf { + .btn-link { + margin-left: 10px; + padding: 4px 0; + min-width: 0; + color: @color-pf-black; // was #252525; + font-size: 16px; + line-height: 1; + &:hover { color: @color-pf-blue-400; } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.ts new file mode 100644 index 000000000..706659c76 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.component.ts @@ -0,0 +1,146 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { cloneDeep, defaults, isEqual } from 'lodash-es'; + +import { SortConfig } from './sort-config'; +import { SortField } from './sort-field'; +import { SortEvent } from './sort-event'; + + +/** + * Sort component + * + * Usage: + *
+ * // Individual module import
+ * import { SortModule } from 'patternfly-ng/sort';
+ * // Or
+ * import { SortModule } from 'patternfly-ng';
+ *
+ * @NgModule({
+ *   imports: [SortModule,...]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { SortConfig, SortEvent, SortField } from 'patternfly-ng/sort';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-sort', + templateUrl: './sort.component.html', + imports: [ + CommonModule + ] +}) +export class SortComponent implements DoCheck, OnInit { + /** + * The sort config containing component properties + */ + @Input() config?: SortConfig; + + /** + * The event emitted when the sort has changed + */ + @Output('onChange') onChange = new EventEmitter(); + + currentField: SortField = new SortField; + private defaultConfig: SortConfig = { + isAscending: true, + visible: true + } as SortConfig; + private prevConfig: SortConfig = new SortConfig; + + /** + * The default constructor + */ + constructor() { + } + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + + if (this.config && this.config.fields && this.config.fields.length > 0) { + if (this.currentField === undefined) { + this.currentField = this.config.fields[0]; + } + if (this.config.isAscending === undefined) { + this.config.isAscending = true; + } + } + this.prevConfig = cloneDeep(this.config); + } + + // Actions + + getIconStyleClass(): string { + let iconStyleClass: string; + if (this.currentField && this.currentField.sortType + && this.currentField.sortType === 'numeric') { + if (this.config?.isAscending) { + iconStyleClass = 'fa fa-sort-numeric-asc'; + } else { + iconStyleClass = 'fa fa-sort-numeric-desc'; + } + } else { + if (this.config?.isAscending) { + iconStyleClass = 'fa fa-sort-alpha-asc'; + } else { + iconStyleClass = 'fa fa-sort-alpha-desc'; + } + } + return iconStyleClass; + } + + onChangeDirection(): void { + this.config!.isAscending = !this.config?.isAscending; + this.onChange.emit({ + field: this.currentField, + isAscending: this.config?.isAscending + } as SortEvent); + } + + selectField(field: SortField): void { + this.currentField = field; + this.onChange.emit({ + field: this.currentField, + isAscending: this.config?.isAscending + } as SortEvent); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.module.ts new file mode 100644 index 000000000..a5fa4991d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/sort/sort.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { SortComponent } from './sort.component'; + +/** + * A module containing objects associated with the sort component + */ +@NgModule({ + imports: [CommonModule, BsDropdownModule.forRoot(), SortComponent], + declarations: [ + //SortComponent + ], + exports: [SortComponent], + providers: [BsDropdownConfig] +}) +export class SortModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/index.ts new file mode 100644 index 000000000..3b5b2b3d3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/index.ts @@ -0,0 +1,4 @@ +export { ToolbarConfig } from './toolbar-config'; +export { ToolbarComponent } from './toolbar.component'; +export { ToolbarModule } from './toolbar.module'; +export { ToolbarView } from './toolbar-view'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar-config.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar-config.ts new file mode 100644 index 000000000..5ac332dbc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar-config.ts @@ -0,0 +1,41 @@ +import { ActionConfig } from '../action/action-config'; +import { FilterConfig } from '../filter/filter-config'; +import { SortConfig } from '../sort/sort-config'; +import { ToolbarView } from './toolbar-view'; + +/** + * A config containing properties for toolbar + */ +export class ToolbarConfig { + /** + * Config properties for toolbar actions + */ + actionConfig?: ActionConfig; + + /** + * A flag indicating the component is disabled + * + * Note: This will not disable components within your custom action and view templates + */ + disabled?: boolean; + + /** + * Config properties for toolbar filter. If undefined, filter features are not shown. + */ + filterConfig?: FilterConfig; + + /** + * Config properties for toolbar sort. If undefined, sort features are not shown. + */ + sortConfig?: SortConfig; + + /** + * The currently selected view + */ + view?: ToolbarView; + + /** + * List of available views. + */ + views!: ToolbarView[]; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar-view.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar-view.ts new file mode 100644 index 000000000..277324bdd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar-view.ts @@ -0,0 +1,29 @@ +/** + * An view containing common properties + */ +export class ToolbarView { + /** + * True if view is disabled + */ + disabled?: boolean; + + /** + * Style class to use for the view selector + */ + iconStyleClass?: string; + + /** + * Unique id for the view + */ + id?: string; + + /** + * A tooltip for the view selector + */ + tooltip?: string; + + /** + * Text announced to screen readers for the action config button + */ + ariaLabel?: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.html new file mode 100644 index 000000000..6d9e07fc5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.html @@ -0,0 +1,43 @@ +
+
+
+
+ +
+
+ +
+
+ + +
+
+
+ + + + +
+
+
+ +
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.less b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.less new file mode 100644 index 000000000..671e08385 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.less @@ -0,0 +1,30 @@ +.dropdown-kebab-pf.invisible { + opacity: 0; + pointer-events: none; +} + +.toolbar-pf-actions { + .btn { min-width: unset; } + .toolbar-pf-view-selector { + a { cursor: pointer; } + } + .dropdown-menu { + a { cursor: pointer; } + } + .dropdown-kebab-pf { float: right; } + .toolbar-apf-filter { + /* stylelint-disable */ + padding-left: 0 !important; + /* stylelint-enable */ + @media (min-width: 768px) { + padding-left: 0; + } + } +} + +.toolbar-pf-include-actions { + display: inline-block; + margin: 0 5px; +} + +.toolbar-pf-actions.no-filter-results { margin-bottom: 10px; } diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.ts new file mode 100644 index 000000000..2e76ebc9f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.component.ts @@ -0,0 +1,283 @@ +import { + Component, + DoCheck, + EventEmitter, + Input, + OnInit, + Output, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { cloneDeep, defaults, find, isEqual, remove } from 'lodash-es'; + +import { Action } from '../action/action'; +import { Filter } from '../filter/filter'; +import { FilterFieldsComponent } from '../filter/filter-fields.component'; +import { FilterEvent } from '../filter/filter-event'; +import { SortEvent } from '../sort/sort-event'; +import { ToolbarConfig } from './toolbar-config'; +import { ToolbarView } from './toolbar-view'; +import { ActionComponent } from '../action'; +import { SortComponent } from '../sort'; +import { FilterResultsComponent } from '../filter'; + + +/** + * Toolbar component + * + * Usage: + *
+ * // Individual module import
+ * import { ToolbarModule } from 'patternfly-ng/toolbar';
+ * // Or
+ * import { ToolbarModule } from 'patternfly-ng';
+ *
+ * // NGX Bootstrap
+ * import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown';
+ *
+ * @NgModule({
+ *   imports: [BsDropdownModule.forRoot(), ToolbarModule,...],
+ *   providers: [BsDropdownConfig]
+ * })
+ * export class AppModule(){}
+ * 
+ * + * Optional: + *
+ * import { ToolbarConfig, ToolbarView } from 'patternfly-ng/toolbar';
+ * 
+ */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-toolbar', + templateUrl: './toolbar.component.html', + imports: [ + CommonModule, + ActionComponent, + FilterFieldsComponent, + FilterResultsComponent, + SortComponent + ] +}) +export class ToolbarComponent implements DoCheck, OnInit { + /** + * The toolbar config containing component properties + */ + @Input() config!: ToolbarConfig; + + /** + * The name of the template containing actions + */ + @Input() actionTemplate!: TemplateRef; + + /** + * The name of the template containing views + */ + @Input() viewTemplate!: TemplateRef; + + /** + * The event emitted when an action (e.g., button, kebab, etc.) has been selected + */ + @Output('onActionSelect') onActionSelect = new EventEmitter(); + + /** + * The event emitted when a field menu option is selected + */ + @Output('onFilterFieldSelect') onFilterFieldSelect = new EventEmitter(); + + /** + * The event emitted when a filter has been changed + */ + @Output('onFilterChange') onFilterChange = new EventEmitter(); + + /** + * The event emitted when a filter has been saved + */ + @Output('onFilterSave') onFilterSave = new EventEmitter(); + + /** + * The event emitted when the user types ahead in the query input field + */ + @Output('onFilterTypeAhead') onFilterTypeAhead = new EventEmitter(); + + /** + * The event emitted when the sort has changed + */ + @Output('onSortChange') onSortChange = new EventEmitter(); + + /** + * The event emitted when a view has been selected + */ + @Output('onViewSelect') onViewSelect = new EventEmitter(); + + @ViewChild('filterFields') private filterFields!: FilterFieldsComponent; + + private defaultConfig: ToolbarConfig = { + disabled: false + } as ToolbarConfig; + private prevConfig!: ToolbarConfig; + + /** + * The default constructor + */ + constructor() { + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + + if (this.config && this.config.filterConfig) { + this.config.filterConfig.disabled = this.config.disabled; + if (this.config.filterConfig.appliedFilters === undefined) { + this.config.filterConfig.appliedFilters = []; + } + } + if (this.config && this.config.sortConfig) { + this.config.sortConfig.disabled = this.config.disabled; + if (this.config.sortConfig.fields === undefined) { + this.config.sortConfig.fields = []; + } + } + if (this.config.sortConfig !== undefined && this.config.sortConfig.visible === undefined) { + this.config.sortConfig.visible = true; + } + if (this.config && this.config.views === undefined) { + this.config.views = []; + } + if (this.config && this.config.view === undefined) { + this.config.view = this.config.views[0]; + } + this.prevConfig = cloneDeep(this.config); + } + + // Actions + + /** + * Handle clear filter event + * + * @param $event An array of current Filter objects + */ + clearFilter($event: Filter[]): void { + if (this.config.filterConfig) { + this.config.filterConfig.appliedFilters = $event; + } + this.onFilterChange.emit({ + appliedFilters: $event + } as FilterEvent); + } + + /** + * Reset current field and value + */ + resetFilterField() { + if (this.filterFields !== undefined) { + this.filterFields.reset(); + } + } + + // Private + + filterAdded($event: FilterEvent): void { + let newFilter = { + field: $event.field, + query: $event.query, + value: $event.value + } as Filter; + + if (!this.filterExists(newFilter)) { + if (newFilter.field.type === 'select') { + this.enforceSingleSelect(newFilter); + } + if (this.config.filterConfig && this.config.filterConfig.appliedFilters) { + this.config.filterConfig.appliedFilters.push(newFilter); + } + if (this.config.filterConfig) { + $event.appliedFilters = this.config.filterConfig.appliedFilters; + } + this.onFilterChange.emit($event); + } + } + + private filterExists(filter: Filter): boolean { + let foundFilter; + if (this.config.filterConfig && this.config.filterConfig.appliedFilters) { + foundFilter = find(this.config.filterConfig.appliedFilters, { + field: filter.field, + query: filter.query, + value: filter.value + }); + } + return foundFilter !== undefined; + } + + handleAction(action: Action): void { + if (action && action.disabled !== true) { + this.onActionSelect.emit(action); + } + } + + handleFilterFieldSelect($event: FilterEvent): void { + this.onFilterFieldSelect.emit($event); + } + + handleFilterSave($event: any) { + this.onFilterSave.emit($event); + } + + handleFilterTypeAhead($event: FilterEvent) { + this.onFilterTypeAhead.emit($event); + } + + sortChange($event: SortEvent): void { + this.onSortChange.emit($event); + } + + isViewSelected(currentView: ToolbarView): boolean { + return !!this.config.view && this.config.view.id === currentView.id; + } + + viewSelected(currentView: ToolbarView): void { + this.config.view = currentView; + if (!currentView.disabled) { + this.onViewSelect.emit(currentView); + } + } + + // Utils + + private enforceSingleSelect(filter: Filter): void { + const filterField = { title: filter.field.title }; + if (this.config.filterConfig && this.config.filterConfig.appliedFilters) { + remove(this.config.filterConfig.appliedFilters, { field: filterField }); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.module.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.module.ts new file mode 100644 index 000000000..86d9afdce --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/toolbar/toolbar.module.ts @@ -0,0 +1,27 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { BsDropdownConfig, BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { ActionModule } from '../action/action.module'; +import { FilterModule } from '../filter/filter.module'; +import { SortModule } from '../sort/sort.module'; +import { ToolbarComponent } from './toolbar.component'; + +/** + * A module containing objects associated with the toolbar component + */ +@NgModule({ + imports: [ + ActionModule, + BsDropdownModule.forRoot(), + CommonModule, + FilterModule, + SortModule, + ToolbarComponent + ], + //declarations: [ToolbarComponent], + exports: [ToolbarComponent], + providers: [BsDropdownConfig] +}) +export class ToolbarModule {} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/utilities/index.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/utilities/index.ts new file mode 100644 index 000000000..09881f194 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/utilities/index.ts @@ -0,0 +1 @@ +export * from './window.reference'; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/utilities/window.reference.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/utilities/window.reference.ts new file mode 100644 index 000000000..35cc2deed --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/patternfly-ng/utilities/window.reference.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; + +function _window(): any { + // return the global native browser window object + return window; +} + +/** + * Native window reference + * + * Usage: + *
+ * // Individual module import
+ * import { WindowReference } from 'patternfly-ng/utilities';
+ * // Or
+ * import { WindowReference } from 'patternfly-ng';
+ */
+@Injectable()
+export class WindowReference {
+  get nativeWindow(): any {
+    return _window();
+  }
+}
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/score-treemap/score-treemap.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/score-treemap/score-treemap.component.css
new file mode 100644
index 000000000..61977d3f6
--- /dev/null
+++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/score-treemap/score-treemap.component.css
@@ -0,0 +1,12 @@
+.rect-defocused {
+  opacity: 0.3 !important;
+}
+.score-treemap-legend-container {
+  display: flex;
+}
+.score-treemap-legend {
+  margin: 5px; 
+  width: 100px; 
+  height: 10px; 
+  background-image: linear-gradient(to left, #def3ff, #39a5dc, #004368);
+}
\ No newline at end of file
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/score-treemap/score-treemap.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/score-treemap/score-treemap.component.ts
new file mode 100644
index 000000000..6b5b19991
--- /dev/null
+++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/score-treemap/score-treemap.component.ts
@@ -0,0 +1,234 @@
+/*
+ * Copyright The Microcks Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit, Input } from '@angular/core';
+import { Router } from '@angular/router';
+import * as d3 from 'd3';
+import { HierarchyRectangularNode } from 'd3-hierarchy';
+
+const phi: number = (1 + Math.sqrt(5)) / 2;
+
+@Component({
+  selector: 'app-score-treemap',
+  styleUrls: ['./score-treemap.component.css'],
+  template: `
+    
+
+
+ {{ legend }} +
+
+ 0% 100% +
+
+
+
+ ` +}) +export class ScoreTreemapComponent implements OnInit { + + @Input() + data!: HierarchyRectangularNode; + //data: d3.layout.treemap.Node; + + @Input() + scoreAttr!: string; + + @Input() + block!: string; + + @Input() + elements!: string; + + @Input() + legend!: string; + + margin = {top: 5, right: 5, bottom: 5, left: 5}; + width: number = 30; + height: number = 10; + + + treemap: d3.TreemapLayout = {} as d3.TreemapLayout; + tooltip: d3.Selection = {} as d3.Selection; + + //treemap: d3.layout.Treemap; + //tooltip: d3.Selection; + + constructor(private router: Router) { + } + + ngOnInit() { + this.width = parseInt(d3.select('#scoreTreemap').style('width')) - this.margin.left - this.margin.right, + this.height = this.width / 3; + + this.tooltip = d3.select('body') + .append('div') + .style('position', 'absolute') + .style('z-index', '10') + .style('visibility', 'hidden') + .style('padding', '0 6px') + .style('color', '#fff') + .style('background', '#292e34'); + + const svg = d3.select('#scoreTreemap') + .append('svg') + .attr('width', this.width + this.margin.left + this.margin.right) + .attr('height', this.height + this.margin.top + this.margin.bottom) + .append('g') + .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); + + this.treemap = d3.treemap() + .round(false) + .size([this.width, this.height]) + .padding(1) + //.tile(d3.treemapBinary); + //.tile(d3.treemapSquarify); + //.tile(d3.treemapResquarify.ratio(phi)); + + const nodes: HierarchyRectangularNode[] = this.treemap( + d3.hierarchy(this.data) + .sum((d) => (d.value || 0)) + .sort((a, b) => (b.data as any)[this.scoreAttr] - (a.data as any)[this.scoreAttr]) + ).leaves(); + + /* + for (let i = 0; i < nodes.length; i++) { + console.log("nodes[" + i + "]: data.name: " + nodes[i].data.name + ", data.value: " + nodes[i].data.value + ", data.score: " + (nodes[i].data as any).score); + console.log(" x0: " + nodes[i].x0 + ", y0: " + nodes[i].y0 + ", x1: " + nodes[i].x1 + ", y1: " + nodes[i].y1); + } + */ + + const cell = svg.selectAll('g') + .data(nodes) + .enter().append('g') + .attr('class', 'cell') + .attr('transform', (d) => 'translate(' + d.x0 + ',' + d.y0 + ')'); + + this.initializeTreemap(cell); + } + + private initializeTreemap(cell: d3.Selection, SVGGElement, unknown>): void { + const tooltip = this.tooltip; + const scoreAttr = this.scoreAttr; + const block = this.block; + const elements = this.elements; + + cell.append('rect') + .attr('id', (d) => d.data.name) + .attr('width', (d) => { + return d.x1 - d.x0 + }) + .attr('height', (d) => d.y1 - d.y0) + .on('mouseover', (event, d) => { + tooltip.text(d.value + ' ' + elements + ' - ' + (d.data as any)[scoreAttr] + ' %'); + cell.selectAll('rect') + .filter(function(item) { + return (item as any).data.name !== d.data.name; + }) + .classed('rect-defocused', true); + return tooltip.style('visibility', 'visible'); + }) + .on('mousemove', (event) => tooltip + .style('top', (event.pageY - 10) + 'px') + .style('left', (event.pageX + 10) + 'px') + ) + .on('mouseout', () => { + cell.selectAll('rect') + .filter(function(d) { + return true; + }) + .classed('rect-defocused', false); + return tooltip.style('visibility', 'hidden'); + }) + .style('fill', d => this.colorGradient(1 - ((d.data as any)[scoreAttr] / 100)) ); + + cell.append('text') + .attr('x', (d) => (d.x1 - d.x0) / 2) + .attr('y', (d) => (d.y1 - d.y0) / 2) + .attr('dy', '.35em') + .attr('text-anchor', 'middle') + .attr('fill', d => (d.data as any)[scoreAttr] < 50 ? 'white' : 'black') + .text(d => block + ':' + d.data.name ); + + d3.select(window).on('resize', () => + this.resizeTreemap() + ); + } + + private resizeTreemap(): void { + this.width = parseInt(d3.select('#scoreTreemap').style('width')) - this.margin.left - this.margin.right; + this.height = this.width / 3; + + let svg = d3.select('#scoreTreemap').select('svg') + .attr('width', this.width + this.margin.left + this.margin.right) + .attr('height', this.height + this.margin.top + this.margin.bottom); + + this.treemap.round(false) + .size([this.width, this.height]) + .padding(1) + .tile(d3.treemapSquarify); + + const nodes: HierarchyRectangularNode[] = this.treemap( + d3.hierarchy(this.data) + .sum((d) => (d.value || 0)) + .sort((a, b) => (b.data as any)[this.scoreAttr] - (a.data as any)[this.scoreAttr]) + ).leaves(); + + // Reinitialize graphics in svg. + let cell = svg.selectAll('g'); + cell.remove(); + + let newSvg = svg.append('g') + .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); + + let newCell = newSvg.selectAll('g') + .data(nodes) + .enter().append('g') + .attr('class', 'cell') + .attr('transform', (d) => 'translate(' + d.x0 + ',' + d.y0 + ')'); + + this.initializeTreemap(newCell); + } + + private colorGradient(fadeFraction: number): string { + /* + let rgbColor1 = {red: 63, green: 156, blue: 53}; + let rgbColor2 = {red: 239, green: 170, blue: 0}; + let rgbColor3 = {red: 204, green: 0, blue: 0}; + //background-image: linear-gradient(to left, #3f9c35, #efaa00, #cc0000); + */ + const rgbColor1 = {red: 222, green: 243, blue: 255}; + const rgbColor2 = {red: 57, green: 165, blue: 220}; + const rgbColor3 = {red: 0, green: 67, blue: 104}; + // background-image: linear-gradient(to left, #def3ff, #39a5dc, #004368); + let color1 = rgbColor1; + let color2 = rgbColor2; + let fade = fadeFraction * 2; + if (fade >= 1) { + fade -= 1; + color1 = rgbColor2; + color2 = rgbColor3; + } + const diffRed = color2.red - color1.red; + const diffGreen = color2.green - color1.green; + const diffBlue = color2.blue - color1.blue; + const gradient = { + red: parseInt(Math.floor(color1.red + (diffRed * fade)).toString(), 10), + green: parseInt(Math.floor(color1.green + (diffGreen * fade)).toString(), 10), + blue: parseInt(Math.floor(color1.blue + (diffBlue * fade)).toString(), 10), + }; + return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')'; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/test-bar-chart/test-bar-chart.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/test-bar-chart/test-bar-chart.component.css new file mode 100644 index 000000000..ff2c0c330 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/test-bar-chart/test-bar-chart.component.css @@ -0,0 +1,18 @@ +.bar { + display: inline-block; + border: 1px; + border-bottom: 1px solid #CCC; + margin: 5px; +} +.bar-success { + background: #7BB33D; +} +.bar-success:hover { + background: #73A839; +} +.bar-failure { + background: #dd1f26; +} +.bar-failure:hover { + background: #d41e24; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/test-bar-chart/test-bar-chart.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/test-bar-chart/test-bar-chart.component.ts new file mode 100644 index 000000000..906bd37fa --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/test-bar-chart/test-bar-chart.component.ts @@ -0,0 +1,131 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit, Input } from '@angular/core'; + +import { TestResult } from '../../models/test.model'; + +import * as d3 from 'd3'; + +@Component({ + selector: 'app-test-bar-chart', + styleUrls: ['./test-bar-chart.component.css'], + template: ` +
+ ` +}) +export class TestBarChartComponent implements OnInit { + @Input() + data!: TestResult[]; + + minValues = 10; + displayThreshold = 3; + + height = 140; + margin = {top: 5, right: 5, bottom: 5, left: 5}; + + constructor() { + } + + ngOnInit() { + let maxval = 0; + let quinte = 0; + let minval = Number.MAX_VALUE; + const chartData = this.data.slice(0, this.data.length).reverse() + .map(function(item) { + maxval = Math.max(maxval, item.elapsedTime); + quinte = Math.max(maxval / 5, item.elapsedTime); + minval = Math.min(minval, item.elapsedTime); + return { + id : item.id, + success : item.success, + testDate : new Date(item.testDate), + testNumber: item.testNumber, + elapsedTime : item.elapsedTime + }; + }); + if (maxval == 0) { + maxval = 1; + } + + if (chartData.length < this.minValues) { + const dataLength = chartData.length; + for (let i = 0; i < this.minValues - dataLength; i++) { + chartData.unshift({ + id : 'empty', + success : true, + testDate : new Date(), + testNumber: 0, + elapsedTime : 0 + }); + } + } + + const height = this.height; + const width = parseInt(d3.select('#testBarChart').style('width')) + - (chartData.length * (this.margin.left + this.margin.right)); + + const vis = d3.select('#testBarChart').selectAll('div').data(chartData); + vis.enter() + .append('div').attr('class', 'test-box-container') + .append('div').attr('class', function(d) { + if (d.id !== 'empty') { + if (d.success === true) { + return 'bar bar-success tooltipaware'; + } else { + return 'bar bar-failure tooltipaware'; + } + } else { + return 'bar'; + } + }) + .style('height', function(d) { + if (d.elapsedTime == 0) { + d.elapsedTime = 1; + } + let h = d.elapsedTime * height / maxval as number; + // Enhanced display of lower value so that they're still visible. + if (d.elapsedTime < quinte) { + h = d.elapsedTime * height / quinte; + } + return h + 'px'; + }) + .style('width', function(d) { + // console.log('width: ' + width); + const w = width / chartData.length as number; + // console.log('w: ' + w); + return w + 'px'; + }) + .attr('data-placement', 'left').attr('title', function(d) { + return '[' + d.testDate.toISOString() + '] : ' + d.elapsedTime + ' ms'; + }) + .on('click', (event, d) => { + document.location.href = '/#/tests/' + d.id.toString(); + }); + + d3.selectAll('#testBarChart > .test-box-container > .bar').each(function(d) { + const div = document.createElement('div'); + div.setAttribute('class', 'text-center'); + if ((d as any)['testNumber'] != 0) { + div.innerText = 'Test #' + (d as any)['testNumber']; + } else { + div.innerText = '-'; + } + (this as any).parentNode.insertBefore(div, (this as any).nextSibling); + }); + + vis.exit().remove(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/time-ago.pipe.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/time-ago.pipe.ts new file mode 100644 index 000000000..7932c2ffb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/time-ago.pipe.ts @@ -0,0 +1,100 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { OnDestroy, Pipe, PipeTransform, ChangeDetectorRef, NgZone, Inject } from "@angular/core"; + +@Pipe({ + name: 'timeAgo', + pure: false, // required to update the value when stateChanges emits +}) +export class TimeAgoPipe implements PipeTransform, OnDestroy { + private timer?: number | null; + + constructor( + @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, + @Inject(NgZone) private ngZone: NgZone) {} + + transform(value: string): string { + this.removeTimer(); + let d = new Date(value); + let now = new Date(); + let seconds = Math.round(Math.abs((now.getTime() - d.getTime())/1000)); + let timeToUpdate = (Number.isNaN(seconds)) ? 1000 : this.getSecondsUntilUpdate(seconds) *1000; + this.timer = this.ngZone.runOutsideAngular(() => { + if (typeof window !== 'undefined') { + return window.setTimeout(() => { + this.ngZone.run(() => this.changeDetectorRef.markForCheck()); + }, timeToUpdate); + } + return null; + }); + let minutes = Math.round(Math.abs(seconds / 60)); + let hours = Math.round(Math.abs(minutes / 60)); + let days = Math.round(Math.abs(hours / 24)); + let months = Math.round(Math.abs(days/30.416)); + let years = Math.round(Math.abs(days/365)); + if (Number.isNaN(seconds)){ + return ''; + } else if (seconds <= 45) { + return 'a few seconds ago'; + } else if (seconds <= 90) { + return 'a minute ago'; + } else if (minutes <= 45) { + return minutes + ' minutes ago'; + } else if (minutes <= 90) { + return 'an hour ago'; + } else if (hours <= 22) { + return hours + ' hours ago'; + } else if (hours <= 36) { + return 'a day ago'; + } else if (days <= 25) { + return days + ' days ago'; + } else if (days <= 45) { + return 'a month ago'; + } else if (days <= 345) { + return months + ' months ago'; + } else if (days <= 545) { + return 'a year ago'; + } else { // (days > 545) + return years + ' years ago'; + } + } + + ngOnDestroy(): void { + this.removeTimer(); + } + + private removeTimer() { + if (this.timer) { + window.clearTimeout(this.timer); + this.timer = null; + } + } + + private getSecondsUntilUpdate(seconds:number) { + let min = 60; + let hr = min * 60; + let day = hr * 24; + if (seconds < min) { // less than 1 min, update every 2 secs + return 2; + } else if (seconds < hr) { // less than an hour, update every 30 secs + return 30; + } else if (seconds < day) { // less then a day, update every 5 mins + return 300; + } else { // update every hour + return 3600; + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/vertical-nav/vertical-nav.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/vertical-nav/vertical-nav.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/vertical-nav/vertical-nav.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/vertical-nav/vertical-nav.component.html new file mode 100644 index 000000000..5af20195d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/vertical-nav/vertical-nav.component.html @@ -0,0 +1,84 @@ + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/vertical-nav/vertical-nav.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/vertical-nav/vertical-nav.component.ts new file mode 100644 index 000000000..602e1bf2b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/components/vertical-nav/vertical-nav.component.ts @@ -0,0 +1,209 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Component, + OnInit, + TemplateRef, + ViewEncapsulation, + ChangeDetectionStrategy, + AfterViewInit, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { Router, NavigationStart, RouterLink } from '@angular/router'; +import { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal'; + +import { AboutModalConfig, AboutModalEvent, AboutModalModule } from '../patternfly-ng/modal'; + +import { HelpDialogComponent } from '../help-dialog/help-dialog.component'; + +import { IAuthenticationService } from '../../services/auth.service'; +import { VersionInfoService } from '../../services/versioninfo.service'; +import { User } from '../../models/user.model'; +import { ConfigService } from '../../services/config.service'; +import { KeycloakAuthenticationService } from '../../services/auth-keycloak.service'; + +// Thanks to https://github.com/onokumus/metismenu/issues/110#issuecomment-317254128 +//import * as $ from 'jquery'; +declare let $: any; + +@Component({ + selector: 'app-vertical-nav', + encapsulation: ViewEncapsulation.None, + templateUrl: './vertical-nav.component.html', + styleUrls: ['./vertical-nav.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AboutModalModule, + BsDropdownModule, + CommonModule, + ModalModule, + RouterLink + ], +}) +export class VerticalNavComponent implements OnInit, AfterViewInit { + aboutConfig: AboutModalConfig = {}; + modalRef?: BsModalRef; + + constructor( + protected authService: IAuthenticationService, + private modalService: BsModalService, + private versionInfoSvc: VersionInfoService, + private config: ConfigService, + private router: Router + ) {} + + ngOnInit() { + this.user().subscribe((currentUser) => { + this.versionInfoSvc.getVersionInfo().subscribe((versionInfo) => { + this.aboutConfig = { + additionalInfo: + 'Microcks is Open Source mocking and testing platform for API and microservices. Visit https://microcks.io for more information.', + copyright: 'Distributed under Apache Licence v2.0', + logoImageAlt: 'Microcks', + logoImageSrc: 'assets/microcks.png', + title: 'About Microcks', + productInfo: [ + { name: 'Version', value: versionInfo.versionId }, + { name: 'Build timestamp', value: versionInfo.buildTimestamp }, + { name: 'User Login', value: currentUser.login }, + { name: 'User Name', value: currentUser.name }, + ], + } as AboutModalConfig; + }); + }); + + this.router.events + .pipe(filter((event) => event instanceof NavigationStart)) + .subscribe((event: NavigationStart) => { + // Do something with the NavigationStart event object. + //console.log('Navigation start event: ' + JSON.stringify(event)); + const navigationEvent = { type: 'navigationEvent', url: event.url }; + try { + window.parent.postMessage(JSON.stringify(navigationEvent), '*'); + } catch (error) { + console.warn('Error while posting navigationEvent to parent', error); + } + + // Navigation between sections may happen outside vertical navigation (ex: when we move from Hub to + // freshly installed API or Service). We then have to refresh the active class on the correct section. + let section = '/'; + if (event.url != '/') { + if (event.url.substring(1).indexOf('/') > 0) { + section += event.url.substring( + 1, + event.url.substring(1).indexOf('/') + 1 + ); + } else { + // Default to short section name. + section = event.url; + } + } + $( + 'div.nav-pf-vertical-with-sub-menus li.list-group-item a[routerLink="' + + section + + '"]' + ) + .parent() + .addClass('active'); + $( + 'div.nav-pf-vertical-with-sub-menus li.list-group-item a[routerLink!="' + + section + + '"]' + ) + .parent() + .removeClass('active'); + }); + } + + ngAfterViewInit() { + $().setupVerticalNavigation(true); + } + + public openHelpDialog() { + const initialState = {}; + this.modalRef = this.modalService.show(HelpDialogComponent, { + initialState, + }); + } + + public openAboutModal(template: TemplateRef): void { + this.modalRef = this.modalService.show(template); + } + public closeAboutModal($event: AboutModalEvent): void { + if (this.modalRef) { + this.modalRef.hide(); + } + } + + public user(): Observable { + return this.authService.getAuthenticatedUser(); + } + + /** + * Checks if authentication is enabled using the configurationService. + * @returns Returns true if authentication is enabled, false otherwise. + */ + public isAuthEnabled(): boolean { + return this.config.authType() !== 'anonymous'; + } + + public getPreferencesLink(): string { + if (this.config.authType() === 'keycloakjs') { + const keycloakSvc = this.authService as KeycloakAuthenticationService; + return keycloakSvc.getRealmUrl() + '/account/?referrer=microcks-app-js'; + } + return ''; + } + + public logout(): void { + this.authService.logout(); + } + + public hasRole(role: string): boolean { + return this.authService.hasRole(role); + } + + public canUseMicrocksHub(): boolean { + if (this.hasFeatureEnabled('microcks-hub')) { + const rolesStr = this.config.getFeatureProperty( + 'microcks-hub', + 'allowed-roles' + ); + if (rolesStr == undefined || rolesStr === '') { + return true; + } + // If roles specified, check if any is endorsed. + const roles = rolesStr.split(','); + for (const role of roles) { + if (this.hasRole(role)) { + return true; + } + } + return false; + } + // Default is false to keep behaviour of previous releases. + return false; + } + + public hasFeatureEnabled(feature: string): boolean { + return this.config.hasFeatureEnabled(feature); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/commons.model.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/commons.model.ts new file mode 100644 index 000000000..ff2ce3fa3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/commons.model.ts @@ -0,0 +1,21 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type Metadata = { + createdOn: number; + lastUpdate: number; + annotations: any; + labels: any; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/hub.model.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/hub.model.ts new file mode 100644 index 000000000..512d10736 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/hub.model.ts @@ -0,0 +1,75 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type APIPackage = { + name: string; + displayName: string; + categories: string[]; + createdAt: Date; + updatedAt: Date; + description: string; + imgUrl: string; + thumbUrl: string; + provider: string; + source: string; + maturity: string; + longDescription: string; + apis: APISummary[]; +} + +export type APISummary = { + name: string; + currentVersion: string; + versions: APINameVersion[]; +} + +export type APINameVersion = { + name: string; + version: string; +} + +export type APIVersion = { + id: string; + name: string; + displayName: string; + version: string; + versionForCompare: string; + createdAt: string; + replaces: string; + description: string; + imgUrl: string; + thumbUrl: string; + capabilityLevel: string; + contracts: Contract[]; + links: Link[]; + maintainers: Maintainer[]; + keywords: string; + packageName: string; +} + +export type Contract = { + type: string; + url: string; +} + +export type Link = { + name: string; + url: string; +} + +export type Maintainer = { + name: string; + email: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/importer.model.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/importer.model.ts new file mode 100644 index 000000000..a4490189d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/importer.model.ts @@ -0,0 +1,41 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Metadata } from './commons.model'; +import { SecretRef } from './secret.model'; + +export type ImportJob = { + id: string; + name: string; + repositoryUrl: string; + mainArtifact: boolean; + repositoryDisableSSLValidation: boolean; + frequency: string; + createdDate: Date; + lastImportDate: Date; + lastImportError: string; + active: boolean; + etag: string; + + metadata: Metadata; + secretRef?: SecretRef; + serviceRefs: ServiceRef[]; +} + +export type ServiceRef = { + serviceId: string; + name: string; + version: string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/metric.model.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/metric.model.ts new file mode 100644 index 000000000..8e64ad0e2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/metric.model.ts @@ -0,0 +1,57 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type DailyInvocations = { + id: string; + day: string; + serviceName: string; + serviceVersion: string; + dailyCount: number; + + hourlyCount: { string: number }; + minuteCount: { string: number }; +}; + +export type TestConformanceMetric = { + id: string; + serviceId: string; + aggregationLabelValue: string; + maxPossibleScore: number; + currentScore: number; + lastUpdateDay: string; + latestTrend: Trend; + + latestScores: { string: number }; +}; +export enum Trend { + DOWN = "DOWN", + LOW_DOWN = "LOW_DOWN", + STABLE = "STABLE", + LOW_UP = "LOW_UP", + UP = "UP", +} + +export type WeightedMetricValue = { + name: string; + weight: number; + value: number; +}; + +export type TestResultSummary = { + id: string; + testDate: number; + serviceId: string; + success: boolean; +}; \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/secret.model.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/secret.model.ts new file mode 100644 index 000000000..7302f03e5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/secret.model.ts @@ -0,0 +1,35 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type Secret = { + id: string; + name: string; + description: string; + username?: string; + password?: string; + token?: string; + tokenHeader?: string; + caCertPem?: string; +} + +export class SecretRef { + secretId: string; + name: string; + + constructor(secretId: string, name: string) { + this.secretId = secretId; + this.name = name; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/service.model.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/service.model.ts new file mode 100644 index 000000000..d8f5017af --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/service.model.ts @@ -0,0 +1,185 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Metadata } from "./commons.model"; + +export type Api = { + type: ServiceType; + name: string; + version: string; + resource: string; + referencePayload: string; +}; + +export type Service = { + id: string; + name: string; + version: string; + xmlNS: string; + type: ServiceType; + operations: Operation[]; + metadata: Metadata; + sourceArtifact: string; +}; +export enum ServiceType { + SOAP_HTTP = "SOAP_HTTP", + REST = "REST", + EVENT = "EVENT", + GRPC = "GRPC", + GENERIC_REST = "GENERIC_REST", + GENERIC_EVENT = "GENERIC_EVENT", + GRAPHQL = "GRAPHQL", +} + +export type Operation = { + name: string; + method: string; + action: string; + inputName: string; + outputName: string; + bindings: { [key: string]: Binding }; + dispatcher: string; + dispatcherRules: string; + defaultDelay: number; + resourcePaths: string[]; + parameterConstraints: ParameterConstraint[]; +}; + +export type OperationMutableProperties = { + dispatcher: string; + dispatcherRules: string; + defaultDelay: number; + parameterConstraints: ParameterConstraint[]; +}; + +export type Binding = { + type: BindingType; + keyType: string; + destinationType: string; + destinationName: string; + method: string; + qoS: string; + persistent: boolean; +}; +export enum BindingType { + KAFKA, + MQTT, + NATS, + WS, + AMQP, + AMQP1, + GOOGLEPUBSUB, + SQS, +} + +export type ParameterConstraint = { + name: string; + in: ParameterLocation; + required: boolean; + recopy: boolean; + mustMatchRegexp: string; +}; +export enum ParameterLocation { + path, + query, + header, +} + +export type Contract = { + id: string; + name: string; + content: string; + type: ContractType; + serviceId: string; + sourceArtifact: string; + mainArtifact: boolean; +}; +export enum ContractType { + WSDL = "WSDL", + XSD = "XSD", + JSON_SCHEMA = "JSON_SCHEMA", + SWAGGER = "SWAGGER", + RAML = "RAML", + OPEN_API_SPEC = "OPEN_API_SPEC", + OPEN_API_SCHEMA = "OPEN_API_SCHEMA", + ASYNC_API_SPEC = "ASYNC_API_SPEC", + ASYNC_API_SCHEMA = "ASYNC_API_SCHEMA", + AVRO_SCHEMA = "AVRO_SCHEMA", + PROTOBUF_SCHEMA = "PROTOBUF_SCHEMA", + PROTOBUF_DESCRIPTOR = "PROTOBUF_DESCRIPTOR", + GRAPHQL_SCHEMA = "GRAPHQL_SCHEMA", + POSTMAN_COLLECTION = "POSTMAN_COLLECTION", + SOAP_UI_PROJECT = "SOAP_UI_PROJECT", + JSON_FRAGMENT = "JSON_FRAGMENT" +} + +export type Header = { + name: string; + values: string[]; +}; + +export type Parameter = { + name: string; + value: string; +}; + +type Message = { + name: string; + content: string; + operationId: string; + testCaseId: string; + sourceArtifact: string; + headers: Header[]; +}; +export interface Request extends Message { + id: string; + responseId: string; + queryParameters: Parameter[]; +} +export interface Response extends Message { + id: string; + status: string; + mediaType: string; + dispatchCriteria: string; + isFault: boolean; +} +export interface EventMessage extends Message { + id: string; + mediaType: string; + dispatchCriteria: string; +} + +export type Exchange = { + type: string; +}; +export interface UnidirectionalEvent extends Exchange { + eventMessage: EventMessage; +} +export interface RequestResponsePair extends Exchange { + request: Request; + response: Response; +} + +export type ServiceView = { + service: Service; + //messagesMap: { string: Exchange[] }; + messagesMap: Record; +}; + +export type GenericResource = { + id: string; + serviceId: string; + payload: any; +}; \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/test.model.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/test.model.ts new file mode 100644 index 000000000..97b780633 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/test.model.ts @@ -0,0 +1,92 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { SecretRef } from './secret.model'; + +export enum OAuth2GrantType { + PASSWORD = 'PASSWORD', + CLIENT_CREDENTIALS = 'CLIENT_CREDENTIALS', + REFRESH_TOKEN = 'REFRESH_TOKEN' +} + +export type OAuth2ClientContext = { + clientId: string; + clientSecret: string; + tokenUri: string; + scopes: string; + username: string; + password: string; + refreshToken: string; + grantType: OAuth2GrantType | undefined; +} + +export type OAuth2AuthorizedClient = { + grantType: OAuth2GrantType; + principalName: string; + tokenUri: string; + scopes: string; +} + +export type TestRequest = { + serviceId: string; + testEndpoint: string; + runnerType: TestRunnerType; + operationsHeaders: any; + oAuth2Context: OAuth2ClientContext | undefined; +} + +export type TestResult = { + id: string; + version: number; + testNumber: number; + testDate: number; + testedEndpoint: string; + serviceId: string; + timeout: number; + elapsedTime: number; + success: boolean; + inProgress: boolean; + runnerType: TestRunnerType; + operationsHeaders: any; + testCaseResults: TestCaseResult[]; + secretRef: SecretRef; + authorizedClient: OAuth2AuthorizedClient; +} + +export type TestCaseResult = { + success: boolean; + elapsedTime: number; + operationName: string; + testStepResults: TestStepResult[]; +} + +export type TestStepResult = { + success: boolean; + elapsedTime: number; + requestName: string; + eventMessageName: string; + message: string; +} + +export enum TestRunnerType { + HTTP = 'HTTP', + SOAP_HTTP = 'SOAP_HTTP', + SOAP_UI = 'SOAP_UI', + POSTMAN = 'POSTMAN', + OPEN_API_SCHEMA = 'OPEN_API_SCHEMA', + ASYNC_API_SCHEMA = 'ASYNC_API_SCHEMA', + GRPC_PROTOBUF = 'GRPC_PROTOBUF', + GRAPHQL_SCHEMA = 'GRAPHQL_SCHEMA' +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/user.model.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/user.model.ts new file mode 100644 index 000000000..97bbf86f2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/models/user.model.ts @@ -0,0 +1,28 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class User { + username?: string; + login?: string; + name?: string; + email?: string; + + constructor() { + this.login = ''; + this.username = ''; + this.name = ''; + this.email = ''; + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.css new file mode 100644 index 000000000..c737799ab --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.css @@ -0,0 +1,23 @@ +.group-scrollable { + min-height: 140px; + max-height: 140px; + overflow-y: scroll; +} + +li.group { + padding: 3px 0px 3px 5px; + margin-top: 2px; + margin-bottom: 2px; +} + +li.group-selected { + margin-top: 1px; + margin-bottom: 1px; + background-color: #def3ff; + border-top-color: #bee1f4; + border-bottom-color: #bee1f4; + border-top-style: solid; + border-bottom-style: solid; + border-top-width: 1px; + border-bottom-width: 1px; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.html new file mode 100644 index 000000000..f6ba8b46d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.html @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.ts new file mode 100644 index 000000000..930dcd733 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/_components/groups-management.dialog.ts @@ -0,0 +1,120 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { NotificationService, NotificationType } from '../../../../components/patternfly-ng/notification'; + +import { UsersService } from '../../../../services/users.service'; + +export type KeycloakUser = { + id: string; + username: string; +} + +@Component({ + selector: 'app-groups-management-dialog', + templateUrl: './groups-management.dialog.html', + styleUrls: ['./groups-management.dialog.css'], + imports: [ + CommonModule + ] +}) +export class GroupsManagementDialogComponent implements OnInit { + + closeBtnName?: string; + + //user: User; + user!: KeycloakUser; + userGroups!: any[]; + groups!: any[]; + availableGroups: any[] = []; + + memberSelected: any[] = []; + availableSelected: any[] = []; + + constructor(public usersSvc: UsersService, public bsModalRef: BsModalRef, private notificationService: NotificationService) {} + + ngOnInit() { + this.computeAvailableGroups(); + } + + computeAvailableGroups(): void { + for (const group of this.groups) { + if (this.userGroups.map(g => g.id).indexOf(group.id) == -1) { + this.availableGroups.push(group); + } + } + } + + isMemberGroupSelected(group: any): boolean { + return this.memberSelected.indexOf(group) != -1; + } + isAvailableGroupSelected(group: any): boolean { + return this.availableSelected.indexOf(group) != -1; + } + + toggleMember(group: any): void { + this.isMemberGroupSelected(group) ? + this.memberSelected.splice(this.memberSelected.indexOf(group), 1) : this.memberSelected.push(group); + } + toggleAvailable(group: any): void { + this.isAvailableGroupSelected(group) ? + this.availableSelected.splice(this.availableSelected.indexOf(group), 1) : this.availableSelected.push(group); + } + + leaveSelectedGroups(): void { + for (const memberOfGroup of this.memberSelected) { + this.usersSvc.removeUserFromGroup(this.user.id, memberOfGroup.id).subscribe( + { + next: res => { + this.userGroups.splice(this.userGroups.indexOf(memberOfGroup), 1); + this.availableGroups.push(memberOfGroup); + this.notificationService.message(NotificationType.SUCCESS, + this.user.username, this.user.username + ' is no longer member of ' + memberOfGroup.path, false); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + this.user.username, this.user.username + ' cannot leave ' + memberOfGroup.path + ' (' + err.message + ')', false); + }, + complete: () => console.log('Observer got a complete notification'), + } + ); + } + this.memberSelected = []; + } + joinSelectedGroups(): void { + for (const availableGroup of this.availableSelected) { + this.usersSvc.putUserInGroup(this.user.id, availableGroup.id).subscribe( + { + next: res => { + this.availableGroups.splice(this.availableGroups.indexOf(availableGroup), 1); + this.userGroups.push(availableGroup); + this.notificationService.message(NotificationType.SUCCESS, + this.user.username, this.user.username + ' is now member of ' + availableGroup.path, false); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + this.user.username, this.user.username + ' cannot join ' + availableGroup.path + ' (' + err.message + ')', false); + }, + complete: () => {} //console.log('Observer got a complete notification'), + } + ); + } + this.availableSelected = []; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.css new file mode 100644 index 000000000..3aee799b8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.css @@ -0,0 +1,7 @@ +.secretName { + flex-basis: 40%; + max-width: 200px; +} +.list-view-pf-description { + flex-basis: 80%; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.html new file mode 100644 index 000000000..373ff448a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.html @@ -0,0 +1,152 @@ + +Manage the Authentication options available for different importers. + +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
{{ secret.name }}
+
{{ secret.description }}
+
+
+
+ Usages +
+
+
+
+ +
+
+
+
+ + +
+ +
+
+
+ +
+ '{{ secret.name }}' Secret + New Secret +
+
+
+ +
+ +

The name this secret will have for later referencing it (should be unique).

+
+
+
+ +
+ +

The description of this secret.

+
+
+
+ +
+ +

The type of Authentication if required.

+
+
+
+ +
+ +

The username information for authentication.

+
+
+
+ +
+ +

The credentials information for authentication.

+
+
+
+ +
+ +

The token to send to remote repository.

+
+
+
+ +
+ +

The header to use for sending token. If not specified, assume it is the + Authorization header set to Bearer: ABOVE_TOKEN_VALUE

+
+
+
+ +
+ +

The PEM format CA certificate chain. Copy/Paste here from the clipboard.

+
+
+
+
+   + +
+
+
+
+
+
+ + +

Do you really want to delete the selected Secret ?

+ +
+ + NOTE : + This will permanently delete the Secret from Microcks. All Importers using this Secret may be + broken ! This operation cannot be undone. +
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.ts new file mode 100644 index 000000000..4d1f47413 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/secrets.tab.ts @@ -0,0 +1,209 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; + +import { NotificationService, NotificationType } from '../../../components/patternfly-ng/notification'; +import { PaginationConfig, PaginationModule, PaginationEvent } from '../../../components/patternfly-ng/pagination'; +import { ToolbarConfig, ToolbarModule } from '../../../components/patternfly-ng/toolbar'; +import { + FilterConfig, + FilterEvent, + FilterField, + FilterType +} from '../../../components/patternfly-ng/filter'; + +import { ConfirmDeleteDialogComponent } from '../../../components/confirm-delete/confirm-delete.component'; + +import { Secret } from '../../../models/secret.model'; +import { SecretsService } from '../../../services/secrets.service'; + +@Component({ + selector: 'app-secrets-tab', + templateUrl: './secrets.tab.html', + styleUrls: ['./secrets.tab.css'], + imports: [ + CommonModule, + ConfirmDeleteDialogComponent, + BsDropdownModule, + FormsModule, + PaginationModule, + ToolbarModule, + ] +}) +export class SecretsTabComponent implements OnInit { + + secrets?: Secret[]; + secretsCount: number = 0; + toolbarConfig: ToolbarConfig = new ToolbarConfig; + filterConfig: FilterConfig = new FilterConfig; + paginationConfig: PaginationConfig = new PaginationConfig; + filterTerm: string | null = null; + filtersText = ''; + + secret: Secret = {} as Secret; + createOrUpdateBtn = 'Create'; + authenticationType?: string; + + constructor(private secretsSvc: SecretsService, private notificationService: NotificationService) {} + + ngOnInit() { + this.getSecrets(); + this.countSecrets(); + this.paginationConfig = { + pageNumber: 1, + pageSize: 20, + pageSizeIncrements: [], + totalItems: 20 + } as PaginationConfig; + + this.filterConfig = { + fields: [{ + id: 'name', + title: 'Name', + placeholder: 'Filter by Name...', + type: FilterType.TEXT + }] as FilterField[], + resultsCount: 20, + appliedFilters: [] + } as FilterConfig; + + this.toolbarConfig = { + actionConfig: undefined, + filterConfig: this.filterConfig, + sortConfig: undefined, + views: [] + } as ToolbarConfig; + } + + getSecrets(page: number = 1): void { + this.secretsSvc.getSecrets(page).subscribe(results => this.secrets = results); + } + filterSecrets(filter: string): void { + this.secretsSvc.filterSecrets(filter).subscribe(results => { + this.secrets = results; + this.filterConfig.resultsCount = results.length; + }); + } + + countSecrets(): void { + this.secretsSvc.countSecrets().subscribe(results => { + this.secretsCount = results.counter; + this.paginationConfig.totalItems = this.secretsCount; + }); + } + + handlePageSize($event: PaginationEvent) { + // this.updateItems(); + } + + handlePageNumber($event: PaginationEvent) { + this.getSecrets($event.pageNumber); + } + + handleFilter($event: FilterEvent): void { + this.filtersText = ''; + if (!$event.appliedFilters || $event.appliedFilters.length == 0) { + this.filterTerm = null; + this.getSecrets(); + } else { + $event.appliedFilters.forEach((filter) => { + this.filtersText += filter.field.title + ' : ' + filter.value + '\n'; + this.filterTerm = filter.value; + }); + this.filterSecrets(this.filterTerm!); + } + } + + editSecret(secret: Secret): void { + this.secret = secret; + this.createOrUpdateBtn = 'Update'; + if (secret.username != null && secret.password != null) { + this.authenticationType = 'basic'; + } else if (secret.token != null) { + this.authenticationType = 'token'; + } + } + resetEditedSecret(): void { + this.secret = {} as Secret; + this.createOrUpdateBtn = 'Create'; + } + + saveOrUpdateSecret(secret: Secret): void { + if (secret.id) { + this.secretsSvc.updateSecret(secret).subscribe( + { + next: res => { + this.notificationService.message(NotificationType.SUCCESS, + secret.name, 'Secret has been updated', false); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + secret.name, 'Secret cannot be updated (' + err.message + ')', false); + }, + complete: () => {} //console.log('Observer got a complete notification'), + } + ); + } else { + this.secretsSvc.createSecret(secret).subscribe( + { + next: res => { + this.notificationService.message(NotificationType.SUCCESS, + secret.name, 'Secret has been created', false); + this.secret = {} as Secret; + this.createOrUpdateBtn = 'Create'; + this.getSecrets(); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + secret.name, 'Secret cannot be created (' + err.message + ')', false); + }, + complete: () => {} //console.log('Observer got a complete notification'), + } + ); + } + } + + deleteSecret(secret: Secret): void { + this.secretsSvc.deleteSecret(secret).subscribe( + { + next: res => { + this.notificationService.message(NotificationType.SUCCESS, + secret.name, 'Secret has been deleted', false); + this.getSecrets(); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + secret.name, 'Secret cannot be deleted (' + err.message + ')', false); + }, + complete: () => {} //console.log('Observer got a complete notification'), + } + ); + } + + updateSecretProperties() { + if (this.authenticationType === 'basic') { + this.secret.token = undefined; + this.secret.tokenHeader = undefined; + } else if (this.authenticationType === 'token') { + this.secret.username = undefined; + this.secret.password = undefined; + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/snapshots.tab.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/snapshots.tab.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/snapshots.tab.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/snapshots.tab.html new file mode 100644 index 000000000..a54334c37 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/snapshots.tab.html @@ -0,0 +1,52 @@ + +Upload and import a previously exported JSON repository snapshot. + +
+
+ +
+ + + +Choose APIs & Services you want to export as JSON file among this {{ servicesCount}} ones. + +
+
+
+
    +
  • + +
  • +
+
+
+
    +
  • + +
  • +
+
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/snapshots.tab.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/snapshots.tab.ts new file mode 100644 index 000000000..48502b065 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/snapshots.tab.ts @@ -0,0 +1,113 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { NotificationService, NotificationType, } from '../../../components/patternfly-ng/notification'; +import { FileUploader, FileUploadModule } from 'ng2-file-upload'; + +import { Service } from '../../../models/service.model'; +import { ServicesService } from '../../../services/services.service'; +import { IAuthenticationService } from '../../../services/auth.service'; + +@Component({ + selector: 'app-snapshots-tab', + templateUrl: './snapshots.tab.html', + styleUrls: ['./snapshots.tab.css'], + imports: [ + CommonModule, + FormsModule, + FileUploadModule + ] +}) +export class SnapshotsTabComponent implements OnInit { + + halfServices?: Service[]; + secondHalfServices?: Service[]; + servicesCount: number = 0; + + selectedServices: any = { ids: {} }; + uploader: FileUploader = new FileUploader({url: '/api/import', itemAlias: 'file'}); + + constructor( + private servicesSvc: ServicesService, + private authService: IAuthenticationService, + private notificationService: NotificationService + ) {} + + ngOnInit() { + this.getAllServices(); + this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; }; + this.uploader.onCompleteItem = (item: any, response: any, status: any, headers: any) => { + if (status == 201) { + this.notificationService.message(NotificationType.SUCCESS, + 'Snapshot', 'File uploaded successfully', false); + } else { + this.notificationService.message(NotificationType.DANGER, + 'Snapshot', 'File uploaded failed with status ' + status, false); + } + }; + this.uploader.authToken = 'Bearer ' + this.authService.getAuthenticationSecret(); + } + + getAllServices(): void { + this.servicesSvc.getServices(1, 1000).subscribe( + results => { + this.halfServices = results.slice(0, (results.length / 2) + 1); + this.secondHalfServices = results.slice((results.length / 2) + 1, results.length); + this.servicesCount = results.length; + } + ); + } + + public createExport(): void { + let downloadPath = '/api/export?'; + Object.keys(this.selectedServices.ids).forEach(function(element, index, array) { + downloadPath += '&serviceIds=' + element; + }); + + // Just opening a window with the download path is not working + // because Authorization header is not sent. + //window.open(downloadPath, '_blank', ''); + + // So we have to use XMLHttpRequest to send Authorization header and get the file + // before triggering the Save as dialog by simulating a click on a link. + const xhr = new XMLHttpRequest(); + xhr.open('GET', location.origin + downloadPath, true); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.setRequestHeader('Authorization', 'Bearer ' + this.authService.getAuthenticationSecret()); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + const blob = new Blob([xhr.response], { type: 'text/plain' }); + const url = window.URL.createObjectURL(blob); + + var a = document.createElement("a"); + document.body.appendChild(a); + a.href = url; + a.download = 'microcks-repository.json'; + a.click(); + + window.URL.revokeObjectURL(url); + } else { + alert('Problem while retrieving snapshot export'); + } + } + }; + xhr.send(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.css new file mode 100644 index 000000000..3f84a62e9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.css @@ -0,0 +1,22 @@ +span.label { + margin-left: 4px !important; + margin-right: 4px !important; + padding-top: 0.6em; + padding-bottom: 0.6em; + border-radius: .25rem; +} +span.label a { + color: #ffffff; +} + +.userName { + flex-basis: 40%; + max-width: 150px; +} +.userFullName { + flex-basis: 40%; + max-width: 200px; +} +.list-view-pf-description { + flex-basis: 40%; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.html new file mode 100644 index 000000000..5fc17090c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.html @@ -0,0 +1,57 @@ + +Manage the users of the Keycloak / RH-SSO realm, retrieving and assigning them roles. + +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
{{user.username }}
+
{{ kUser(user).firstName }} {{ kUser(user).lastName }}
+
+
+
+ {{ user.email }} +
+
+ + Registred + Manager + Admin +
+
+
+ +
+
+ + + +
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.ts new file mode 100644 index 000000000..8d4ba52f0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/_components/users.tab.ts @@ -0,0 +1,361 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { forkJoin } from 'rxjs'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal'; + +import { NotificationService, NotificationType } from '../../../components/patternfly-ng/notification'; +import { PaginationConfig, PaginationModule, PaginationEvent } from '../../../components/patternfly-ng/pagination'; +import { ToolbarConfig, ToolbarModule } from '../../../components/patternfly-ng/toolbar'; +import { + FilterConfig, + FilterEvent, + FilterField, + FilterType, +} from '../../../components/patternfly-ng/filter'; + +import { User } from '../../../models/user.model'; +import { IAuthenticationService } from '../../../services/auth.service'; +import { ConfigService } from '../../../services/config.service'; +import { ServicesService } from '../../../services/services.service'; +import { UsersService } from '../../../services/users.service'; +import { GroupsManagementDialogComponent } from './_components/groups-management.dialog'; + +type KeycloakUser = { + id: string; + username: string; + firstName: string; + lastName: string; +} + +@Component({ + selector: 'app-users-tab', + templateUrl: './users.tab.html', + styleUrls: ['./users.tab.css'], + imports: [ + CommonModule, + BsDropdownModule, + PaginationModule, + ToolbarModule, + ] +}) +export class UsersTabComponent implements OnInit { + + modalRef?: BsModalRef; + allowedToManageUsers = true; + users?: User[]; + usersCount: number = 0; + usersRoles: Record = {}; + groups?: any[]; + managerGroup: any; + tenants?: string[]; + toolbarConfig: ToolbarConfig = new ToolbarConfig; + filterConfig: FilterConfig = new FilterConfig; + paginationConfig: PaginationConfig = new PaginationConfig; + filterTerm: string | null = null; + filtersText = ''; + + constructor( + private usersSvc: UsersService, + protected authService: IAuthenticationService, + private config: ConfigService, + private servicesSvc: ServicesService, + private modalService: BsModalService, + private notificationService: NotificationService + ) {} + + ngOnInit() { + if (this.hasRepositoryTenancyEnabled()) { + this.getAndUpdateGroups(); + } + this.getUsers(); + this.countUsers(); + this.paginationConfig = { + pageNumber: 1, + pageSize: 20, + pageSizeIncrements: [], + totalItems: 20, + } as PaginationConfig; + + this.filterConfig = { + fields: [ + { + id: 'name', + title: 'Name', + placeholder: 'Filter by Name...', + type: FilterType.TEXT, + }, + ] as FilterField[], + resultsCount: 20, + appliedFilters: [], + } as FilterConfig; + + this.toolbarConfig = { + actionConfig: undefined, + filterConfig: this.filterConfig, + sortConfig: undefined, + views: [], + } as ToolbarConfig; + } + + getAndUpdateGroups(): void { + this.usersSvc.getGroups().subscribe({ + next: (results) => { + // Flatten the groups. + this.groups = results + .filter((group) => group.path === '/microcks') + .flatMap((group) => group.subGroups); + // We may a direct array of subGroups because of using the new populateHierarchy=false flag. + if (this.groups.length == 0 || (this.groups.length == 1 && this.groups[0].subGroups.length == 0)) { + this.groups = results.filter((group) => + group.path.startsWith('/microcks/manager') + ); + } + this.groups.forEach((group) => { + if (group.path === '/microcks/manager') { + this.managerGroup = group; + } + }); + + // Flatten once again if necessary. + this.groups = this.groups.flatMap((group) => { + if (group.subGroups.length > 0) { + return group.subGroups; + } else { + return group; + } + }); + this.checkGroupsCompleteness(); + }, + error: (err) => { + if (err.status == 403) { + this.notificationService.message( + NotificationType.DANGER, + 'Authorization Error', + 'Current user does not appear to have the **manage-groups** role from **realm-management** client. Please contact your administrator to setup correct role.', + false + ); + } else { + this.notificationService.message( + NotificationType.WARNING, + 'Unknown Error', + err.message, + false + ); + } + }, + }); + } + checkGroupsCompleteness(): void { + this.servicesSvc.getServicesLabels().subscribe((results) => { + this.tenants = results[this.repositoryFilterFeatureLabelKey()]; + // Check that each tenant has correct groups, otherwise create them. + this.tenants!.forEach((tenant) => { + const mGroup = this.groups?.find( + (g) => g.path === '/microcks/manager/' + tenant + ); + if (mGroup == null) { + this.usersSvc.createGroup(this.managerGroup.id, tenant).subscribe(); + } + }); + }); + } + + kUser(user: User): KeycloakUser { + return user as KeycloakUser; + } + + getUsers(page: number = 1): void { + this.usersSvc.getUsers(page).subscribe({ + next: (results) => { + this.users = results; + this.usersRoles = {}; + }, + error: (err) => { + if (err.status == 403) { + this.notificationService.message( + NotificationType.DANGER, + 'Authorization Error', + 'Current user does not appear to have the **manage-users** role from **realm-management** client. Please contact your administrator to setup correct role.', + false + ); + } else { + this.notificationService.message( + NotificationType.WARNING, + 'Unknown Error', + err.message, + false + ); + } + }, + }); + } + filterUsers(filter: string): void { + this.usersSvc.filterUsers(filter).subscribe((results) => { + this.users = results; + this.usersRoles = {}; + this.filterConfig.resultsCount = results.length; + }); + } + + countUsers(): void { + this.usersSvc.countUsers().subscribe((results) => { + this.usersCount = results; + this.paginationConfig.totalItems = this.usersCount; + }); + } + + getUserRoles(userId: string): void { + const userRoles = this.usersSvc.getUserRoles(userId); + const userRealmRoles = this.usersSvc.getUserRealmRoles(userId); + + forkJoin([userRoles, userRealmRoles]).subscribe({ + next: (results) => { + this.usersRoles[userId] = results[0]; + this.usersRoles[userId].push(...results[1]); + console.log(JSON.stringify(this.usersRoles[userId])); + }, + error: (err) => { + if (err.status == 403) { + this.notificationService.message( + NotificationType.DANGER, + 'Authorization Error', + 'Current user does not appear to have the **manage-clients** role from **realm-management** client. Please contact your administrator to setup correct role.', + false + ); + } else { + this.notificationService.message( + NotificationType.WARNING, + 'Unknown Error', err.message, + false + ); + } + }, + }); + } + userHasRole(userId: string, expectedRole: string): boolean { + if (expectedRole === 'user') { + expectedRole = 'default-roles-' + this.usersSvc.getRealmName(); + } + if (this.usersRoles[userId] === undefined) { + return false; + } + for (const role of this.usersRoles[userId]) { + if (expectedRole === role.name) { + return true; + } + } + return false; + } + + assignRoleToUser(userId: string, userName: string, role: string) { + this.usersSvc.assignRoleToUser(userId, role).subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + userName, userName + ' is now ' + role, + false + ); + this.getUserRoles(userId); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + userName, userName + ' cannot be made ' + role + ' (' + err.message + ')', + false + ); + }, + complete: () => {} //console.log('Observer got a complete notification'), + }); + } + removeRoleFromUser(userId: string, userName: string, role: string) { + this.usersSvc.removeRoleFromUser(userId, role).subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + userName, userName + ' is no more ' + role, + false + ); + this.getUserRoles(userId); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + userName, userName + ' cannot be downgraded ' + role + ' (' + err.message + ')', + false + ); + }, + complete: () => {} //console.log('Observer got a complete notification'), + }); + } + + openGroupsManagementDialog(user: User): void { + this.usersSvc.getUserGroups((user as any).id).subscribe((userGroups) => { + const initialState = { + user: { + id: (user as any).id, + username: (user as any).username, + }, + userGroups: userGroups.filter((group) => + group.path.startsWith('/microcks/manager/') + ), + groups: this.groups, + }; + this.modalRef = this.modalService.show(GroupsManagementDialogComponent, { + initialState, + }); + this.modalRef.content.closeBtnName = 'Close'; + }); + } + + handlePageSize($event: PaginationEvent) { + // this.updateItems(); + } + + handlePageNumber($event: PaginationEvent) { + this.getUsers($event.pageNumber); + } + + handleFilter($event: FilterEvent): void { + this.filtersText = ''; + if (!$event.appliedFilters || $event.appliedFilters.length == 0) { + this.filterTerm = null; + this.getUsers(); + } else { + $event.appliedFilters.forEach((filter) => { + this.filtersText += filter.field.title + ' : ' + filter.value + '\n'; + this.filterTerm = filter.value; + }); + this.filterUsers(this.filterTerm!); + } + } + + public hasRepositoryTenancyEnabled(): boolean { + return this.config.hasFeatureEnabled('repository-filter'); + } + public repositoryTenantLabel(): string { + return this.config + .getFeatureProperty('repository-filter', 'label-key') + .toLowerCase(); + } + + public repositoryFilterFeatureLabelKey(): string { + return this.config.getFeatureProperty('repository-filter', 'label-key'); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/admin.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/admin.page.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/admin.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/admin.page.html new file mode 100644 index 000000000..50fe57c83 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/admin.page.html @@ -0,0 +1,22 @@ +
+ + +
+ +

Administration

+Use this section to manage your Users, export and import repository Snapshots and manage + Secrets. + +

+ + + + + + + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/admin.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/admin.page.ts new file mode 100644 index 000000000..5bd8a881d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/admin/admin.page.ts @@ -0,0 +1,56 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; + +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { + Notification, + NotificationEvent, + NotificationService, + ToastNotificationListComponent +} from '../../components/patternfly-ng/notification'; + +import { SecretsTabComponent } from './_components/secrets.tab'; +import { UsersTabComponent } from './_components/users.tab'; +import { SnapshotsTabComponent } from './_components/snapshots.tab'; + +@Component({ + selector: 'app-admin-page', + templateUrl: './admin.page.html', + styleUrls: ['./admin.page.css'], + imports: [ + UsersTabComponent, + SecretsTabComponent, + SnapshotsTabComponent, + TabsModule, + ToastNotificationListComponent + ] +}) +export class AdminPageComponent implements OnInit { + + notifications: Notification[] = []; + + constructor(private notificationService: NotificationService) {} + + ngOnInit() { + this.notifications = this.notificationService.getNotifications(); + } + + handleCloseNotification($event: NotificationEvent): void { + this.notificationService.remove($event.notification); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.css new file mode 100644 index 000000000..1294743fd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.css @@ -0,0 +1,29 @@ +h1.arvo { + font-size: 2.4em; + line-height: 1.2em; +} +.hl { + color: #ff1745; + font-weight: 600; +} + +.megatron { + padding: 40px; + padding-bottom: 20px; + margin-bottom: 20px; + color: inherit; + background-color: #f1f1f1; +} +.megatron p { + font-weight: 200; +} + +blockquote.top-invocation { + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: 4px; +} +p.top-invocation { + margin-bottom: 0; + font-size: 0.85em; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.html new file mode 100644 index 000000000..ddf71057a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.html @@ -0,0 +1,87 @@ +
+ +
+
+
+
+

The communication and runtime tool for your APIs & + Micro-services Mocks +

+
+
+ + Microcks +
+
+
+ +
+ +
+
Browse, get informations and request/response mocks on Microcks managed APIs & Services. +
+
+
+ Importers +
+
+
Visualize, create or edit a new Importer Job for filling Microcks with mocks and samples. +
+
+
+
+
+ +
+
+
+ + + +
+
+ +
    +
  1. +
    +

    {{ stat.serviceName }} - {{ stat.serviceVersion }}

    + with {{ stat.dailyCount }} invocations +
    +
  2. +
+
+
+
+ + + +
+
+
+ +
+
+
+ + + +
+
+ + + +
+
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.ts new file mode 100644 index 000000000..bb7a70370 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/dashboard/dashboard.page.ts @@ -0,0 +1,309 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnInit, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + CardConfig, + CardFilter, + CardModule, +} from '../../components/patternfly-ng/card'; +import { DonutChartConfig, DonutChartModule } from '../../components/patternfly-ng/chart'; +import { SparklineChartData, SparklineChartConfig, SparklineChartModule } from '../../components/patternfly-ng/chart/sparkline-chart'; + +import { ScoreTreemapComponent } from '../../components/score-treemap/score-treemap.component'; + +import { ConfigService } from '../../services/config.service'; +import { MetricsService } from '../../services/metrics.service'; +import { ServicesService } from '../../services/services.service'; + +@Component({ + selector: 'app-dashboard-page', + templateUrl: 'dashboard.page.html', + styleUrls: ['dashboard.page.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + CardModule, + DonutChartModule, + ScoreTreemapComponent, + SparklineChartModule + ] +}) +export class DashboardPageComponent implements OnInit { + aDayLong: number = 1000 * 60 * 60 * 24; + today = new Date(); + todayStr: string; + + servicesCount = 0; + aggregatesCount = 0; + + chartCardConfig: CardConfig = { + action: { + hypertext: 'View All Events', + iconStyleClass: 'fa fa-flag', + }, + filters: [ + { title: 'Last 50 Days', value: '50' }, + { default: true, title: 'Last 20 Days', value: '20' }, + { title: 'Last 10 Days', value: '10' } + ], + title: 'APIs | Services Mocks Invocations', + } as CardConfig; + + topCardConfig: CardConfig = { + filters: [ + { default: true, title: 'Today', value: 'today' }, + { title: 'Yesterday', value: 'yesterday' } + ], + title: 'Most Used APIs | Services', + } as CardConfig; + + repositoryCardConfig: CardConfig = { + title: 'APIs | Services Repository', + } as CardConfig; + + testConformanceCardConfig: CardConfig = { + title: 'API | Services Conformance Risks', + } as CardConfig; + + testResultsCardConfig: CardConfig = { + filters: [ + { default: true, title: 'Last 7 Days', value: '7' }, + { title: 'Last 15 Days', value: '15', } + ], + title: 'API | Services Tests', + } as CardConfig; + + actionsText = ''; + chartDates: any[] = ['dates']; + chartConfig: SparklineChartConfig = { + chartId: 'invocationsSparkline', + chartHeight: 150, + tooltipType: 'default', + }; + chartData: SparklineChartData = { + dataAvailable: false, + total: 100, + xData: this.chartDates, + yData: ['used'], + }; + + repositoryDonutChartData: any[] = [ + ['REST', 0], + ['DIRECT', 0], + ['SOAP', 0], + ['EVENT', 0], + ['GRAPH', 0], + ['GRPC', 0], + ]; + repositoryDonutChartConfig: DonutChartConfig = { + chartId: 'repositoryDonut', + chartHeight: 220, + colors: { + REST: '#89bf04', + DIRECT: '#9c27b0', + SOAP: '#39a5dc', + EVENT: '#ec7a08', + GRAPH: '#e10098', + GRPC: '#379c9c', + }, + /* + data: { + onclick: (data: any, element: any) => { + alert('You clicked on donut arc: ' + data.id); + } + }, + */ + donut: { title: 'APIs & Services' }, + legend: { show: true }, + }; + + testResultsDonutChartData: any[] = [ + ['SUCCESS', 3], + ['FAILURE', 5], + ]; + testResultsDonutChartConfig: DonutChartConfig = { + chartId: 'testsDonut', + chartHeight: 220, + colors: { + SUCCESS: '#7bb33d', + FAILURE: '#d1d1d1', + }, + donut: { title: 'Tests' }, + legend: { show: true }, + }; + + topInvocations: any; //DailyInvocations[]; + conformanceScores: any; + + constructor( + private servicesSvc: ServicesService, + private config: ConfigService, + private metricsSvc: MetricsService, + private ref: ChangeDetectorRef + ) { + this.todayStr = metricsSvc.formatDayDate(this.today); + } + + ngOnInit() { + this.getServicesMap(); + this.getTopInvocations(); + this.getInvocationsTrend(); + this.getAggregatedTestConformanceMetrics(); + this.getLatestTestsTrend(); + } + + isRepositoryPanelDisplayed(): boolean { + return this.servicesCount > 1; + } + isTestsPanelDisplayed(): boolean { + return this.aggregatesCount > 1; + } + + getServicesMap(): void { + this.servicesSvc.getServicesMap().subscribe((results) => { + this.servicesCount = Object.keys(results).length; + this.repositoryDonutChartData = [ + ['REST', 0], + ['DIRECT', 0], + ['SOAP', 0], + ['EVENT', 0], + ['GRPC', 0], + ['GRAPH', 0], + ]; + let directCount = 0; + for (const key in results) { + if (key === 'GENERIC_REST' || key === 'GENERIC_EVENT') { + directCount += results[key]; + this.addServiceCountToDonutTuple('DIRECT', results[key]); + } else if (key === 'SOAP_HTTP') { + this.addServiceCountToDonutTuple('SOAP', results[key]); + } else if (key === 'GRAPHQL') { + this.addServiceCountToDonutTuple('GRAPH', results[key]); + } else { + this.addServiceCountToDonutTuple(key, results[key]); + } + } + this.ref.detectChanges(); + }); + } + + private addServiceCountToDonutTuple(tupleName: string, results: number): void { + let tuple = this.repositoryDonutChartData.find((tuple) => tuple[0] === tupleName); + if (tuple) { + tuple[1] += results; + } + } + + getTopInvocations(day: Date = this.today): void { + this.metricsSvc.getTopInvocations(day).subscribe((results) => { + this.topInvocations = results.slice(0, 3); + this.ref.detectChanges(); + }); + } + + getInvocationsTrend(limit: number = 20): void { + this.metricsSvc.getInvocationsStatsTrend(limit).subscribe((results) => { + this.chartData.dataAvailable = false; + this.chartData.xData = ['dates']; + this.chartData.yData = ['hits']; + for (let i = limit - 1; i >= 0; i--) { + const pastDate: Date = new Date( + this.today.getTime() - i * this.aDayLong + ); + this.chartData.xData.push(pastDate); + const pastDateStr = this.metricsSvc.formatDayDate(pastDate); + const result = results[pastDateStr]; + if (result == null || result == undefined) { + this.chartData.yData.push(0); + } else { + this.chartData.yData.push(result); + } + } + this.chartData.dataAvailable = true; + this.ref.detectChanges(); + }); + } + + getAggregatedTestConformanceMetrics(): void { + this.metricsSvc + .getAggregatedTestConformanceMetrics() + .subscribe((results) => { + this.aggregatesCount = results.length; + const children = results.map(function(metric) { + return { + name: metric.name, + value: metric.weight, + score: metric.value, + }; + }); + this.conformanceScores = { + name: 'root', + children: [{ name: 'domains', children, score: 1 }], + }; + this.ref.detectChanges(); + }); + } + + getLatestTestsTrend(limit: number = 7): void { + this.metricsSvc.getLatestTestsTrend(limit).subscribe((results) => { + let successCount = 0; + let failureCount = 0; + results.forEach((result) => { + result.success ? successCount++ : failureCount++; + }); + const ratio = successCount / results.length; + if (ratio > 0.66) { + this.testResultsDonutChartConfig.colors.SUCCESS = '#7bb33d'; + } else if (ratio < 0.33) { + this.testResultsDonutChartConfig.colors.SUCCESS = '#dd1f26'; + } else { + this.testResultsDonutChartConfig.colors.SUCCESS = '#efaa00'; + } + this.testResultsDonutChartData = [ + ['SUCCESS', successCount], + ['FAILURE', failureCount], + ]; + this.ref.detectChanges(); + }); + } + + handleChartFilterSelect($event: CardFilter): void { + this.getInvocationsTrend(+$event.value); + } + + handleTopFilterSelect($event: CardFilter): void { + if ($event.value === 'yesterday') { + this.getTopInvocations(new Date(this.today.getTime() - this.aDayLong)); + } else { + this.getTopInvocations(); + } + } + + handleTestsFilterSelect($event: CardFilter): void { + this.getLatestTestsTrend(+$event.value); + } + + repositoryFilterFeatureLabelKey(): string { + return this.config.getFeatureProperty('repository-filter', 'label-key'); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.css new file mode 100644 index 000000000..8475289a9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.css @@ -0,0 +1,276 @@ +/* New specific styles */ + +.mh-header-bar { + background-color: #4E5D6C !important; + margin-left: -20px; + margin-right: -20px; + margin-top: -20px; + padding: 10px; + height: 40px; +} + +/* Overriden style from styles.css, header-bar.component.css or home.page.css */ + +.container-sm { + width: 100% !important; +} +@media (min-width: 1400px) { + .container-sm { + width: 100% !important; + } +} + +.mh-header-bar-inner_logo { + padding-left: 30px !important; +} + +.mh-page_filters { + width: 260px !important; +} + +/* Global styles coming from styles.css */ + +.mh-page-contents { + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; + width: 100%; + min-height: calc(100vh - 700px); +} +.mh-page-contents_inner { + background: transparent; + display: flex; + padding: 20px 15px 0 15px; +} + +.mh-external-link.indicator { + padding-right: 15px; +} +.mh-external-link.block { + display: block; +} + +@media (min-width: 1400px) { + .container-sm { + /*max-width: 1220px;*/ + } +} + +/* Specific styles comming from header-bar.component.css */ + +.mh-header-bar-inner_logo { + height: 20px; + text-shadow: 0 1px 1px rgba(0,0,0,.2); + margin-bottom: 30px; +} + +/* Specific styles comming from home.page.css */ + +.mh-page_filters { + width: 220px; + margin: 0 20px 0 0; + display: none; +} +@media (min-width: 480px) { + .mh-page_filters { + display: block; + } +} + +.filter-panel-pf { + margin: 0 0 30px; + padding: 0 15px; +} +.filter-panel-pf-category { + margin-top: 20px; +} +.filter-panel-pf-category:first-of-type { + margin-top: 0; +} +.filter-panel-pf-category-title { + border: 0; + color: #8b8d8f; + font-size: inherit; + margin: 0; + text-transform: uppercase; +} +.mh-page_filters .filter-panel-pf-category-title { + margin-bottom: 10px; + color: #b6b6b6; + font-size: 10px; + font-weight: 700; + letter-spacing: 1px; +} +.filter-panel-pf-category-items { + margin-top: 5px !important; + margin-bottom: 0 !important; +} +.mh-category-item { + border-radius: 4px; + display: flex; + width: 100%; +} +.mh-category-item.selected, .mh-category-item:hover { + background-color: #eaeaea; + +} +.mh-category-item>button { + background: transparent; + border: none; + padding: 2px 5px; + text-align: left; +} +.mh-category-item>button:focus { + outline: none; + text-decoration: underline; +} +.mh-category-item_select { + flex: 1; +} +.mh-category-item.selected .mh-category-item_select, .mh-category-item:hover .mh-category-item_select { + color: #df691a; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +.filter-panel-pf-category-item { + font-weight: 400; + margin-bottom: 0; + margin-top: 5px; +} +.filter-panel-pf-category-item:first-of-type { + margin-top: 0; +} +.filter-panel-pf-category-item .item-count { + padding-left: 3px; +} + +.filter-panel-pf-category-item .checkbox { + margin: 0; +} +.checkbox, .radio { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.checkbox label, .radio label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: 400; + cursor: pointer; + display: inline-block; + max-width: 100%; +} +.checkbox-inline input[type=checkbox], .checkbox input[type=checkbox], .radio-inline input[type=radio], .radio input[type=radio] { + position: absolute; + margin-left: -20px; +} +input[type=checkbox], input[type=radio] { + margin: 4px 0 0; + line-height: normal; + box-sizing: border-box; + padding: 0; +} + +.mh-page_toolbar { + border-bottom: 1px solid #eaeaea; + display: flex; + flex-direction: row; + margin-bottom: 20px; + padding: 0 0 20px; +} +.mh-page_toolbar-item-label { + border: 0; + font-size: inherit; + margin: 0; + text-transform: uppercase; + margin-bottom: 10px; + color: #b6b6b6; + font-size: 10px; + font-weight: 700; + letter-spacing: 1px; +} + +.mh-page_contents .catalog-tile-view-pf-no-categories { + flex-direction: column; + align-items: center; + margin-right: -15px; +} + +.catalog-tile-view-pf-no-categories { + display: flex; + flex-wrap: wrap; +} +@media (min-width: 480px) { + .mh-page_contents .catalog-tile-view-pf-no-categories { + flex-direction: row; + } +} + +.catalog-tile-pf { + border: none; + border-radius: 5px; + box-shadow: 0 3px 6px rgba(0,0,0,.1); +} +.catalog-tile-pf { + background: #fff; + border: 1px solid hsla(0,0%,82%,.75); + color: inherit; + display: flex; + flex: 0 0 auto; + flex-direction: column; + height: 230px; + margin: 0 15px 15px 0; + padding: 15px; + width: 220px; +} + +.catalog-tile-pf-icon { + font-size: 40px; + height: 40px; + max-width: 80px; + min-width: 40px; +} +.catalog-tile-pf-header { + display: flex; + flex-shrink: 0; + font-size: 16px; + font-weight: 400; +} +.catalog-tile-pf-body { + display: flex; + flex: 1; + flex-direction: column; + margin-top: 15px; + min-height: 0; +} +.catalog-tile-pf-body .catalog-tile-pf-title { + font-size: 15px; + font-weight: 400; +} +.catalog-tile-pf-body .catalog-tile-pf-subtitle { + color: #8b8d8f; + font-size: 14px; +} +.catalog-tile-pf-body .catalog-tile-pf-description { + flex: 1; + margin-top: 15px; + overflow: hidden; + position: relative; +} +.catalog-tile-pf:active, .catalog-tile-pf:focus, .catalog-tile-pf:hover, .catalog-tile-pf:visited { + color: inherit; + text-decoration: none; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.html new file mode 100644 index 000000000..07896ce95 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.html @@ -0,0 +1,65 @@ +
+
+ +
+
+ +

+ Welcome to Hub Microcks.io +

+Hub Microcks.io is a community place for sharing API mocks and contract tests for using with Microcks. Look for your favorite API mocks. + +
+
+
+
+
+
+ categories +
+ + +
+
+
+ +
+
+ provider +
+
+ +
+
+ +
+
+
+
+
+
+
+ {{ filteredPackages.length }} Packages, {{ countFilteredAPIs() }} APIs +
+
+ +
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.ts new file mode 100644 index 000000000..ac5718220 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/hub.page.ts @@ -0,0 +1,189 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit, Renderer2 } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { HubService } from '../../services/hub.service'; +import { APIPackage } from '../../models/hub.model'; +import { RouterLink } from '@angular/router'; + +const OTHER_CATEGORY = 'Other'; +const IGNORED_PROVIDER_TAILS = [', Inc.', ', Inc', ' Inc.', ' Inc', ', LLC', ' LLC']; + +const sanitizeProviderValue = (value: string) => { + if (!value) { + return value; + } + let providerTail = null; + IGNORED_PROVIDER_TAILS.forEach((tail, index, array) => { + if (value.endsWith(tail)) { + providerTail = tail; + } + }); + if (providerTail) { + return value.substring(0, value.indexOf(providerTail)); + } + return value; +}; + +@Component({ + selector: 'app-hub-page', + templateUrl: './hub.page.html', + styleUrls: ['./hub.page.css'], + imports: [ + CommonModule, + FormsModule, + RouterLink + ] +}) +export class HubPageComponent implements OnInit { + + categories: string[] = []; + providers: string[] = []; + packages: APIPackage[] = []; + maxProviders = 6; + + filterString: string | null = null; + selectedCategory: string | null = null; + selectedProviders: string[] = []; + filteredPackages: APIPackage[] = []; + sortType: string | null = null; + + constructor(private renderer: Renderer2, private packagesSvc: HubService) { } + + ngOnInit() { + this.getPackages(); + } + + getPackages(): void { + this.packagesSvc.getPackages().subscribe(results => { + this.packages = results; + this.filteredPackages = results; + this.initializeAvailableCategories(); + this.initializeAvailableProviders(); + }); + } + + selectCategory(category: string): void { + this.selectedCategory = category; + this.applyFilters(); + } + unselectCategory(category: string): void { + this.selectedCategory = null; + this.applyFilters(); + } + toggleProvider(provider: string): void { + if (this.selectedProviders.indexOf(provider) != -1) { + this.selectedProviders.splice(this.selectedProviders.indexOf(provider), 1); + } else { + this.selectedProviders.push(provider); + } + this.applyFilters(); + } + + applyFilters(): void { + this.filteredPackages = this.packages.filter((item, index, array) => { + return this.challengeFilterString(item) && this.challengeSelectedCategory(item) + && this.challengeSelectedProviders(item); + }); + } + challengeFilterString(item: APIPackage): boolean { + if (this.filterString && this.filterString != null) { + if (item.name.toLowerCase().indexOf(this.filterString) != - 1 || + item.displayName.toLowerCase().indexOf(this.filterString) != -1) { + return true; + } + return false; + } + return true; + } + challengeSelectedCategory(item: APIPackage): boolean { + if (this.selectedCategory && this.selectedCategory != null) { + if (item.categories.indexOf(this.selectedCategory) != -1) { + return true; + } + return false; + } + return true; + } + challengeSelectedProviders(item: APIPackage): boolean { + if (this.selectedProviders && this.selectedProviders.length > 0) { + return this.selectedProviders.indexOf(sanitizeProviderValue(item.provider)) != -1; + } + return true; + } + + countFilteredPackagesForProvider(provider: string): number { + return this.packages.filter((item, index, array) => { + if (this.selectedCategory && item.categories.indexOf(this.selectedCategory) != -1) { + return false; + } else if (sanitizeProviderValue(item.provider) === provider) { + return true; + } + return false; + }).length; + } + countFilteredAPIs(): number { + return this.filteredPackages.reduce((total, item) => total + item.apis.length, 0); + } + + showAllProviders(): void { + this.maxProviders = this.packages.length; + } + + private initializeAvailableCategories(): void { + const unsortedCategories: Record = {}; + this.packages.forEach((apiPackage, index, packs) => { + if (apiPackage.categories.length > 0) { + apiPackage.categories.forEach((category, packageIndex, cats) => { + if (!unsortedCategories[category]) { + unsortedCategories[category] = []; + } + unsortedCategories[category].push(apiPackage); + }); + } else { + if (!unsortedCategories[OTHER_CATEGORY]) { + unsortedCategories[OTHER_CATEGORY] = []; + } + unsortedCategories[OTHER_CATEGORY].push(apiPackage); + } + }); + this.categories = Object.keys(unsortedCategories).sort((cat1, cat2) => { + if (cat1 === OTHER_CATEGORY) { + return 1; + } + if (cat2 === OTHER_CATEGORY) { + return -1; + } + return cat1.localeCompare(cat2); + }); + } + + private initializeAvailableProviders(): void { + const unsortedProviders: Record = {}; + this.packages.forEach((apiPackage, index, packs) => { + const provider = sanitizeProviderValue(apiPackage.provider); + if (!unsortedProviders[provider]) { + unsortedProviders[provider] = []; + } + unsortedProviders[provider].push(apiPackage); + }); + this.providers = Object.keys(unsortedProviders).sort((pro1, pro2) => { + return pro1.localeCompare(pro2); + }); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.css new file mode 100644 index 000000000..81814fbdc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.css @@ -0,0 +1,335 @@ +/* Specific styles */ + +.modal-header { + background-color: #ffffff !important; + border-bottom: 1px solid rgba(0,0,0,0.2); +} + +.mh-api-install-modal_row { + padding-top: 20px; + padding-bottom: 15px; +} + +.mh-button.mh-button-navig { + background-color: #92d400; + color: #fff; +} + +/* Overriden styles */ + +.mh-api-header-content { + padding: 20px 0 10px 0 !important; +} +.mh-api-header_info h1 { + margin-top: 10px; +} +.mh-api-header_version { + font-weight: 400; + font-size: 0.9em; + line-height: 1; + color: #9c9c9c !important; +} + +.mh-api-page_side-panel .properties-side-panel-pf { + background-color: #f1f1f1 !important; +} + +.mh-api-install-modal_list { + padding-top: 10px; + padding-bottom: 10px; +} + +.mh-api-install-modal_list > li:last-child { + margin-top: 20px; + margin-bottom: 25px; +} + +.mh-button { + width: 100%; +} + +/* Global styles coming from styles.css */ + +.mh-page-contents { + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; + width: 100%; + min-height: calc(100vh - 700px); +} +.mh-page-contents_inner { + background: transparent; + display: flex; + padding: 20px 15px 0 15px; +} + +.mh-external-link.indicator { + padding-right: 15px; +} +.mh-external-link.block { + display: block; +} + +@media (min-width: 1400px) { + .container-sm { + max-width: 1220px; + } +} + +/* Specific styles comming from apiVersion.page.css,, ignoring .mh-api-header_title */ + +.mh-api-header-content { + align-items: flex-start; + display: flex; + flex-direction: row; + padding: 30px 0; + overflow: hidden; +} +.mh-api-header_image-container { + background: #fff; + /*border-radius: 50px;*/ + /*box-shadow: 0 2px 4px rgba(0,0,0,.2);*/ + /*height: 100px;*/ + /*width: 100px;*/ + display: flex; + justify-content: center; + align-items: center; +} +.mh-api-header_image { + max-height: 60px; + max-width: 60px; +} + +.mh-api-header_info { + flex: 1; + margin-left: 20px; + overflow: hidden; +} +.mh-api-header_version { + color: #fff; +} + + +@media (min-width: 768px) { + .col-md-push-9 { + left: 75%; + } +} +@media (min-width: 768px) { + .col-md-pull-3 { + right: 25%; + } +} + +@media (min-width: 768px) { + .mh-api-page_side-panel { + margin-top: 20px; + } +} +.mh-api-page_side-panel .mh-button { + width: 100%; +} +.mh-button.mh-button-primary { + background-color: #1991eb; + color: #fff; +} +.mh-button { + border: none; + border-radius: 4px; + padding: 6px 15px 7px; +} + +.mh-api-page_side-panel_separator { + margin: 30px 0; + border-top: 1px solid #eaeaea; +} + +.mh-api-page_side-panel .properties-side-panel-pf { + background-color: #ffffff; + display: block; + padding: 30px 30px 30px 10px; + width: 100%; +} +.properties-side-panel-pf-property:first-of-type { + margin-top: 0; +} +.properties-side-panel-pf-property { + margin-top: 20px; +} +.mh-api-page_side-panel .properties-side-panel-pf .properties-side-panel-pf-property-label { + color: #b6b6b6; + font-size: 10px; + font-weight: 700; + letter-spacing: 1px; + margin: 0; + text-transform: uppercase; +} + +.properties-side-panel-pf-property .navbar { + padding: 0; +} + +.btn-group { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group>.btn:first-child { + margin-left: 0; +} +.btn { + padding: 2px 6px; +} +.btn-default.active, .btn-default .open .dropdown-toggle.btn-default, .btn-default:active, .btn-default:focus, .btn-default:hover { + background-color: #f1f1f1; +} + +.mh-api-page_side-panel_version-dropdown { + background: transparent; + border: none; + box-shadow: none; + color: #363636; + font-size: inherit; + font-weight: inherit; +} +.mh-api-page_side-panel .dropdown-menu, .mh-api-page_side-panel_version-dropdown { + font-size: inherit; + font-weight: inherit; + +} +.mh-api-page_side-panel .dropdown-menu { + background-color: #fafbfd; + border-radius: 4px; + border: 1px solid #eaeaea; + -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175); + box-shadow: 0 6px 12px rgba(0,0,0,.175); +} +.mh-api-page_side-panel .dropdown-menu>li>a { + border-color: transparent; + border-style: solid; + border-width: 1px 0; + padding: 1px 10px; + display: block; + clear: both; + font-weight: 400; + line-height: 1.66667; + color: #363636; + white-space: nowrap; +} +.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover { + color: #4d5258; + text-decoration: none; + background-color: #f5f5f5; +} + +.properties-side-panel-pf-property-value { + font-size: 14px; + margin-top: 5px; + word-break: break-word; +} + +.mh-capability-level-popover_icon { + display: none; + font-size: 10px; + cursor: pointer; +} +@media (min-width: 768px) { + .mh-capability-level-popover_icon { + display: inline-block; + margin-left: 3px; + } +} + +.mh-capability-level-popover_link { + display: none; +} + +.mh-api-page_side-panel_image { + padding-top: 5px; + width: 139px; +} + +.mh-api-markdown-view { + padding-bottom: 15px; +} + +.modal-content { + background-color: #fff; +} +.modal-header .close { + color: #000; +} +.modal-body { + padding: 20px; + padding-top: 0px; +} +.modal-body h2 { + font-size: 22px; + letter-spacing: 1.22px; + padding-top: 0; + margin: 20px 0; + border: 0; +} +.catalog-item-header-pf { + display: flex; + align-items: center; +} +.catalog-item-header-pf-icon { + font-size: 60px; + max-height: 60px; + width: 60px; +} +.catalog-item-header-pf-text { + margin-left: 20px; +} +.catalog-item-header-pf-title { + font-weight: 400; + margin-bottom: 0; + margin-top: 0; +} +.catalog-item-header-pf-subtitle { + color: #8b8d8f; + margin-top: 10px; + margin-bottom: 0; + font-size: 15px; + letter-spacing: .83px; +} + +.mh-api-install-modal_list { + padding-left: 20px; +} +.mh-api-install-modal_list>li:not(:last-child) { + margin-top: 15px; + margin-bottom: 20px; +} +.mh-api-install-modal_list>li { + display: list-item; + text-align: -webkit-match-parent; +} +.mh-api-install-modal_list-item .mh-code { + margin-bottom: 0; +} +.mh-api-install-modal_install-command-container { + display: flex; + flex-direction: row; + align-items: center; + width: 100%; +} +.mh-api-install-modal .mh-code { + margin: 10px 20px 15px 0; + padding: 10px; + background-color: #fafbfd; + border-radius: 3px; + flex: 1; + word-break: break-all; +} +.mh-code { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.mh-capability-level-popover { + max-width: 730px; + height: 330px; + width: 730px; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.html new file mode 100644 index 000000000..0acdb682d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.html @@ -0,0 +1,184 @@ +
+ + +
+ +
+ + +
+
+
+ +
+
+

{{ resolvedAPIVersion.displayName }}

+
version {{resolvedAPIVersion.version }}
+
+
+
+ +
+
+
+
+
+ + +
+ +
+
+
Version
+
+ +
+
+
+
+ + Capability Level + + + +
+
+ + {{ resolvedAPIVersion.capabilityLevel }} + {{ resolvedAPIVersion.capabilityLevel }} + +
+
+
+
Provider
+
+   + {{ resolvedPackage!.source }} +
+
+
+
Links
+ +
+
+
Contracts
+ +
+
+
Created At
+
{{ resolvedAPIVersion.createdAt | date:'EE MMM d, y'}}
+
+
+
Maintainers
+
+
{{ maintainer.name }}
+ {{ maintainer.email }} +
+
+
+
Categories
+
+
{{ category }}
+
+
+
+
+
+
+

{{ resolvedAPIVersion.displayName }}

+
+
+
+ + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.ts new file mode 100644 index 000000000..9c056bfb8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/apiVersion/apiVersion.page.ts @@ -0,0 +1,254 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit, TemplateRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, ParamMap, Router, RouterLink } from '@angular/router'; + +import { Observable, concat } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; + +import { + Notification, + NotificationEvent, + NotificationService, + NotificationType, + ToastNotificationListComponent, +} from '../../../../components/patternfly-ng/notification'; + +import { markdownConverter } from '../../../../components/markdown'; + +import { HubService } from '../../../../services/hub.service'; +import { ImportersService } from '../../../../services/importers.service'; +import { + APIPackage, + APIVersion, + APISummary, +} from '../../../../models/hub.model'; +import { ImportJob } from '../../../../models/importer.model'; + +@Component({ + selector: 'app-hub-api-version-page', + templateUrl: './apiVersion.page.html', + styleUrls: ['./apiVersion.page.css'], + imports: [ + CommonModule, + BsDropdownModule, + RouterLink, + ToastNotificationListComponent + ] +}) +export class HubAPIVersionPageComponent implements OnInit { + + modalRef?: BsModalRef; + package: Observable | null = null; + packageAPIVersion: Observable | null = null; + resolvedPackage?: APIPackage; + resolvedPackageAPI?: APISummary; + resolvedAPIVersion?: APIVersion; + notifications: Notification[] = []; + + importJobId: string | null = null; + discoveredService: string | null = null; + + constructor( + private packagesSvc: HubService, + private importersSvc: ImportersService, + private modalService: BsModalService, + private notificationService: NotificationService, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit() { + this.notifications = this.notificationService.getNotifications(); + + this.package = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.packagesSvc.getPackage(params.get('packageId')!) + ) + ); + this.packageAPIVersion = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.packagesSvc.getAPIVersion( + params.get('packageId')!, + params.get('apiVersionId')! + ) + ) + ); + + this.package.subscribe((result) => { + this.resolvedPackage = result; + this.packageAPIVersion!.subscribe((apiVersion) => { + this.resolvedPackage!.apis.forEach((api) => { + if (api.name === apiVersion.id) { + this.resolvedPackageAPI = api; + } + }); + }); + }); + this.packageAPIVersion.subscribe((result) => { + this.resolvedAPIVersion = result; + }); + } + + renderDescription(): string { + return markdownConverter.makeHtml(this.resolvedAPIVersion!.description); + } + + renderCapabilityLevel(): string { + if ('Full Mocks' === this.resolvedAPIVersion!.capabilityLevel) { + return '/assets/images/mocks-level-2.svg'; + } else if ( + 'Mocks + Assertions' === this.resolvedAPIVersion!.capabilityLevel + ) { + return '/assets/images/mocks-level-2.svg'; + } + return '/assets/images/mocks-level-1.svg'; + } + + openModal(template: TemplateRef) { + this.modalRef = this.modalService.show(template, { class: 'modal-lg' }); + } + + handleCloseNotification($event: NotificationEvent): void { + this.notificationService.remove($event.notification); + } + + installByDirectUpload(): void { + this.notificationService.message( + NotificationType.INFO, + this.resolvedAPIVersion!.name, + 'Starting install in Microcks. Hold on...', + false + ); + + const uploadBatch = []; + for (let i = 0; i < this.resolvedAPIVersion!.contracts.length; i++) { + console.log('Uploading contract: ' + this.resolvedAPIVersion!.contracts[i].url); + uploadBatch.push( + this.packagesSvc.importAPIVersionContractContent( + this.resolvedAPIVersion!.contracts[i].url, + i == 0 + ) + ); + } + + // Concat all the observables to run them in sequence. + concat(...uploadBatch).subscribe({ + next: (res: any) => { + this.discoveredService = res.name; + this.notificationService.message( + NotificationType.SUCCESS, + this.discoveredService!, + 'Import and discovery of service has been done', false + ); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + this.resolvedAPIVersion!.name, + 'Importation error on server side (' + err.error.text + ')', false + ); + }, + complete: () => {} //console.log('Observer got a complete notification'), + }); + } + + installByImporterCreation(): void { + for (let i = 0; i < this.resolvedAPIVersion!.contracts.length; i++) { + const job = {} as ImportJob; + job.name = + this.resolvedAPIVersion!.id + + ' - v. ' + + this.resolvedAPIVersion!.version + + ' [' + i + ']'; + job.repositoryUrl = this.resolvedAPIVersion!.contracts[i].url; + // Mark is as secondary artifact if not the first. + if (i > 0) { + job.mainArtifact = false; + } + this.importersSvc.createImportJob(job).subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + job.name, 'Import job has been created', false + ); + // Retrieve job id before activating. + job.id = res.id; + this.importJobId = job.id; + this.activateImportJob(job); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + job.name, 'Import job cannot be created (' + err.message + ')', false + ); + }, + complete: () => {} //console.log('Observer got a complete notification'), + }); + } + } + + activateImportJob(job: ImportJob): void { + this.importersSvc.activateImportJob(job).subscribe({ + next: (res) => { + job.active = true; + this.notificationService.message( + NotificationType.SUCCESS, + job.name, 'Import job has been started/activated', false + ); + this.startImportJob(job); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + job.name, 'Import job cannot be started/activated (' + err.message + ')', false + ); + }, + complete: () => {} //console.log('Observer got a complete notification'), + }); + } + + startImportJob(job: ImportJob): void { + this.importersSvc.startImportJob(job).subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + job.name, 'Import job has been forced', false + ); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + job.name, 'Import job cannot be forced now', false + ); + }, + complete: () => {} //console.log('Observer got a complete notification'), + }); + } + + navigateToImporters(): void { + this.modalRef?.hide(); + this.router.navigate(['/importers']); + } + navigateToService(): void { + this.modalRef?.hide(); + this.router.navigate(['/services', this.discoveredService]); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.css new file mode 100644 index 000000000..5cb26ff21 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.css @@ -0,0 +1,146 @@ +/* Overriden styles */ + +.mh-package-header-content { + padding: 20px 0 10px 0 !important; +} +.mh-package-header_info h1 { + margin-top: 10px; +} +.mh-package-header_description { + font-weight: 400; + font-size: 0.9em; + color: #9c9c9c !important; +} + +/* Global styles coming from styles.css */ + +.mh-page-contents { + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; + width: 100%; + min-height: calc(100vh - 700px); +} +.mh-page-contents_inner { + background: transparent; + display: flex; + padding: 20px 15px 0 15px; +} + +.mh-external-link.indicator { + padding-right: 15px; +} +.mh-external-link.block { + display: block; +} + +@media (min-width: 1400px) { + .container-sm { + max-width: 1220px; + } +} + +/* Specific styles comming from package.page.css, ignoring .mh-package-header_title */ + +.mh-package-header-content { + align-items: flex-start; + display: flex; + flex-direction: row; + padding: 30px 0; + overflow: hidden; +} +.mh-package-header_image-container { + background: #fff; + /*border-radius: 50px;*/ + /*box-shadow: 0 2px 4px rgba(0,0,0,.2);*/ + /*height: 100px;*/ + /*width: 100px;*/ + display: flex; + justify-content: center; + align-items: center; +} +.mh-package-header_image { + max-height: 60px; + max-width: 60px; +} + +.mh-package-header_info { + flex: 1; + margin-left: 20px; + overflow: hidden; +} +.mh-package-header_description { + color: #fff; +} + +@media (min-width: 768px) { + .col-md-push-7 { + left: 58.32%; + } +} +@media (min-width: 768px) { + .col-md-pull-5 { + right: 41.68%; + } +} + +.mh-package-api-list-view { + width: 100%; +} +.mh-package-api-list-view_item { + background: #fff; + border: none; + border-radius: 5px; + box-shadow: 0 3px 6px rgba(0,0,0,.1); + color: inherit; + display: flex; + flex: 1; + flex-direction: column; + height: 100px; + margin: 0 15px 15px 0; + padding: 15px; +} + +.catalog-tile-pf-icon { + font-size: 40px; + height: 40px; + max-width: 80px; + min-width: 40px; +} +.mh-package-api-list-view_item .catalog-tile-pf-icon { + margin-right: 10px; +} +.mh-package-api-list-view_item:active, .mh-package-api-list-view_item:focus, .mh-package-api-list-view_item:hover, .mh-package-api-list-view_item:visited { + color: inherit; + text-decoration: none; +} + +.catalog-tile-pf-header { + display: flex; + flex-shrink: 0; + font-size: 16px; + font-weight: 400; +} +.catalog-tile-pf-body { + display: flex; + flex: 1; + flex-direction: column; + margin-top: 15px; + min-height: 0; +} +.catalog-tile-pf-body .catalog-tile-pf-title { + font-size: 15px; + font-weight: 400; +} + +.mh-package-api-list-view_item .catalog-tile-pf-subtitle { + color: #bbb; + font-size: 12px; +} +.mh-package-api-list-view_item .catalog-tile-pf-description { + margin-top: 5px; + overflow: hidden; + position: relative; + width: 100%; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.html new file mode 100644 index 000000000..b92dea1ec --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.html @@ -0,0 +1,50 @@ +
+ + +
+
+
+ +
+
+

{{ resolvedPackage.displayName }}

+
{{ resolvedPackage.description }}
+
+
+
+ +
+
+
+ +
+

{{ resolvedPackage.displayName }}

+
+
+
+
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.ts new file mode 100644 index 000000000..ce13164c9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/hub/package/package.page.ts @@ -0,0 +1,69 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, ParamMap, RouterLink } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { HubService } from '../../../services/hub.service'; +import { APIPackage, APIVersion } from '../../../models/hub.model'; + +import { markdownConverter } from '../../../components/markdown'; + +@Component({ + selector: 'app-hub-package-page', + templateUrl: './package.page.html', + styleUrls: ['./package.page.css'], + imports: [ + CommonModule, + FormsModule, + RouterLink + ] +}) +export class HubPackagePageComponent implements OnInit { + + package: Observable | null = null; + packageAPIVersions?: Observable; + resolvedPackage?: APIPackage; + resolvedAPIVersions?: APIVersion[]; + + constructor(private packagesSvc: HubService, private route: ActivatedRoute) { } + + ngOnInit() { + this.package = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.packagesSvc.getPackage(params.get('packageId')!)) + ); + this.packageAPIVersions = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.packagesSvc.getLatestAPIVersions(params.get('packageId')!)) + ); + + this.package.subscribe( result => { + this.resolvedPackage = result; + }); + this.packageAPIVersions.subscribe (result => { + this.resolvedAPIVersions = result; + }); + } + + renderLongDescription(): string { + return markdownConverter.makeHtml(this.resolvedPackage?.longDescription); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/importer.wizard.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/importer.wizard.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/importer.wizard.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/importer.wizard.html new file mode 100644 index 000000000..1b68ba6ba --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/importer.wizard.html @@ -0,0 +1,147 @@ + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/importer.wizard.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/importer.wizard.ts new file mode 100644 index 000000000..5c784dc3e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/importer.wizard.ts @@ -0,0 +1,183 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Component, + OnInit, + viewChild, + ViewEncapsulation, + Output, + EventEmitter, + ChangeDetectorRef, + AfterViewInit, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { MatStepper, MatStepperModule } from '@angular/material/stepper'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { EditLabelsComponent } from '../../../components/edit-labels/edit-labels.component'; +import { LabelListComponent } from '../../../components/label-list/label-list.component'; + +import { ImportJob } from '../../../models/importer.model'; +import { Secret, SecretRef } from '../../../models/secret.model'; +import { Metadata } from '../../../models/commons.model'; +import { SecretsService } from '../../../services/secrets.service'; + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'app-importer-wizard', + templateUrl: './importer.wizard.html', + styleUrls: ['./importer.wizard.css'], + imports: [ + CommonModule, + EditLabelsComponent, + FormsModule, + LabelListComponent, + MatStepperModule, + ReactiveFormsModule + ] +}) +export class ImporterWizardComponent implements OnInit, AfterViewInit { + private stepper = viewChild('stepper'); + + @Output() saveImportJobAction = new EventEmitter(); + + wizardTitle = 'Create a new Job'; + formInvalid = true; + + data: any = {}; + job: ImportJob = {} as ImportJob; + useSecret = false; + secrets: Secret[] = []; + + constructor( + private secretsSvc: SecretsService, + private ref: ChangeDetectorRef, + public bsModalRef: BsModalRef + ) { + } + + ngOnInit() { + this.wizardTitle = 'Create a new Job'; + if (this.job.id != null) { + this.useSecret = this.job.secretRef != null; + this.wizardTitle = 'Edit existing Job "' + this.job.name + '"'; + this.updateJobProperties(); + } else { + this.job = { + metadata: { + labels: {}, + } as Metadata, + } as ImportJob; + } + } + + ngAfterViewInit() { + // TODO: verify if needed? + this.getSecrets(); + } + + getSecrets(page: number = 1): void { + this.secretsSvc + .getSecrets(page) + .subscribe((results) => (this.secrets = results)); + } + + updateJobProperties(): void { + this.formInvalid = !( + this.job.name !== undefined && + this.job.name.length > 0 && + this.job.repositoryUrl !== undefined && + this.job.repositoryUrl.length > 0); + + if (this.useSecret && this.job.secretRef == null) { + this.job.secretRef = new SecretRef('none', ''); + } else if (!this.useSecret) { + this.job.secretRef = undefined; + } + + if (this.job.metadata == undefined) { + this.job.metadata = {} as Metadata; + this.job.metadata.labels = {}; + } + } + updateMainArtifact(event: any): void { + this.job.mainArtifact = !event; + } + updateSecretProperties(event: any): void { + const secretId = event.target.value; + if ('none' != event.target.value) { + for (const secret of this.secrets) { + if (secretId === secret.id) { + this.job.secretRef = new SecretRef(secret.id, secret.name); + break; + } + } + } else { + this.job.secretRef = undefined; + } + } + + save($event: any) { + this.saveImportJobAction.emit(this.job); + this.close(); + } + + close(): void { + this.bsModalRef.hide() + } + + isNoSecretSelected(): boolean { + return this.job.secretRef == undefined || this.job.secretRef?.secretId === 'none'; + } + isSecretSelected(secret: Secret): boolean { + return secret.id === this.job.secretRef?.secretId; + } + + isNextDisabled(): boolean { + return this.formInvalid; + } + next($event: any): void { + // Because label list is eagerly loaded, it doesn't see changes in labels. + // And because the label list uses the ChangeDetectionStrategy.OnPush, we have to explicitely + // set a new value (and not only mutate) to this.job to force evaluation later on. + // This is the only way I know to build a deep clone of job and force reassignement... + this.job = JSON.parse(JSON.stringify(this.job)); + // Trigger view reevaluation to update the label list component. + this.ref.detectChanges(); + + if (this.stepper && this.stepper()?.selectedIndex) { + if (this.stepper()?.selectedIndex === 3) { + this.save($event); + } + } + this.stepper && this.stepper()?.next(); + } + + isPreviousDisabled(): boolean { + if (this.stepper && this.stepper()?.selectedIndex) { + return this.stepper()?.selectedIndex === 0; + } + return true; + } + previous(): void { + this.stepper && this.stepper()?.previous(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/uploader.dialog.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/uploader.dialog.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/uploader.dialog.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/uploader.dialog.html new file mode 100644 index 000000000..ee3576139 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/uploader.dialog.html @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/uploader.dialog.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/uploader.dialog.ts new file mode 100644 index 000000000..23bc324bb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/_components/uploader.dialog.ts @@ -0,0 +1,106 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { + NotificationService, + NotificationType, +} from '../../../components/patternfly-ng/notification'; +import { FileUploader, FileItem, ParsedResponseHeaders, FileUploadModule } from 'ng2-file-upload'; +import { IAuthenticationService } from '../../../services/auth.service'; + +@Component({ + selector: 'app-uploader-dialog', + templateUrl: './uploader.dialog.html', + styleUrls: ['./uploader.dialog.css'], + imports: [ + FileUploadModule, + FormsModule, + ReactiveFormsModule + ], +}) +export class ArtifactUploaderDialogComponent implements OnInit { + title?: string; + closeBtnName?: string; + + mainArtifact = true; + uploader: FileUploader; + + constructor( + public bsModalRef: BsModalRef, + private notificationService: NotificationService, + protected authService: IAuthenticationService + ) { + if (this.authService.isAuthenticated()) { + this.uploader = new FileUploader({ + url: '/api/artifact/upload', + authToken: 'Bearer ' + this.authService.getAuthenticationSecret(), + itemAlias: 'file', + parametersBeforeFiles: true, + }); + } else { + this.uploader = new FileUploader({ + url: '/api/artifact/upload', + itemAlias: 'file', + parametersBeforeFiles: true, + }); + } + } + + ngOnInit() { + this.uploader.onErrorItem = ( + item: FileItem, + response: string, + status: number, + headers: ParsedResponseHeaders + ) => { + this.notificationService.message( + NotificationType.DANGER, + item.file.name ?? 'Unknown file', + 'Importation error on server side (' + response + ')', + false + ); + }; + this.uploader.onSuccessItem = ( + item: FileItem, + response: string, + status: number, + headers: ParsedResponseHeaders + ) => { + this.notificationService.message( + NotificationType.SUCCESS, + item.file.name ?? 'Unknown file', + 'Import of ' + response + ' done!', + false + ); + }; + } + + protected updateMainArtifact(event: any): void { + this.mainArtifact = !event; + } + protected upload(): void { + this.uploader.onBuildItemForm = (item: FileItem, form: any) => { + form.append('mainArtifact', this.mainArtifact); + }; + this.uploader.uploadAll(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.css new file mode 100644 index 000000000..52a464043 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.css @@ -0,0 +1,47 @@ +.repositoryUrl { + max-width: 700px; + word-wrap: break-word; +} +@media (min-width: 1200px){ +.repositoryUrl { + max-width: none; +} +} + +:host > .tooltip-inner { + max-width: 400px; + word-wrap: break-word; +} + +span.label { + margin-left: 4px !important; + margin-right: 4px !important; + padding-top: 0.6em; + padding-bottom: 0.6em; +} +span.label a { + color: #ffffff; +} + +.list-view-pf-description { + flex-basis: 60%; +} + +@media (min-width: 1200px){ +.list-view-pf-additional-info { + width: 20%; +} +} +@media (min-width: 1500px){ +.list-view-pf-additional-info { + width: 10%; +} +} + +.metadata-label { + margin-top: 10px !important; +} +.job-update { + margin-top: 10px; + display: inline-block; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.html new file mode 100644 index 000000000..23b83248b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.html @@ -0,0 +1,100 @@ +
+ + +
+ +

+ Import Jobs +
+   + +
+

+Importers periodically scan source code repositories to check for update in your Services and API mock + definitions. + +
+
+
+
+ +
+
+
+
+
+
+
+ {{ job.name }} +
+
+ {{ job.repositoryUrl }}
+ +
+ Updated on {{ job.metadata.lastUpdate | date : 'medium' }} +
+
+
+
+
+ + +
+
+ Imported + Not Imported + Scanned + Not scanned + + Import Error + + + Services + +
+
+
+
+ +
+ +
+
+
+ + + +
+ + +

Do you really want to delete the selected Import Job ?

+ +
+ + NOTE : + This will permanently delete the Import Job from Microcks. Already discovered Services & + APIs remain but will not be updated. This operation cannot be undone. +
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.ts new file mode 100644 index 000000000..c08d30f99 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/importers.page.ts @@ -0,0 +1,459 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CommonModule, DatePipe } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + Injectable, + OnInit, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Params, Router } from '@angular/router'; + +/* +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +*/ + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; + +import { PaginationConfig, PaginationEvent, PaginationModule } from '../../components/patternfly-ng/pagination'; +import { ToolbarConfig, ToolbarModule } from '../../components/patternfly-ng/toolbar'; +import { + FilterConfig, + FilterEvent, + FilterField, + FilterType, + Filter, +} from '../../components/patternfly-ng/filter'; +import { + Notification, + NotificationEvent, + NotificationService, + NotificationType, + ToastNotificationListComponent, +} from '../../components/patternfly-ng/notification'; + +import { ConfirmDeleteDialogComponent } from '../../components/confirm-delete/confirm-delete.component'; +import { LabelListComponent } from '../../components/label-list/label-list.component'; + +import { ImportJob, ServiceRef } from '../../models/importer.model'; +import { IAuthenticationService } from '../../services/auth.service'; +import { ConfigService } from '../../services/config.service'; +import { ImportersService } from '../../services/importers.service'; +import { ServicesService } from '../../services/services.service'; +import { ArtifactUploaderDialogComponent } from './_components/uploader.dialog'; +import { ImporterWizardComponent} from './_components/importer.wizard'; +import { ServiceRefsDialogComponent } from './service-refs.dialog'; + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'app-importers-page', + templateUrl: './importers.page.html', + styleUrls: ['./importers.page.css'], + imports: [ + CommonModule, + ConfirmDeleteDialogComponent, + LabelListComponent, + BsDropdownModule, + DatePipe, + FormsModule, + //MatButtonModule, + //MatFormFieldModule, + //MatInputModule, + PaginationModule, + ToolbarModule, + ToastNotificationListComponent, + TooltipModule + ], + providers: [ImportersPageComponent] +}) +export class ImportersPageComponent implements OnInit { + + modalRef?: BsModalRef; + importJobs?: ImportJob[]; + importJobsCount: number = 0; + servicesLabels?: Map; + toolbarConfig: ToolbarConfig = new ToolbarConfig; + filterConfig: FilterConfig = new FilterConfig; + paginationConfig: PaginationConfig = new PaginationConfig; + nameFilterTerm: string | null = null; + repositoryFilter: string | null = null; + filtersText = ''; + selectedJob?: ImportJob | null; + notifications: Notification[] = []; + + constructor(private importersSvc: ImportersService, private servicesSvc: ServicesService, + private modalService: BsModalService, private notificationService: NotificationService, + protected authService: IAuthenticationService, private config: ConfigService, + private route: ActivatedRoute, private router: Router, private ref: ChangeDetectorRef) { } + + ngOnInit() { + this.notifications = this.notificationService.getNotifications(); + this.getImportJobs(); + this.countImportJobs(); + + const filterFieldsConfig = []; + if (this.hasRepositoryFilterFeatureEnabled()) { + this.getServicesLabels(); + filterFieldsConfig.push({ + id: this.repositoryFilterFeatureLabelKey(), + title: this.repositoryFilterFeatureLabelLabel(), + placeholder: 'Filter by ' + this.repositoryFilterFeatureLabelLabel() + '...', + type: FilterType.SELECT, + queries: [] + }); + } + filterFieldsConfig.push({ + id: 'name', + title: 'Name', + placeholder: 'Filter by Name...', + type: FilterType.TEXT + }); + + this.paginationConfig = { + pageNumber: 1, + pageSize: 20, + pageSizeIncrements: [], + totalItems: 20 + } as PaginationConfig; + + this.filterConfig = { + fields: filterFieldsConfig as FilterField[], + resultsCount: 20, + appliedFilters: [] + } as FilterConfig; + + this.toolbarConfig = { + actionConfig: undefined, + filterConfig: this.filterConfig, + sortConfig: undefined, + views: [] + } as ToolbarConfig; + + this.route.queryParams.subscribe(queryParams => { + // Look at query parameters to apply filters. + this.filterConfig.appliedFilters = []; + if (queryParams['name']) { + this.nameFilterTerm = queryParams['name']; + this.filterConfig.appliedFilters.push({ + field: {title: 'Name'} as FilterField, + value: this.nameFilterTerm + } as Filter); + } + if (queryParams['labels.' + this.repositoryFilterFeatureLabelKey()]) { + this.repositoryFilter = queryParams['labels.' + this.repositoryFilterFeatureLabelKey()]; + this.filterConfig.appliedFilters.push({ + field: {title: this.repositoryFilterFeatureLabelLabel()} as FilterField, + value: this.repositoryFilter + } as Filter); + } + if (this.nameFilterTerm != null || this.repositoryFilter != null) { + this.filterImportJobs(this.repositoryFilter!, this.nameFilterTerm!); + } else { + // Default - retrieve all the jobs + this.getImportJobs(); + this.countImportJobs(); + } + }); + } + + getImportJobs(page: number = 1): void { + this.importersSvc.getImportJobs(page).subscribe(results => this.importJobs = results); + } + filterImportJobs(repositoryFilter: string, nameFilterTerm: string): void { + const labelsFilter = new Map(); + if (repositoryFilter != null) { + labelsFilter.set(this.repositoryFilterFeatureLabelKey(), repositoryFilter); + } + this.importersSvc.filterImportJobs(labelsFilter, nameFilterTerm).subscribe(results => { + this.importJobs = results; + this.filterConfig.resultsCount = results.length; + }); + // Update browser URL to make the page bookmarkable. + const queryParams: any = { name: nameFilterTerm }; + for (const key of Array.from( labelsFilter.keys() )) { + queryParams['labels.' + key] = labelsFilter.get(key); + } + this.router.navigate([], {relativeTo: this.route, queryParams: queryParams as Params, queryParamsHandling: 'merge'}); + } + + countImportJobs(): void { + this.importersSvc.countImportJobs().subscribe(results => { + this.importJobsCount = results.counter; + this.paginationConfig.totalItems = this.importJobsCount; + }); + } + + getServicesLabels(): void { + this.servicesSvc.getServicesLabels().subscribe(results => { + this.servicesLabels = results; + const queries: any[] = []; + // Get only the label values corresponding to key used for filtering, then transform them for Patternfly. + if ( + this.servicesLabels && (this.servicesLabels as any)[this.repositoryFilterFeatureLabelKey()] != undefined + ) { + (this.servicesLabels as any)[this.repositoryFilterFeatureLabelKey()].map( + (label: any) => queries.push({ id: label, value: label }) + ); + } + this.filterConfig.fields[0].queries = queries; + }); + } + + handlePageSize($event: PaginationEvent) { + // this.updateItems(); + } + + handlePageNumber($event: PaginationEvent) { + this.getImportJobs($event.pageNumber); + } + + handleFilter($event: FilterEvent): void { + this.filtersText = ''; + if (!$event.appliedFilters || $event.appliedFilters.length == 0) { + this.nameFilterTerm = null; + this.repositoryFilter = null; + this.getImportJobs(); + } else { + $event.appliedFilters.forEach((filter) => { + if (this.hasRepositoryFilterFeatureEnabled() && filter.field.id === this.repositoryFilterFeatureLabelKey()) { + this.repositoryFilter = filter.value; + } else { + this.nameFilterTerm = filter.value; + } + }); + this.filterImportJobs(this.repositoryFilter!, this.nameFilterTerm!); + } + } + + openArtifactUploader(): void { + const initialState = { + }; + this.modalRef = this.modalService.show(ArtifactUploaderDialogComponent, {initialState}); + this.modalRef.content.closeBtnName = 'Close'; + } + + openServiceRefs(serviceRefs: ServiceRef[]): void { + const initialState = { + serviceRefs + }; + this.modalRef = this.modalService.show(ServiceRefsDialogComponent, {initialState}); + this.modalRef.content.closeBtnName = 'Close'; + } + + createImportJob(): void { + this.modalRef = this.modalService.show(ImporterWizardComponent, { class: 'modal-lg' }); + + this.modalRef.content.saveImportJobAction.subscribe((job: ImportJob) => { + this.saveOrUpdateImportJob(job); + }); + } + editImportJob(job: ImportJob): void { + this.selectedJob = job; + this.modalRef = this.modalService.show(ImporterWizardComponent, { + class: 'modal-lg', + initialState: { + job: this.selectedJob + } + }); + + this.modalRef.content.saveImportJobAction.subscribe((job: ImportJob) => { + this.saveOrUpdateImportJob(job); + }); + } + + saveOrUpdateImportJob(job: ImportJob): void { + if (job.id) { + this.importersSvc.updateImportJob(job).subscribe( + { + next: res => { + this.notificationService.message(NotificationType.SUCCESS, + job.name, 'Import job has been updated', false); + // Trigger view reevaluation to update the label list component. + this.importJobs = JSON.parse(JSON.stringify(this.importJobs)); + this.ref.detectChanges(); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + job.name, 'Import job cannot be updated (' + err.message + ')', false); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + } + ); + } else { + this.importersSvc.createImportJob(job).subscribe( + { + next: res => { + this.notificationService.message(NotificationType.SUCCESS, + job.name, 'Import job has been created', false); + this.getImportJobs(); + // Retrieve job id before activating. + job.id = res.id; + this.activateImportJob(job); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + job.name, 'Import job cannot be created (' + err.message + ')', false); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + } + ); + } + } + + deleteImportJob(job: ImportJob): void { + this.importersSvc.deleteImportJob(job).subscribe( + { + next: res => { + job.active = true; + this.notificationService.message(NotificationType.SUCCESS, + job.name, 'Import job has been deleted', false); + this.getImportJobs(); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + job.name, 'Import job cannot be deleted (' + err.message + ')', false); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + } + ); + } + + activateImportJob(job: ImportJob): void { + this.importersSvc.activateImportJob(job).subscribe( + { + next: res => { + job.active = true; + this.notificationService.message(NotificationType.SUCCESS, + job.name, 'Import job has been started/activated', false); + this.startImportJob(job); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + job.name, 'Import job cannot be started/activated (' + err.message + ')', false); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + } + ); + } + + startImportJob(job: ImportJob): void { + this.importersSvc.startImportJob(job).subscribe( + { + next: res => { + this.notificationService.message(NotificationType.SUCCESS, + job.name, 'Import job has been forced', false); + console.log('ImportJobs in 2 secs'); + // TODO run this outsize NgZone using zone.runOutsideAngular() : https://angular.io/api/core/NgZone + setTimeout(() => { + this.getImportJobs(); + }, 2000); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + job.name, 'Import job cannot be forced now', false); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + } + ); + } + + stopImportJob(job: ImportJob): void { + this.importersSvc.stopImportJob(job).subscribe( + { + next: res => { + job.active = false; + this.notificationService.message(NotificationType.SUCCESS, + job.name, 'Import job has been stopped/desactivated', false); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + job.name, 'Import job cannot be stopped/desactivated (' + err.message + ')', false); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + } + ); + } + + handleCloseNotification($event: NotificationEvent): void { + this.notificationService.remove($event.notification); + } + + public canImportArtifact(): boolean { + if (this.hasRepositoryTenancyFeatureEnabled()) { + const rolesStr = this.config.getFeatureProperty('repository-tenancy', 'artifact-import-allowed-roles'); + if (rolesStr == undefined || rolesStr === '') { + return true; + } + // If roles specified, check if any is endorsed. + const roles = rolesStr.split(','); + for (const role of roles) { + if (this.hasRole(role)) { + return true; + } + if (role === 'manager-any') { + const managerOfAny = this.hasRoleForAny('manager'); + if (managerOfAny) { + return true; + } + } + } + return false; + } + // Default is manager to keep coherent behaviour with multi-tenant feature. + return this.hasRole('manager') || this.hasRole('admin'); + } + + public hasRole(role: string): boolean { + return this.authService.hasRole(role); + } + public hasRoleForAny(role: string): boolean { + return this.authService.hasRoleForAnyResource(role); + } + public hasRoleForJob(role: string, job: ImportJob): boolean { + if (this.hasRepositoryTenancyFeatureEnabled() && job.metadata && job.metadata.labels) { + const tenant = job.metadata.labels[this.repositoryFilterFeatureLabelKey()]; + if (tenant !== undefined && this.authService.hasRoleForResource(role, tenant)) { + return true; + } + } + return this.hasRole(role); + } + + public hasRepositoryFilterFeatureEnabled(): boolean { + return this.config.hasFeatureEnabled('repository-filter'); + } + public hasRepositoryTenancyFeatureEnabled(): boolean { + return this.config.hasFeatureEnabled('repository-tenancy'); + } + + public repositoryFilterFeatureLabelKey(): string { + return this.config.getFeatureProperty('repository-filter', 'label-key'); + } + public repositoryFilterFeatureLabelLabel(): string { + return this.config.getFeatureProperty('repository-filter', 'label-label'); + } + public repositoryFilterFeatureLabelList(): string { + return this.config.getFeatureProperty('repository-filter', 'label-list'); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/service-refs.dialog.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/service-refs.dialog.ts new file mode 100644 index 000000000..68b6ac598 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/importers/service-refs.dialog.ts @@ -0,0 +1,56 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import type { ServiceRef } from '../../models/importer.model'; + +@Component({ + selector: 'app-servicerefs-dialog', + template: ` + + + + `, + imports: [ + CommonModule + ], +}) +export class ServiceRefsDialogComponent implements OnInit { + title?: string; + closeBtnName?: string; + serviceRefs: ServiceRef[] = []; + + constructor(public bsModalRef: BsModalRef) {} + + ngOnInit() { + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/metrics/invocations/{serviceId}/invocations-service.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/metrics/invocations/{serviceId}/invocations-service.page.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/metrics/invocations/{serviceId}/invocations-service.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/metrics/invocations/{serviceId}/invocations-service.page.html new file mode 100644 index 000000000..c944543b5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/metrics/invocations/{serviceId}/invocations-service.page.html @@ -0,0 +1,55 @@ +
+ + +

Invocations for {{ serviceName }} - {{ serviceVersion }}

+ +
+
+ +
+
+
+
+ + + + + + +
+
+ Go +
+
+ +
+
+

Day invocations

+ +
+

Today

+

{{ day | date : 'mediumDate' }}

+
+
+
+

Hour invocations

+ +
+

From {{ hour }}h to {{ hour + 1 }}h

+
+
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/metrics/invocations/{serviceId}/invocations-service.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/metrics/invocations/{serviceId}/invocations-service.page.ts new file mode 100644 index 000000000..378e62acd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/metrics/invocations/{serviceId}/invocations-service.page.ts @@ -0,0 +1,82 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router, ParamMap, RouterLink } from '@angular/router'; + +import { Observable, of } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; + +import { DayInvocationsBarChartComponent } from '../../../../components/day-invocations-bar-chart/day-invocations-bar-chart.component'; +import { HourInvocationsBarChartComponent } from '../../../../components/hour-invocations-bar-chart/hour-invocations-bar-chart.component'; + +import { DailyInvocations } from '../../../..//models/metric.model'; +import { MetricsService } from '../../../../services/metrics.service'; + + +@Component({ + selector: 'app-invocations-service-page', + templateUrl: './invocations-service.page.html', + styleUrls: ['./invocations-service.page.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + RouterLink, + BsDatepickerModule, + DayInvocationsBarChartComponent, + HourInvocationsBarChartComponent + ] +}) +export class InvocationsServicePageComponent implements OnInit { + + serviceName!: string; + serviceVersion!: string; + dailyInvocations?: Observable; + + day: Date | null = null; + hour: number = 0; + serviceNameAndVersion!: string; + + constructor(private metricsSvc: MetricsService, + private route: ActivatedRoute, private router: Router, private ref: ChangeDetectorRef) { + } + + ngOnInit() { + this.dailyInvocations = this.route.paramMap.pipe( + switchMap((params: ParamMap) => { + this.serviceName = params.get('serviceName')!; + this.serviceVersion = params.get('serviceVersion')!; + this.serviceNameAndVersion = this.serviceName + ':' + this.serviceVersion; + return this.metricsSvc.getServiceInvocationStats(params.get('serviceName')!, params.get('serviceVersion')!, new Date()); + }) + ); + } + + updateServiceInvocationStats(): void { + if (this.day != null) { + this.dailyInvocations = this.metricsSvc.getServiceInvocationStats(this.serviceName, this.serviceVersion, this.day); + } + } + + changeDay(value: Date): void { + this.day = value; + } + changeHour(value: number): void { + this.hour = value; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.css new file mode 100644 index 000000000..202aeb470 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.css @@ -0,0 +1,29 @@ +.api-type-title { + margin-bottom: 40px; +} + +.api-card:hover { + background-color: #ededed; +} +.api-card-selected { + background-color: #ededed; +} + +.card-pf-header { + padding: 10px; +} + +.card-pf-header .fa { + border-radius: 50%; + font-size: 1em; + height: 30px; + width: 30px; + line-height: 26px; + margin-right: 5px; +} + +.card-pf-body { + padding-left: 10px; + padding-right: 10px; + padding-bottom: 20px !important; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.html new file mode 100644 index 000000000..78885a6c9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.html @@ -0,0 +1,163 @@ + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.ts new file mode 100644 index 000000000..9fdb58b8e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/_components/direct-api.wizard.ts @@ -0,0 +1,123 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit, viewChild, ViewEncapsulation, Output, EventEmitter } from '@angular/core'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { MatStepper, MatStepperModule } from '@angular/material/stepper'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { Api, ServiceType } from '../../../models/service.model'; +import { ServicesPageComponent } from '../services.page'; + +const API_TYPE = { + GENERIC_EVENT: 'GENERIC_EVENT', + GENERIC_REST: 'GENERIC_REST', +} as const; + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'app-direct-api-wizard', + templateUrl: './direct-api.wizard.html', + styleUrls: ['./direct-api.wizard.css'], + imports: [FormsModule, MatStepperModule, ReactiveFormsModule] +}) +export class DirectAPIWizardComponent implements OnInit { + private stepper = viewChild('stepper'); + + @Output() saveDirectAPIAction = new EventEmitter(); + + API_TYPE = API_TYPE; + selectedApiType: keyof typeof API_TYPE | undefined; + + formInvalid = true; + api: Api = {} as Api; + apiType: ServiceType | undefined; + + wizardHost: ServicesPageComponent | undefined; + + constructor(public bsModalRef: BsModalRef) { + } + + ngOnInit() { + } + + changeApiType(value: keyof typeof API_TYPE) { + this.selectedApiType = value; + this.apiType = value === 'GENERIC_EVENT' ? ServiceType.GENERIC_EVENT : ServiceType.GENERIC_REST; + this.stepper && this.stepper()?.next(); + } + updateApiProperties(): void { + this.formInvalid = false; + if (this.api.name == null || this.api.version == null || this.api.resource == null) { + this.formInvalid = true; + } + } + updateApiReference(): void { + this.formInvalid = false; + if (this.api.referencePayload != null && this.api.referencePayload.trim() != '') { + try { + JSON.parse(this.api.referencePayload); + } catch (e) { + this.formInvalid = true; + } + } + if (this.apiType === ServiceType.GENERIC_EVENT && + ((this.api.referencePayload == null || this.api.referencePayload.trim() === ''))) { + this.formInvalid = true; + } + } + + save($event: any) { + /* + if (this.wizardHost) { + this.wizardHost.createDirectAPI(this.apiType || ServiceType.GENERIC_REST, this.api); + this.wizardHost.closeDirectAPIWizardModal($event); + } + */ + this.api.type = this.apiType || ServiceType.GENERIC_REST; + this.saveDirectAPIAction.emit(this.api); + this.close(); + } + + close(): void { + this.bsModalRef.hide() + } + + isNextDisabled(): boolean { + return this.formInvalid; + } + next($event: any): void { + if (this.stepper && this.stepper()?.selectedIndex) { + if (this.stepper()?.selectedIndex === 3) { + this.save($event); + } + } + this.stepper && this.stepper()?.next(); + } + + isPreviousDisabled(): boolean { + if (this.stepper && this.stepper()?.selectedIndex) { + return this.stepper()?.selectedIndex === 0; + } + return true; + } + previous(): void { + this.stepper && this.stepper()?.previous(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.css new file mode 100644 index 000000000..83e6b7c32 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.css @@ -0,0 +1,31 @@ +.serviceName { + flex-basis: 40%; + max-width: 300px; + display: inline-block; + word-wrap: break-word; + word-break: break-all; +} +.serviceMetadata { + flex-basis: 60%; + display: inline-block; + word-wrap: break-word; + word-break: break-all; +} +.list-view-pf-description { + flex-basis: 70%; +} + +@media (min-width: 992px) { + .list-view-pf-additional-info { + width: 25%; + } +} + +:host > .tooltip-inner { + max-width: 800px; + word-wrap: break-word; +} + +.modal-dialog { + width: 900px; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.html new file mode 100644 index 000000000..241cd9be6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.html @@ -0,0 +1,106 @@ +
+ + +
+ +

+ APIs & Services +
+ +
+

+ These are the APIs | Services managed by Microcks. You can discover new ones adding Import Job or creating a new + Direct API... + +
+
+
+
+ +
+
+
+
+
+ + +
+
+
+ + +
+
+
+ + {{ service.operations.length }} Operations +
+
+
+ +
+ + +
+
+
+
+ + +
Operations
+ +
+ + + +
+ + + +

Do you really want to delete the selected API or Service ?

+ +
+ + NOTE : + This will permanently delete the API or Service definition from Microcks as well as all the + Test results. This operation cannot be undone. +
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.ts new file mode 100644 index 000000000..b4fa922b1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/services.page.ts @@ -0,0 +1,408 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CommonModule, DatePipe } from '@angular/common'; +import { + Component, + inject, + model, + OnInit, + signal +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Params, Router, RouterLink } from '@angular/router'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; + +import { PaginationConfig, PaginationEvent, PaginationModule } from '../../components/patternfly-ng/pagination'; +import { ToolbarConfig, ToolbarModule } from '../../components/patternfly-ng/toolbar'; +import { + FilterConfig, + FilterEvent, + FilterField, + FilterType, + Filter, + FilterQuery, +} from '../../components/patternfly-ng/filter'; +import { + Notification, + NotificationEvent, + NotificationService, + NotificationType, + ToastNotificationListComponent, +} from '../../components/patternfly-ng/notification'; + +import { ConfirmDeleteDialogComponent } from '../../components/confirm-delete/confirm-delete.component'; +import { LabelListComponent } from '../../components/label-list/label-list.component'; + +import { Api, Service, ServiceType } from '../../models/service.model'; +import { IAuthenticationService } from '../../services/auth.service'; +import { ConfigService } from '../../services/config.service'; +import { ServicesService } from '../../services/services.service'; +import { DirectAPIWizardComponent } from './_components/direct-api.wizard'; + +@Component({ + selector: 'app-services-page', + templateUrl: './services.page.html', + styleUrls: ['./services.page.css'], + imports: [ + CommonModule, + ConfirmDeleteDialogComponent, + LabelListComponent, + BsDropdownModule, + DatePipe, + FormsModule, + RouterLink, + PaginationModule, + ToolbarModule, + TooltipModule, + ToastNotificationListComponent, + ], +}) +export class ServicesPageComponent implements OnInit { + + modalRef?: BsModalRef; + services?: Service[]; + servicesCount: number = 0; + servicesLabels?: Map; + toolbarConfig: ToolbarConfig = new ToolbarConfig; + filterConfig: FilterConfig = new FilterConfig; + paginationConfig: PaginationConfig = new PaginationConfig; + nameFilterTerm: string | null = null; + repositoryFilter: string | null = null; + notifications: Notification[] = []; + + html = ''; + + constructor( + private servicesSvc: ServicesService, + private modalService: BsModalService, + private notificationService: NotificationService, + protected authService: IAuthenticationService, + private config: ConfigService, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit() { + this.notifications = this.notificationService.getNotifications(); + + const filterFieldsConfig = []; + if (this.hasRepositoryFilterFeatureEnabled()) { + this.getServicesLabels(); + + filterFieldsConfig.push({ + id: this.repositoryFilterFeatureLabelKey(), + title: this.repositoryFilterFeatureLabelLabel(), + placeholder: + 'Filter by ' + this.repositoryFilterFeatureLabelLabel() + '...', + type: FilterType.SELECT, + queries: [], + }); + } + + filterFieldsConfig.push({ + id: 'name', + title: 'Name', + placeholder: 'Filter by Name...', + type: FilterType.TEXT, + }); + + this.paginationConfig = { + pageNumber: 1, + pageSize: 20, + pageSizeIncrements: [], + totalItems: 20, + } as PaginationConfig; + + this.filterConfig = { + fields: filterFieldsConfig as FilterField[], + resultsCount: 20, + appliedFilters: [], + } as FilterConfig; + + this.toolbarConfig = { + actionConfig: undefined, + filterConfig: this.filterConfig, + sortConfig: undefined, + views: [], + } as ToolbarConfig; + + this.route.queryParams.subscribe((queryParams) => { + // Look at query parameters to apply filters. + this.filterConfig.appliedFilters = []; + if (queryParams['name']) { + this.nameFilterTerm = queryParams['name']; + this.filterConfig.appliedFilters.push({ + field: { title: 'Name' } as FilterField, + value: this.nameFilterTerm, + } as Filter); + } + if (queryParams['labels.' + this.repositoryFilterFeatureLabelKey()]) { + this.repositoryFilter = + queryParams['labels.' + this.repositoryFilterFeatureLabelKey()]; + this.filterConfig.appliedFilters.push({ + field: { + title: this.repositoryFilterFeatureLabelLabel(), + } as FilterField, + value: this.repositoryFilter, + } as Filter); + } + if (this.nameFilterTerm != null || this.repositoryFilter != null) { + this.filterServices(this.repositoryFilter!, this.nameFilterTerm!); + } else { + // Default - retrieve all the services + this.getServices(); + this.countServices(); + } + }); + } + + getServices(page: number = 1): void { + this.servicesSvc + .getServices(page) + .subscribe((results) => (this.services = results)); + // Update browser URL to make the page bookmarkable. + this.router.navigate([], { + relativeTo: this.route, + queryParams: {} as Params, + }); + } + filterServices(repositoryFilter: string, nameFilterTerm: string): void { + const labelsFilter = new Map(); + if (repositoryFilter != null) { + labelsFilter.set( + this.repositoryFilterFeatureLabelKey(), + repositoryFilter + ); + } + this.servicesSvc + .filterServices(labelsFilter, nameFilterTerm) + .subscribe((results) => { + this.services = results; + this.filterConfig.resultsCount = results.length; + }); + // Update browser URL to make the page bookmarkable. + const queryParams: any = { name: nameFilterTerm }; + for (const key of Array.from(labelsFilter.keys())) { + queryParams['labels.' + key] = labelsFilter.get(key); + } + this.router.navigate([], { + relativeTo: this.route, + queryParams: queryParams as Params, + queryParamsHandling: 'merge', + }); + } + + countServices(): void { + this.servicesSvc.countServices().subscribe((results) => { + this.servicesCount = results.counter; + this.paginationConfig.totalItems = this.servicesCount; + }); + } + + getServicesLabels(): void { + this.servicesSvc.getServicesLabels().subscribe((results) => { + this.servicesLabels = results; + const queries: any[] = []; + // Get only the label values corresponding to key used for filtering, then transform them for Patternfly. + if ( + this.servicesLabels && (this.servicesLabels as any)[this.repositoryFilterFeatureLabelKey()] != undefined + ) { + (this.servicesLabels as any)[this.repositoryFilterFeatureLabelKey()].map( + (label: any) => queries.push({ id: label, value: label }) + ); + } + if (queries.length === 0) { + this.filterConfig.fields = this.filterConfig.fields.filter( + (field) => field.id === 'name' + ); + } else { + this.filterConfig.fields[0].queries = queries as FilterQuery[]; + } + }); + } + + deleteService(service: Service) { + //console.log('[deleteService]: ' + JSON.stringify(service)); + this.servicesSvc.deleteService(service).subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + service.name, + 'Service has been fully deleted', + false + ); + this.getServices(); + this.servicesCount--; + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + service.name, + 'Service cannot be deleted (' + err.message + ')', + false + ); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + }); + } + + selectService(service: Service) { + this.html = '
    '; + service.operations.forEach((operation) => { + this.html += '
  • ' + operation.name + '
  • '; + }); + this.html += '
'; + } + + handlePageSize($event: PaginationEvent) { + // this.updateItems(); + } + + handlePageNumber($event: PaginationEvent) { + this.getServices($event.pageNumber); + } + + handleFilter($event: FilterEvent): void { + if (!$event.appliedFilters || $event.appliedFilters.length == 0) { + this.nameFilterTerm = null; + this.repositoryFilter = null; + this.getServices(); + } else { + $event.appliedFilters.forEach((filter) => { + if ( + this.hasRepositoryFilterFeatureEnabled() && + filter.field.id === this.repositoryFilterFeatureLabelKey() + ) { + this.repositoryFilter = filter.value; + } else { + this.nameFilterTerm = filter.value; + } + }); + this.filterServices(this.repositoryFilter!, this.nameFilterTerm!); + } + } + + openCreateDirectAPI(): void { + this.modalRef = this.modalService.show(DirectAPIWizardComponent, { class: 'modal-lg' }); + + this.modalRef.content.saveDirectAPIAction.subscribe((api: Api) => { + this.createDirectAPI(api.type, api); + }); + } + + createDirectAPI(apiType: ServiceType, api: Api): void { + switch (apiType) { + case ServiceType.GENERIC_REST: + this.servicesSvc.createDirectResourceAPI(api).subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + api.name, + 'Direct REST API "' + api.name + '" has been created', + false + ); + this.getServices(); + this.countServices(); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + api.name, + 'Service or API "' + api.name + '"already exists with version ' + api.version, + false + ); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + }); + break; + case ServiceType.GENERIC_EVENT: + this.servicesSvc.createDirectEventAPI(api).subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + api.name, + 'Direct EVENT API "' + api.name + '" has been created', + false + ); + this.getServices(); + this.countServices(); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + api.name, + 'Service or API "' + + api.name + + '"already exists with version ' + + api.version, + false + ); + }, + complete: () => {}, //console.log('Observer got a complete notification'), + }); + break; + default: + } + } + + closeDirectAPIWizardModal($event: any): void { + if (this.modalRef) { + this.modalRef.hide(); + } + } + + handleCloseNotification($event: NotificationEvent): void { + this.notificationService.remove($event.notification); + } + + public hasRole(role: string): boolean { + return this.authService.hasRole(role); + } + + public hasRoleForService(role: string, service: Service): boolean { + if (this.hasRepositoryTenancyFeatureEnabled() && service.metadata.labels) { + const tenant = + service.metadata.labels[this.repositoryFilterFeatureLabelKey()]; + if ( + tenant !== undefined && + this.authService.hasRoleForResource(role, tenant) + ) { + return true; + } + } + return this.hasRole(role); + } + + public hasRepositoryFilterFeatureEnabled(): boolean { + return this.config.hasFeatureEnabled('repository-filter'); + } + public hasRepositoryTenancyFeatureEnabled(): boolean { + return this.config.hasFeatureEnabled('repository-tenancy'); + } + + public repositoryFilterFeatureLabelKey(): string { + return this.config.getFeatureProperty('repository-filter', 'label-key'); + } + public repositoryFilterFeatureLabelLabel(): string { + return this.config.getFeatureProperty('repository-filter', 'label-label'); + } + public repositoryFilterFeatureLabelList(): string { + return this.config.getFeatureProperty('repository-filter', 'label-list'); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.css new file mode 100644 index 000000000..402d8a907 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.css @@ -0,0 +1,11 @@ + +.tiny-form-control { + color: #363636; + background-color: #fff; + border: 1px solid #bbb; + border-radius: 1px; + padding: 0px 1px; +} +.tiny-form-control:hover { + border-color: #7dc3e8; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.html new file mode 100644 index 000000000..2f46bcc73 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.html @@ -0,0 +1,254 @@ + + + + {{ getExchangeName(exchange) }} + + + + + +
+
+
+
Message
+
+
Content Type:
+
{{ getUnidirEvent(exchange).eventMessage.mediaType }}
+
+
+ + + + + + + + + + + + + +
Header nameValues
{{ header.name }}{{ v }}{{ last ? '':', '}}
+
+
+
Bindings details
+ +
+
Kafka topic:
+
+
+ + + + +
+
+
Kafka Key type:
+
{{ getBindingProperty(item, 'KAFKA', 'keyType') }}
+
+
+ +
+
MQTT topic:
+
+
+ + + + +
+
+
MQTT QoS:
+
{{ getBindingProperty(item, 'MQTT', 'qoS') }}
+
MQTT retain:
+
{{ getBindingProperty(item, 'MQTT', 'persistent') }}
+
+
+ +
+
NATS topic:
+
+
+ + + + +
+
+
+
+ +
+
WebSocket endpoint:
+
+
+ + + + +
+
+
WS method:
+
{{ getBindingProperty(item, 'WS', 'method') }}
+
+
+ +
+
AMQP exchange:
+
+
+ + + + +
+
+
AMQP exchange type:
+
{{ getBindingProperty(item, 'AMQP', 'destinationType') }}
+
+
+ +
+
Google PubSub topic:
+
+
+ + + + +
+
+
+
+ +
+
AWS SQS Queue:
+
+
+ + + + +
+
+
AWS SQS persistent:
+
{{ getBindingProperty(item, 'SQS', 'persistent') }}
+
+
+ +
+
AWS SNS Topic:
+
+
+ + + + +
+
+
+
+ +
+
AMQP destination:
+
+
+ + + + +
+
+
+
+
+
+
+
+
Request
+
+
+ Mock URL: + + + +
+
+
+ + + + + + + +
+
+
+
+
+
Variables
+
+
+
+
+ + + + + + + + + + + + + + +
Header nameValues
{{ header.name }}{{ v }}{{ last ? '':', '}}
+
+
+
Response
+
+
Response Code and Type:
+
+ {{ getReqRespPair(exchange).response.status }}: {{getReqRespPair(exchange).response.mediaType}} +
+
+
+ + + + + + + + + + + + + +
Header nameValues
{{ header.name }}{{ v }}{{ last ? '':', '}}
+
+
+ +
+
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.ts new file mode 100644 index 000000000..595ecba27 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/exchanges-tabset/exchanges-tabset.component.ts @@ -0,0 +1,82 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, ViewChild, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TabsetComponent, TabsModule } from 'ngx-bootstrap/tabs'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; +import { HighlightAuto } from 'ngx-highlightjs'; + +import { NotificationService } from '../../../../../components/patternfly-ng/notification'; + +import { ServiceView, Operation, Exchange, EventMessage, Parameter, RequestResponsePair, UnidirectionalEvent } from '../../../../../models/service.model'; +import { ConfigService } from '../../../../../services/config.service'; + +@Component({ + selector: 'app-exchanges-tabset', + templateUrl: './exchanges-tabset.component.html', + styleUrls: ['./exchanges-tabset.component.css'], + imports: [ + CommonModule, + FormsModule, + HighlightAuto, + TabsModule, + TooltipModule + ] +}) +export class ExchangesTabsetComponent { + + readonly hlLang: string[] = ['json', 'xml', 'yaml']; + + @ViewChild('tabs', { static: true }) tabs!: TabsetComponent; + + @Input() public item!: Operation; + @Input() public view!: ServiceView; + @Input() public resolvedServiceView!: ServiceView; + @Input() public notificationService!: NotificationService; + @Input() public config!: ConfigService; + @Input() public urlType!: string; + + @Input() public isEventTypeService!: () => boolean; + @Input() public getExchangeName!: (exchange: Exchange) => string; + @Input() public getExchangeSourceArtifact!: (exchange: Exchange) => string; + @Input() public hasBinding!: (operation: Operation, binding: string) => boolean; + @Input() public getBindingProperty!: (operation: Operation, binding: string, property: string) => string | null; + @Input() public formatMockUrl!: (operation: Operation, dispatchCriteria: string, queryParameters: Parameter[]) => string; + @Input() public formatAsyncDestination!: (operation: Operation, eventMessage: EventMessage, binding: string) => string; + @Input() public getDestinationOperationPart!: (peration: Operation, eventMessage: EventMessage) => string; + @Input() public formatRequestContent!: (requestContent: string) => string; + @Input() public formatGraphQLVariables!: (requestContent: string) => string; + @Input() public prettyPrintIfJSON!: (content: string) => string; + @Input() public formatCurlCmd!: (operation: Operation, exchange: RequestResponsePair) => string; + @Input() public copyToClipboard!: (url: string, what?: string) => void; + @Input() public encodeUrl!: (url: string) => string; + @Input() public removeVerbInUrl!: (operationName: string) => string; + @Input() public asyncAPIFeatureEndpoint!: (binding: string) => string; + + public shouldRender(index: number) { + const activeTab = this.tabs.tabs.filter((tab) => tab.active )[0]; + return index == this.tabs.tabs.indexOf(activeTab); + } + + getReqRespPair(exchange: Exchange): RequestResponsePair { + return exchange as RequestResponsePair; + } + getUnidirEvent(exchange: Exchange): UnidirectionalEvent { + return exchange as UnidirectionalEvent; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.css new file mode 100644 index 000000000..ef56d99e4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.css @@ -0,0 +1,10 @@ +.samples-container { + margin-top: 20px; + min-height: 200px; +} +.sample-container { + margin-bottom: 20px; +} +.modal-footer { + margin-top: 20px; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.html new file mode 100644 index 000000000..bc59e8c8a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.html @@ -0,0 +1,145 @@ + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.ts new file mode 100644 index 000000000..57c0dbfa1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generate-samples.dialog.ts @@ -0,0 +1,208 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { HighlightAuto } from 'ngx-highlightjs'; + +import { AICopilotService } from '../../../../services/aicopilot.service'; +import { + Service, + ServiceType, + Exchange, + RequestResponsePair, + UnidirectionalEvent, +} from '../../../../models/service.model'; +import { TabsModule } from 'ngx-bootstrap/tabs'; + +@Component({ + selector: 'app-generate-samples-dialog', + templateUrl: './generate-samples.dialog.html', + styleUrls: ['./generate-samples.dialog.css'], + imports: [ + CommonModule, + FormsModule, + HighlightAuto, + TabsModule + ] +}) +export class GenerateSamplesDialogComponent implements OnInit { + @Output() saveSamplesAction = new EventEmitter(); + + closeBtnName!: string; + service!: Service; + operationName!: string; + + infoMessage?: string; + errorMessage?: string; + saveEnabled = false; + + exchanges: Exchange[] = []; + selectedExchanges: (Exchange | undefined)[] = []; + exchangesNames: string[] = []; + + constructor( + private copilotSvc: AICopilotService, + public bsModalRef: BsModalRef + ) {} + + ngOnInit() { + this.getSamplesSuggestions(2); + /* + this.exchanges.push({"request": {}, "response": {}}); + this.exchanges.push({"request": {}, "response": {}}); + this.selectedExchanges.push({"request": {}, "response": {}}); + this.infoMessage = 'You need to rename and may unselect samples before saving them'; + */ + } + + isEventTypeService(): boolean { + return ( + this.service.type === ServiceType.EVENT || + this.service.type === ServiceType.GENERIC_EVENT + ); + } + + getSamplesSuggestions(numberOfSamples: number = 2): void { + this.copilotSvc + .getSamplesSuggestions(this.service, this.operationName, numberOfSamples) + .subscribe({ + next: (res) => { + if (res.length == 0) { + this.infoMessage = + 'AI Copilot was not able to analyse your specification and provide samples in specified delay. Please retry later...'; + } else { + res.forEach((exchange) => { + if ((exchange as any)['eventMessage'] != undefined) { + exchange.type = 'unidirEvent'; + } else { + exchange.type = 'reqRespPair'; + } + }); + this.exchanges.push(...res); + this.selectedExchanges.push(...res); + this.infoMessage = + 'You need to rename and may unselect samples before saving them'; + this.errorMessage = undefined; + } + }, + error: (err) => { + console.log('Observer got an error: ' + JSON.stringify(err)); + this.errorMessage = 'Got an error on server side: ' + err.error; + this.infoMessage = undefined; + }, + complete: () => {} //console.log('Observer got a complete notification'), + }); + } + + getOtherSamples(): void { + this.infoMessage = undefined; + this.errorMessage = undefined; + this.copilotSvc + .getSamplesSuggestions(this.service, this.operationName, 2) + .subscribe({ + next: (res) => { + if (res.length == 0) { + this.infoMessage = + 'AI Copilot was not able to analyse your specification and provide samples in specified delay. Please retry later...'; + } else { + res.forEach((exchange) => { + if ((exchange as any)['eventMessage'] != undefined) { + exchange.type = 'unidirEvent'; + } else { + exchange.type = 'reqRespPair'; + } + }); + this.exchanges.push(...res); + this.selectedExchanges.push(...res); + this.infoMessage = + 'You need to rename and may unselect samples before saving them'; + this.errorMessage = undefined; + } + }, + error: (err) => { + console.log('Observer got an error: ' + JSON.stringify(err)); + this.errorMessage = 'Got an error on server side: ' + err.error; + this.infoMessage = undefined; + }, + complete: () => {} //console.log('Observer got a complete notification'), + }); + } + + getExchangeName(index: number): string { + if (this.exchangesNames[index] === undefined) { + return 'Sample ' + index; + } + return this.exchangesNames[index]; + } + + getReqRespPair(exchange: Exchange): RequestResponsePair { + return exchange as RequestResponsePair; + } + getUnidirEvent(exchange: Exchange): UnidirectionalEvent { + return exchange as UnidirectionalEvent; + } + + toggleSelectedExchange(index: number): void { + if (this.selectedExchanges[index] == undefined) { + this.selectedExchanges[index] = this.exchanges[index]; + } else { + this.selectedExchanges[index] = undefined; + } + this.isSavingEnabled(); + } + + updateSampleName($event: Event, index: number): void { + this.exchangesNames[index] = String($event); + if (!this.isEventTypeService()) { + // Dissociate request/response pair... + (this.exchanges[index] as RequestResponsePair).request.name = + String($event); + (this.exchanges[index] as RequestResponsePair).response.name = + String($event); + (this.selectedExchanges[index] as RequestResponsePair).request.name = + String($event); + (this.selectedExchanges[index] as RequestResponsePair).response.name = + String($event); + } else { + // ... from unidirectional event. + (this.exchanges[index] as UnidirectionalEvent).eventMessage.name = + String($event); + (this.selectedExchanges[index] as UnidirectionalEvent).eventMessage.name = + String($event); + } + this.isSavingEnabled(); + } + + isSavingEnabled(): void { + let missingSomething = false; + this.selectedExchanges.forEach((exchange, index) => { + if (exchange != undefined) { + if (this.exchangesNames[index] == undefined) { + missingSomething = true; + } + } + }); + this.saveEnabled = !missingSomething && this.selectedExchanges.length > 0; + } + + saveSamples(): void { + this.saveSamplesAction.emit(this.selectedExchanges.filter((e) => e != undefined)); + this.bsModalRef.hide(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generic-resources.dialog.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generic-resources.dialog.css new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generic-resources.dialog.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generic-resources.dialog.html new file mode 100644 index 000000000..d1138427c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generic-resources.dialog.html @@ -0,0 +1,35 @@ + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generic-resources.dialog.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generic-resources.dialog.ts new file mode 100644 index 000000000..159cc5ca6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/generic-resources.dialog.ts @@ -0,0 +1,84 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { PaginationConfig, PaginationEvent, PaginationModule } from '../../../../components/patternfly-ng/pagination'; + +import { ServicesService } from '../../../../services/services.service'; +import { Service, GenericResource } from '../../../../models/service.model'; + +@Component({ + selector: 'app-generic-resources-dialog', + templateUrl: './generic-resources.dialog.html', + styleUrls: ['./generic-resources.dialog.css'], + imports: [ + CommonModule, + PaginationModule + ] +}) +export class GenericResourcesDialogComponent implements OnInit { + + title?: string; + closeBtnName!: string; + paginationConfig!: PaginationConfig; + service!: Service; + + resources!: GenericResource[]; + resourcesCount!: number; + + constructor(private servicesSvc: ServicesService, public bsModalRef: BsModalRef) {} + + ngOnInit() { + this.getGenericResources(); + this.countGenericResources(); + + this.paginationConfig = { + pageNumber: 1, + pageSize: 20, + pageSizeIncrements: [], + totalItems: 20 + } as PaginationConfig; + } + + getGenericResources(page: number = 1): void { + this.servicesSvc.getGenericResources(this.service).subscribe(results => this.resources = results); + } + + countGenericResources(): void { + this.servicesSvc.countGenericResources(this.service).subscribe(results => { + this.resourcesCount = results.counter; + this.paginationConfig.totalItems = this.resourcesCount; + }); + } + + handlePageSize($event: PaginationEvent) { + // this.updateItems(); + } + + handlePageNumber($event: PaginationEvent) { + this.getGenericResources($event.pageNumber); + } + + public printPaylaod(payload: any): string { + if (payload != undefined && payload != null) { + return JSON.stringify(payload); + } + return 'Cannot render payload as string !'; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.css new file mode 100644 index 000000000..5d11a8f3a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.css @@ -0,0 +1,7 @@ +.first-col { + padding-left: 40px; +} + +.samples-container { + margin-top: 20px; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.html new file mode 100644 index 000000000..432846628 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.html @@ -0,0 +1,86 @@ + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.ts new file mode 100644 index 000000000..5e2b32082 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/_components/manage-samples.dialog.ts @@ -0,0 +1,221 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { ListConfig, ListModule } from '../../../../components/patternfly-ng/list'; + +import { Exchange, RequestResponsePair, ServiceType, ServiceView, UnidirectionalEvent } from '../../../../models/service.model'; +import { IAuthenticationService } from '../../../../services/auth.service'; + +@Component({ + selector: 'app-manage-samples-dialog', + templateUrl: './manage-samples.dialog.html', + styleUrls: ['./manage-samples.dialog.css'], + imports: [ + CommonModule, + BsDropdownModule, + FormsModule, + ListModule + ] +}) +export class ManageSamplesDialogComponent implements OnInit { + //@Output() cleanupSelectionAction = new EventEmitter(); + @Output() cleanupSelectionAction = new EventEmitter>>(); + + closeBtnName!: string; + serviceView!: ServiceView; + operationsWithAISamples: any[] = []; + operationsListConfig!: ListConfig; + + //selectedExchanges: any = {}; + selectedExchanges: Record> = {}; + exportFormat: string = 'APIExamples'; + + constructor( + private authService: IAuthenticationService, + public bsModalRef: BsModalRef + ) {} + + ngOnInit() { + this.operationsListConfig = { + dblClick: false, + //emptyStateConfig: null, + multiSelect: false, + selectItems: false, + selectionMatchProp: 'name', + showCheckbox: false, + showRadioButton: false, + useExpandItems: true, + hideClose: true + } as ListConfig; + + this.serviceView.service.operations.forEach(operation => { + let exchanges = this.getOperationAICopilotExchanges(operation.name); + if (exchanges.length > 0) { + this.operationsWithAISamples.push(operation); + if (this.selectedExchanges[operation.name] == undefined) { + this.selectedExchanges[operation.name] = {}; + } + exchanges.forEach(exchange => { + this.selectedExchanges[operation.name][this.getExchangeName(exchange)] = true; + }); + } + }); + + this.operationsWithAISamples.forEach(operation => { + operation.expanded = true; + }); + } + + public selectAllExchanges(): void { + this.operationsWithAISamples.forEach(operation => { + if (this.selectedExchanges[operation.name] == undefined) { + this.selectedExchanges[operation.name] = {}; + } + this.getOperationAICopilotExchanges(operation.name).forEach(exchange => { + this.selectedExchanges[operation.name][this.getExchangeName(exchange)] = true; + }); + }); + } + public unselectAllExchanges(): void { + this.operationsWithAISamples.forEach(operation => { + this.selectedExchanges[operation.name] = {}; + }); + } + public cleanupEnabled(): boolean { + let enabled = false; + this.operationsWithAISamples.forEach(operation => { + // Have a quick look at defintiion and keys. + if (this.selectedExchanges[operation.name] != undefined + && Object.keys(this.selectedExchanges[operation.name]).length > 0 + ) { + // Now inspect the values. + Object.values(this.selectedExchanges[operation.name]).forEach(value => { + if (value === true) { + enabled = true; + return; + } + }); + } + }); + return enabled; + } + + public isEventTypeService(): boolean { + return ( + this.serviceView.service.type === ServiceType.EVENT || + this.serviceView.service.type === ServiceType.GENERIC_EVENT + ); + } + public getExchangeName(exchange: Exchange): string { + if (this.isEventTypeService()) { + return (exchange as UnidirectionalEvent).eventMessage.name; + } else { + return (exchange as RequestResponsePair).request.name; + } + } + + public halfOperationExchanges(operationName: string): Exchange[] { + let exchanges = this.getOperationAICopilotExchanges(operationName); + return exchanges.slice(0, (exchanges.length / 2) + 1); + } + + public secondHalfOperationExchanges(operationName: string): Exchange[] { + let exchanges = this.getOperationAICopilotExchanges(operationName); + return exchanges.slice((exchanges.length / 2) + 1, exchanges.length); + } + + public cleanupSelection(): void { + // Remove exchanges that are not selected. + this.operationsWithAISamples.forEach(operation => { + Object.keys(this.selectedExchanges[operation.name]).forEach(exchangeName => { + if (this.selectedExchanges[operation.name][exchangeName] === false) { + delete this.selectedExchanges[operation.name][exchangeName]; + } + }); + }); + this.cleanupSelectionAction.emit(this.selectedExchanges); + this.bsModalRef.hide(); + } + + public exportSelection(): void { + // Remove exchanges that are not selected. + this.operationsWithAISamples.forEach(operation => { + Object.keys(this.selectedExchanges[operation.name]).forEach(exchangeName => { + if (this.selectedExchanges[operation.name][exchangeName] === false) { + delete this.selectedExchanges[operation.name][exchangeName]; + } + }); + }); + let exchangeSelection: { serviceId: string; exchanges: Record } = { + serviceId: this.serviceView.service.id, + exchanges: {} + }; + Object.keys(this.selectedExchanges).forEach((operationName) => { + exchangeSelection.exchanges[operationName] = []; + Object.keys(this.selectedExchanges[operationName]).forEach((exchangeName) => { + exchangeSelection.exchanges[operationName]!.push(exchangeName); + }); + }); + + // Now download the selected exchanges. + let downloadPath = '/api/copilot/samples/' + this.serviceView.service.id + '/export?format=' + this.exportFormat; + + // Just opening a window with the download path is not working + // because Authorization header is not sent. + //window.open(downloadPath, '_blank', ''); + + // So we have to use XMLHttpRequest to send Authorization header and get the file + // before triggering the Save as dialog by simulating a click on a link. + const xhr = new XMLHttpRequest(); + xhr.open('POST', location.origin + downloadPath, true); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Authorization', 'Bearer ' + this.authService.getAuthenticationSecret()); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + const blob = new Blob([xhr.response], { type: 'text/plain' }); + const url = window.URL.createObjectURL(blob); + + var a = document.createElement("a"); + document.body.appendChild(a); + a.href = url; + a.download = 'api-examples.yaml'; + a.click(); + + window.URL.revokeObjectURL(url); + } else { + alert('Problem while retrieving APIExamples export'); + } + } + }; + xhr.send(JSON.stringify(exchangeSelection)); + } + + getOperationAICopilotExchanges(operationName: string): Exchange[] { + let exchanges = this.serviceView.messagesMap[operationName]; + return exchanges.filter(exchange => + this.isEventTypeService() ? + (exchange as UnidirectionalEvent).eventMessage.sourceArtifact === 'AI Copilot' + : (exchange as RequestResponsePair).request.sourceArtifact === 'AI Copilot' + ); + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.css new file mode 100644 index 000000000..501d342ad --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.css @@ -0,0 +1,58 @@ +.help-bar { + background-color: #f1f1f1; + border-left: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} +.code-block { + display: inline-block; + white-space: pre-wrap; +} + +.parameters-constraints { + padding-bottom: 20px; +} + +.constraint-editor-entry-header { + padding-right: 50px; +} +.constraint-editor-header { + float: left; + margin-bottom: 5px; + padding-right: 5px; + width: 25%; +} + +.constraint-editor .constraint-editor-entry { + display: table; + margin-bottom: 10px; + padding-right: 50px; + position: relative; + table-layout: fixed; + width: 100%; +} + +.constraint-editor .constraint-editor-input{ + float: left; + margin-bottom: 0; + padding-right: 5px; + width: 25%; +} + +.constraint-editor .constraint-editor-buttons { + position: absolute; + right: 0; + top: 0; + width: 50px; + vertical-align: middle; +} + +.constraint-editor .as-item-delete{ + display: inline-block; + font-size: 14px; + opacity: .65; + padding: 5px; + vertical-align: middle; + padding-top: 5px; + padding-bottom: 5px; + color: #333; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.html new file mode 100644 index 000000000..0235fde14 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.html @@ -0,0 +1,340 @@ +
+ + +
+ +
+ + +
+
+

{{ view.service.name }} - {{ view.service.version }} - {{ operationName }}

+ Review and adapt parameters constraints and dispatcher properties for mocks on operation {{ operationName }}. Be sure to know what you're doing: it may break your mocks! + + + +
+
Query parameters
+
+
Name
+
Required?
+
Recopy?
+
Match Regexp?
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+ Add Parameter Constraint + +
Header parameters
+
+
Name
+
Required?
+
Recopy?
+
Match Regexep?
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+ Add Header Constraint +
+
+   + +
+


+ + + +
+
+
+ +
SOAP Action:
+
{{ operation!.action }}
+
+
Dispatcher:
+
{{ operation!.dispatcher }}   + + Learn More + + + +
+
Dispatching rules:
+
{{ operation!.dispatcherRules }}
+
+
+
+
+ +
Message Output Name:
+
{{ operation!.outputName }}
+
+
HTTP Verb:
+
Type:
+
+
+ GET + PUT + DELETE + POST + {{ operation!.method }} +
+
+ +
Default delay:
+
{{ operation?.defaultDelay || 0 }} ms
+
+ +
Frequency:
+
{{ operation?.defaultDelay || 0 }} sec
+
+
+
+
+ + + +
+
+
+ + +
+ ms +

The delay to wait before returning response.

+
+
+ + +
+   sec +
+ + +
+

The frequency of messages publication.

+
+
+
+
+ +
+ +

The dispatcher used by Operation.

+
+
+
+ +
+ +

The dispatcher rules for above dispatcher.

+
+
+
+
+ + + +
+ +

{{ getReqRespPair(exchange).response.name }} - {{ getReqRespPair(exchange).response.dispatchCriteria }}

+
+ +

{{ getUnidirEvent(exchange).eventMessage.name }}

+
+
+
+
+

+
+   + +
+
+
+ + This sidebar provides some explanations and examples on how to setup parameter constraints and use the evolved dispatchers. +
+

Parameters Constraints

+

+ Parameters constraints allows you to define expectations regarding incoming header or query parameters values. + Adding constraints add new behaviour on your operation mocks so that they can: +

+
    +
  • check presence of parameter using the required flag,
  • +
  • recopy incoming parameter value into an outgoing header (think of transaction propagation for example),
  • +
  • match parameter value using a `regular expression` (think of Authorization token validation for example)
  • +
+ +
+
+

Fallback Dispatcher

+

+ Fallback dispatcher allows you to declare a fallback response in case the incoming request to your mocks do not + match any existing dispatch criteria. +

+

+ Consider the following case where you have a single GET /user/:name operation. You've defined mocks responses called + Andrew, Anna and John Doe that are matching with corresponding :name resource path. + What if name does not have one of these values? +

+

+ By default Microcks will return a 404 error. But if you've defined a FALLBACK dispatcher, you'll default to + the response of your choice. Just say you want to default to John Doe response: +

+
+ +
+
{{ fallback }}
+
+ +
+
+

Proxy-Fallback Dispatcher

+

+ Proxy-Fallback dispatcher allows you to forward the incoming request to a proxyUrl in case it do not match + any existing dispatch criteria. +

+

+ Consider the following case where you have a single GET /user/:name operation. You've defined mocks responses called + Andrew, Anna and John Doe that are matching with corresponding :name resource path. + What if name does not have one of these values? +

+

+ By default Microcks will return a 404 error. But if you've defined a PROXY dispatcher, Microcks will forward + the original request to the external URL, the base URL of which was defined in proxyUrl: +

+
+ +
+
{{ proxyFallback }}
+
+ +
+
+

JSON Body Dispatcher

+

+ JSON Body dispatcher allows you to introspect the content of incoming JSON payload for matching values that you'll map to response. + It is paricully well suited for dealing with POST requests where you'll want different responses depending on request content. +

+

+ Consider the following JSON payload representing a request body for creating new Beer entity: +

+
{{ examplePayload }}
+ +

+ Using the JSON_BODY dispatcher, you can select target response depending of the value of a payload element. + Below we are checking for country with the equals operator. Note that you should always have a default case. +

+
+ +
+
{{ equalsOperator }}
+ +

+ You can also dispatch depending on a payload number value using the range operator. Note the orientation + of brackets to include or exclude edge values. default is still required. +

+
+ +
+
{{ rangeOperator }}
+ +

+ The size operator allows you to define dispatching rules depending on the size of an array. Here we only accept + beers that have at least 2 references and at most 100. +

+
+ +
+
{{ sizeOperator }}
+ +

+ For much more complex rules, you may use the regexp operator. There's no guarantee that cases will in the given + order so regular expressions should be exclusives. Otherwise, it falls within the default case. +

+
+ +
+
{{ regexpOperator }}
+ +

+ Finally, you may want to dispatch according the presence of an element within JSON payload. For that, you'll use the presence + operator that implies defining one found OR one missing case. Alternative falls into default case. +

+
+ +
+
{{ presenceOperator }}
+
+
+
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.ts new file mode 100644 index 000000000..c59c051af --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/operation/operation-override.page.ts @@ -0,0 +1,344 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, ParamMap, RouterLink } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { + Notification, + NotificationEvent, + NotificationService, + NotificationType, + ToastNotificationListComponent, +} from '../../../../components/patternfly-ng/notification'; + +import { + Operation, + ServiceType, + ServiceView, + OperationMutableProperties, + Exchange, + UnidirectionalEvent, + RequestResponsePair, +} from '../../../../models/service.model'; +import { ServicesService } from '../../../../services/services.service'; +import { ConfigService } from '../../../../services/config.service'; + +@Component({ + selector: 'app-operation-override-page', + templateUrl: './operation-override.page.html', + styleUrls: ['./operation-override.page.css'], + imports: [ + CommonModule, + FormsModule, + RouterLink, + ToastNotificationListComponent + ], +}) +export class OperationOverridePageComponent implements OnInit { + serviceId!: string; + operationName!: string; + serviceView: Observable | null = null; + resolvedServiceView!: ServiceView; + operation?: Operation; + newOperation?: Operation; + notifications: Notification[] = []; + frequencies: string[] = []; + paramConstraints: any = { + header: [], + query: [], + }; + + dispatchersByServiceType: any = { + REST: [ + { value: 'SEQUENCE', label: 'SEQUENCE' }, + { value: 'URI_PARAMS', label: 'URI PARAMS' }, + { value: 'URI_PARTS', label: 'URI PARTS' }, + { value: 'URI_ELEMENTS', label: 'URI ELEMENTS' }, + { value: 'QUERY_HEADER', label: 'QUERY HEADER' }, + { value: 'SCRIPT', label: 'SCRIPT' }, + { value: 'JSON_BODY', label: 'JSON BODY' }, + { value: 'PROXY', label: 'PROXY' }, + { value: 'FALLBACK', label: 'FALLBACK' }, + { value: 'PROXY_FALLBACK', label: 'PROXY FALLBACK' }, + ], + SOAP: [ + { value: 'QUERY_MATCH', label: 'QUERY MATCH' }, + { value: 'SCRIPT', label: 'SCRIPT' }, + { value: 'PROXY', label: 'PROXY' }, + { value: 'FALLBACK', label: 'FALLBACK' }, + { value: 'PROXY_FALLBACK', label: 'PROXY FALLBACK' }, + ], + EVENT: [], + GRPC: [ + { value: '', label: '' }, + { value: 'QUERY_ARGS', label: 'QUERY_ARGS' }, + { value: 'JSON_BODY', label: 'JSON BODY' }, + { value: 'FALLBACK', label: 'FALLBACK' }, + ], + GRAPHQL: [ + { value: '', label: '' }, + { value: 'QUERY_ARGS', label: 'QUERY_ARGS' }, + { value: 'JSON_BODY', label: 'JSON BODY' }, + { value: 'SCRIPT', label: 'SCRIPT' }, + { value: 'PROXY', label: 'PROXY' }, + { value: 'FALLBACK', label: 'FALLBACK' }, + { value: 'PROXY_FALLBACK', label: 'PROXY FALLBACK' }, + ], + }; + + fallback = `{ + "dispatcher": "URI_PARTS", + "dispatcherRules": "name", + "fallback": "John Doe" +}`; + + proxyFallback = `{ + "dispatcher": "URI_PARTS", + "dispatcherRules": "name", + "proxyUrl": "http://external.net/" +}`; + + examplePayload = `{ + "name": "Abbey Brune", + "country": "Belgium", + "type": "Brown ale", + "rating": 4.2, + "references": [ + { "referenceId": 1234 }, + { "referenceId": 5678 } + ] +}`; + + equalsOperator = `{ + "exp": "/country", + "operator": "equals", + "cases": { + "Belgium": "Accepted", + "default": "Not accepted" + } +}`; + + rangeOperator = `{ + "exp": "/rating", + "operator": "range", + "cases": { + "[4.2;5.0]": "Top notch", + "[3;4.2[": "Medium", + "default": "Not accepted" + } +}`; + + sizeOperator = `{ + "exp": "/references", + "operator": "size", + "cases": { + "[2;100]": "Good references", + "default": "Not enough references" + } +}`; + + regexpOperator = `{ + "exp": "/type", + "operator": "regexp", + "cases": { + ".*[Aa][Ll][Ee].*": "Ale beers", + "default": "Not accepted" + } +}`; + + presenceOperator = `{ + "exp": "/name", + "operator": "presence", + "cases": { + "found": "Got a name", + "default": "Missing a name" + } +}`; + + constructor( + private servicesSvc: ServicesService, + private config: ConfigService, + private notificationService: NotificationService, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit() { + this.notifications = this.notificationService.getNotifications(); + this.operationName = this.route.snapshot.paramMap.get('name')!; + this.serviceView = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.servicesSvc.getServiceView(params.get('serviceId')!) + ) + ); + this.serviceView.subscribe((view) => { + this.serviceId = view.service.id; + this.resolvedServiceView = view; + for (const operation of this.resolvedServiceView.service.operations) { + if (this.operationName === operation.name) { + this.operation = operation; + // Clone mutable properties from operation. + this.newOperation = {} as Operation; + this.newOperation.defaultDelay = this.operation.defaultDelay || 0; + this.newOperation.dispatcher = this.operation?.dispatcher ?? ''; + this.newOperation.dispatcherRules = this.operation.dispatcherRules; + this.newOperation.parameterConstraints = + this.operation.parameterConstraints; + if (this.newOperation.parameterConstraints) { + for (const constraint of this.newOperation.parameterConstraints) { + this.paramConstraints[constraint.in].push(constraint); + } + } + break; + } + } + }); + this.frequencies = this.config + .getFeatureProperty('async-api', 'frequencies') + .split(','); + } + + getReqRespPair(exchange: Exchange): RequestResponsePair { + return exchange as RequestResponsePair; + } + getUnidirEvent(exchange: Exchange): UnidirectionalEvent { + return exchange as UnidirectionalEvent; + } + + public resetOperationProperties() { + this.newOperation = {} as Operation; + if (this.operation) { + this.newOperation.defaultDelay = this.operation?.defaultDelay ?? 0; + this.newOperation.dispatcher = this.operation?.dispatcher ?? ''; + this.newOperation.dispatcherRules = this.operation.dispatcherRules; + } + } + public saveOperationProperties() { + const operationProperties = {} as OperationMutableProperties; + if (this.newOperation) { + operationProperties.defaultDelay = this.newOperation.defaultDelay; + operationProperties.dispatcher = this.newOperation.dispatcher; + operationProperties.dispatcherRules = this.newOperation.dispatcherRules; + } + operationProperties.parameterConstraints = []; + // Now recopy parameter constraints. + for (let i = 0; i < this.paramConstraints.header.length; i++) { + operationProperties.parameterConstraints.push(this.paramConstraints.header[i]); + } + for (let i = 0; i < this.paramConstraints.query.length; i++) { + operationProperties.parameterConstraints.push(this.paramConstraints.query[i]); + } + + console.log( + "[saveOperationProperties] operationProperties: " + + JSON.stringify(operationProperties) + ); + this.servicesSvc + .updateServiceOperationProperties( + this.resolvedServiceView.service, + this.operationName, + operationProperties + ) + .subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + this.operationName, + 'Dispatch properies have been updated', + false + ); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + this.operationName, + 'Dispatch properties cannot be updated (' + err.message + ')', + false + ); + }, + complete: () => console.log('Observer got a complete notification'), + }); + } + + public copyDispatcherRules(operator: string): void { + if (this.newOperation) { + this.newOperation.dispatcherRules = operator; + } + } + + public addParameterConstraint(location: string): void { + const parameterConstraints = this.paramConstraints[location]; + if (parameterConstraints == null) { + this.paramConstraints[location] = [ + { + name: 'my-header', + in: location, + required: false, + recopy: false, + mustMatchRegexp: null, + }, + ]; + } else { + this.paramConstraints[location].push({ + name: 'my-header', + in: location, + required: false, + recopy: false, + mustMatchRegexp: null, + }); + } + } + + public removeParameterConstraint(location: string, index: number): void { + const parameterConstraints = this.paramConstraints[location]; + if (parameterConstraints != null) { + parameterConstraints.splice(index, 1); + } + } + + public isEventTypeService(): boolean { + return ( + this.resolvedServiceView.service.type === ServiceType.EVENT || + this.resolvedServiceView.service.type === ServiceType.GENERIC_EVENT + ); + } + + public isAsyncMockEnabled(): boolean { + return ( + this.config.getFeatureProperty('async-api', 'enabled').toLowerCase() === + 'true' && this.newOperation?.defaultDelay != 0 + ); + } + public disableAsyncMock(): void { + if (this.newOperation) { + this.newOperation.defaultDelay = 0; + } + } + public enableAsyncMock(): void { + if (this.newOperation) { + this.newOperation.defaultDelay = parseInt(this.frequencies[0]); + } + } + + handleCloseNotification($event: NotificationEvent): void { + this.notificationService.remove($event.notification); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.css new file mode 100644 index 000000000..e2c4f4c5a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.css @@ -0,0 +1,38 @@ +.code-block { + display: inline-block; + white-space: pre-wrap; +} + +.metadata-labels { + margin-top: 10px; +} + +.async-mock-status-label { + margin-left: 20px; +} + +.conformance-card { + margin-right: 20px !important; +} +.conformance-score { + font-size: 38px; + font-weight: 300; +} +.conformance-trend { + margin-left: 10px; + font-size: 24px; +} +.conformance-trend.low-up { + transform: rotate(45deg); +} +.conformance-trend.low-down { + transform: rotate(-45deg); +} + +.conformance-index > dt { + margin-bottom: 10px; +} + +.tooltip-break { + word-break: break-word; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.html new file mode 100644 index 000000000..4e3bddade --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.html @@ -0,0 +1,354 @@ +
+ + +
+ +
+ + +

{{ view.service.name }} - {{ view.service.version }}

+ Created {{ view.service.metadata.createdOn.toString() | timeAgo }}, last update on {{ + view.service.metadata.lastUpdate | date : 'medium' }} + + + +
+
+ +
+
Service Type:
+
+ SOAP + REST + EVENT + GRPC + GRAPH + DIRECT REST + DIRECT EVENT +
+ +
Namespace:
+
{{ view.service.xmlNS }}
+
+
Contracts:
+
+ + + + + {{ contract.name }} {{ contract.type }} +
+ + View Documentation +
+
+
+ No contracts attached +
+ Loading contracts... + + + + Swagger 2.0 spec +
+ + OpenAPI 3.0 spec + +
+
+
Statistics:
+
+ Mocks invocations +
+ +
MCP Server:
+
+ + HTTP SSE Endpoint +
+ + HTTP Streamable Endpoint + +
+
+
+
+
+
+
+

Tests

+
+
+
+
+
+
Conformance index
+ +
+ +
+
+
Conformance score
+ {{ (conformanceMetric.currentScore | number:'.1-2') || '0.0' }}% + +
+
+ Loading test conformance metric... You may need to update your service + first. +
+ +
+ +
+
+
+
+ +

+ You may have already used this Direct API to record resources.
+ Check what's in the repository . +

+
+
+
+ +
+ +
+ +
+ + + +
+
+
+
+
{{ item.name }}
+
+ + GET + PUT + DELETE + POST + {{ item.method || 'POST'}} + + + with {{ item.dispatcher }} dispatcher + +
+
+
+
+ + {{ view.messagesMap[item.name].length }} sample(s) +
+
+ + +
+
+ + + +
+
+
+ +
SOAP Action:
+
{{ item.action }}
+
+ +
Dispatcher:
+
{{ item.dispatcher }}   + + Learn More + + + +
+
Dispatching rules:
+
{{ item.dispatcherRules }}
+
+ +
Frequency:
+
+ {{ item.defaultDelay || 0 }} s + Mocks are + disabled +
+
Available bindings:
+
+ {{ getBindingsList(item) }}   + + Learn More + + + +
+
+
+
+
+
+ +
Kafka endpoint:
+
{{ asyncAPIFeatureEndpoint('KAFKA') }}
+
+ +
MQTT endpoint:
+
{{ asyncAPIFeatureEndpoint('MQTT') }}
+
+ +
WebSocket endpoint:
+
{{ asyncAPIFeatureEndpoint('WS') }}
+
+ +
NATS endpoint:
+
{{ asyncAPIFeatureEndpoint('NATS') }}
+
+ +
AMQP endpoint:
+
{{ asyncAPIFeatureEndpoint('AMQP') }}
+
+ +
Google PubSub project:
+
{{ asyncAPIFeatureEndpoint('GOOGLEPUBSUB') }}
+
+ +
Amazon SQS region:
+
{{ asyncAPIFeatureEndpoint('SQS') }}
+
+ +
Amazon SNS region:
+
{{ asyncAPIFeatureEndpoint('SNS') }}
+
+
+
+ +
Input / Output Name:
+
{{ item.inputName }} / {{ item.outputName }}
+
+
HTTP Verb:
+
+
+ GET + PUT + DELETE + POST + {{ item.method || 'POST'}} +
+
+
Default delay:
+
{{ item.defaultDelay || 0 }} ms
+
+
+
+
+
Mock URL:
+
+
+ + + + +
+
+
+ + +
+
+
+ + +
Query param {{ constraint.name }}:
+
+
+
+
+
+
+
+ + +
Header {{ constraint.name }}:
+
+
+
+
+
+
+ + + +
+ +
+
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.ts new file mode 100644 index 000000000..ecef1df11 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/services/{serviceId}/service-detail.page.ts @@ -0,0 +1,1002 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Component, + OnInit, + ChangeDetectionStrategy, + ChangeDetectorRef, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router, ParamMap, RouterLink } from '@angular/router'; + +import { Observable, Subscription, interval } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; + +import { + Notification, + NotificationEvent, + NotificationService, + NotificationType, + ToastNotificationListComponent, +} from '../../../components/patternfly-ng/notification'; +import { ListConfig, ListModule } from '../../../components/patternfly-ng/list'; + +import { EditLabelsDialogComponent } from '../../../components/edit-labels-dialog/edit-labels-dialog.component'; +import { GradeIndexComponent } from '../../../components/grade-index/grade-index.component'; +import { LabelListComponent } from '../../../components/label-list/label-list.component'; +import { TimeAgoPipe } from '../../../components/time-ago.pipe'; + +import { ExchangesTabsetComponent } from './_components/exchanges-tabset/exchanges-tabset.component'; +import { GenerateSamplesDialogComponent } from './_components/generate-samples.dialog'; +import { GenericResourcesDialogComponent } from './_components/generic-resources.dialog'; +import { ManageSamplesDialogComponent } from './_components/manage-samples.dialog'; + +import { + Operation, + ServiceType, + ServiceView, + Contract, + Parameter, + ParameterConstraint, + Exchange, + UnidirectionalEvent, + RequestResponsePair, + EventMessage, +} from '../../../models/service.model'; +import { TestConformanceMetric } from '../../../models/metric.model'; +import { AICopilotService } from '../../../services/aicopilot.service'; +import { IAuthenticationService } from '../../../services/auth.service'; +import { ConfigService } from '../../../services/config.service'; +import { ContractsService } from '../../../services/contracts.service'; +import { MetricsService } from '../../../services/metrics.service'; +import { ServicesService } from '../../../services/services.service'; + +@Component({ + selector: 'app-service-detail-page', + templateUrl: './service-detail.page.html', + styleUrls: ['./service-detail.page.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + BsDropdownModule, + ExchangesTabsetComponent, + GradeIndexComponent, + LabelListComponent, + ListModule, + RouterLink, + TimeAgoPipe, + ToastNotificationListComponent, + TooltipModule, + ], +}) +export class ServiceDetailPageComponent implements OnInit { + readonly hlLang: string[] = ['json', 'xml', 'yaml']; + + modalRef?: BsModalRef; + serviceId!: string; + serviceView: Observable | null = null; + resolvedServiceView!: ServiceView; + contracts?: Observable; + serviceTestConformanceMetric?: Observable; + operations?: Operation[]; + selectedOperation?: Operation; + operationsListConfig!: ListConfig; + notifications: Notification[] = []; + urlType: string = 'raw'; + + aiCopilotSamples: boolean = false; + aiCopilotTaskId: string | null = null; + aiPoller?: Subscription; + + constructor( + private servicesSvc: ServicesService, + private contractsSvc: ContractsService, + private metricsSvc: MetricsService, + private authService: IAuthenticationService, + protected config: ConfigService, + private copilotSvc: AICopilotService, + private modalService: BsModalService, + protected notificationService: NotificationService, + private route: ActivatedRoute, + private router: Router, + private ref: ChangeDetectorRef + ) {} + + ngOnInit() { + this.notifications = this.notificationService.getNotifications(); + this.serviceView = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.servicesSvc.getServiceView(params.get('serviceId')!) + ) + ); + this.contracts = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.contractsSvc.listByServiceId(params.get('serviceId')!) + ) + ); + this.serviceTestConformanceMetric = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.metricsSvc.getServiceTestConformanceMetric(params.get('serviceId')!) + ) + ); + this.serviceView.subscribe((view) => { + this.serviceId = view.service.id; + this.resolvedServiceView = view; + this.operations = view.service.operations; + this.operations.sort((o1, o2) => { + return this.sortOperations(o1, o2); + }); + this.updateAICopilotSamplesFlag(view); + }); + + // Fallback + this.route.paramMap.subscribe((params) => { + // In case the : was used, contracts, tests and + // conformance metrics fail because they can just resolve the technical identifier + // of service. Relaunch everything here. + const idParam = params.get('serviceId')!; + if (idParam.includes(':') && this.serviceView != null) { + this.serviceView.subscribe((view) => { + console.log('Got serviceId for ' + idParam + ': ' + view.service.id); + this.contracts = this.contractsSvc.listByServiceId(view.service.id); + this.serviceTestConformanceMetric = + this.metricsSvc.getServiceTestConformanceMetric(view.service.id); + }); + } + }); + + this.operationsListConfig = { + dblClick: false, + //emptyStateConfig: null, + multiSelect: false, + selectItems: false, + selectionMatchProp: 'name', + showCheckbox: false, + showRadioButton: false, + useExpandItems: true, + } as ListConfig; + } + + private refreshServiceView(): void { + // Because we're using the ChangeDetectionStrategy.OnPush, we have to explicitely + // set a new value (and not only mutate) to serviceView to force async pipe evaluation later on. + // When done multiple times, serviceView re-assignation is not detected... So we have to force + // null, redetect and then re-assign a new Observable... + this.serviceView = null; + this.ref.detectChanges(); + this.serviceView = this.servicesSvc.getServiceView(this.serviceId); + this.serviceView.subscribe((view) => { + this.resolvedServiceView = view; + this.updateAICopilotSamplesFlag(view); + }); + // Then trigger view reevaluation to update the samples list and the notifications toaster. + this.ref.detectChanges(); + } + + private updateAICopilotSamplesFlag(view: ServiceView): void { + this.aiCopilotSamples = false; + this.operations!.forEach((operation) => { + view.messagesMap[operation.name].forEach((exchange) => { + let anyExchange = exchange as any; + if ( + (anyExchange.request != undefined && anyExchange.request.sourceArtifact === 'AI Copilot') + || (anyExchange.eventMessage != undefined && anyExchange.eventMessage.sourceArtifact === 'AI Copilot') + ) { + this.aiCopilotSamples = true; + return; + } + }); + }); + } + + private sortOperations(o1: Operation, o2: Operation): number { + const name1 = this.removeVerbInUrl(o1.name); + const name2 = this.removeVerbInUrl(o2.name); + if (name1 > name2) { + return 1; + } + if (name2 > name1) { + return -1; + } + if (o1.name > o2.name) { + return 1; + } + return -1; + } + + public isEventTypeService(): boolean { + return ( + this.resolvedServiceView.service.type === ServiceType.EVENT || + this.resolvedServiceView.service.type === ServiceType.GENERIC_EVENT + ); + } + + public gotoCreateTest(): void { + this.router.navigate(['/tests/create', { serviceId: this.serviceId }]); + } + + public openEditLabels(): void { + const initialState = { + closeBtnName: 'Cancel', + resourceName: this.resolvedServiceView.service.name + ' - ' + this.resolvedServiceView.service.version, + resourceType: 'Service', + labels: new Map(), + }; + if (this.resolvedServiceView.service.metadata.labels != undefined) { + initialState.labels = JSON.parse( + JSON.stringify(this.resolvedServiceView.service.metadata.labels) + ); + } + this.modalRef = this.modalService.show(EditLabelsDialogComponent, { + initialState, + }); + this.modalRef.content.saveLabelsAction.subscribe((labels: Map) => { + this.resolvedServiceView.service.metadata.labels = labels; + this.servicesSvc + .updateServiceMetadata( + this.resolvedServiceView.service, + this.resolvedServiceView.service.metadata + ) + .subscribe({ + next: (res) => { + // Because we're using the ChangeDetectionStrategy.OnPush, we have to explicitely + // set a new value (and not only mutate) to serviceView to force async pipe evaluation later on. + // When done multiple times, serviceView re-assignation is not detected... So we have to force + // null, redetect and then re-assign a new Observable... + this.serviceView = null; + this.ref.detectChanges(); + this.serviceView = new Observable((observer) => { + observer.next(this.resolvedServiceView); + }); + this.notificationService.message( + NotificationType.SUCCESS, + this.resolvedServiceView.service.name, + 'Labels have been updated', + false + ); + // Then trigger view reevaluation to update the label list component and the notifications toaster. + this.ref.detectChanges(); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + this.resolvedServiceView.service.name, + 'Labels cannot be updated (' + err.message + ')', + false + ); + }, + complete: () => { + //console.log('Observer got a complete notification') + }, + }); + }); + } + + public getContractLink(contract: Contract): string { + if (contract.mainArtifact) { + return "/api/resources/" + contract.name; + } + return "/api/resources/id/" + contract.id; + } + public getContractDocumentationLink(contract: Contract): string { + if (contract.mainArtifact) { + return "/api/documentation/" + contract.name + "/" + contract.type; + } + return "/api/documentation/id/" + contract.id + "/" + contract.type; + } + + public enrichWithAICopilot(): void { + this.copilotSvc.launchSamplesGeneration(this.resolvedServiceView.service).subscribe((res) => { + this.aiCopilotTaskId = res.taskId; + console.log('AI Copilot task id: ' + this.aiCopilotTaskId); + this.notificationService.message( + NotificationType.INFO, + this.resolvedServiceView.service.name, + 'AI Copilot Samples generation started...', + false + ); + // Then trigger view reevaluation to update the spinner, the button and the notifications toaster. + this.ref.detectChanges(); + + // Then start polling for the task status and update the view accordingly. + console.log('Starting polling for AI Copilot task status...'); + this.aiPoller = interval(5000).pipe( + switchMap(() => this.copilotSvc.getGenerationTaskStatus(this.aiCopilotTaskId!)) + ).subscribe((res) => { + console.log("Response: " + JSON.stringify(res)); + if (res.status === 'SUCCESS') { + this.notificationService.message( + NotificationType.SUCCESS, + this.resolvedServiceView.service.name, + 'AI Copilot Samples generation finished!', + false + ); + this.aiCopilotTaskId = null; + this.aiPoller!.unsubscribe(); + } else if (res.status === 'FAILURE') { + this.notificationService.message( + NotificationType.DANGER, + this.resolvedServiceView.service.name, + 'AI Copilot Samples generation failed', + false + ); + this.aiCopilotTaskId = null; + this.aiPoller!.unsubscribe(); + } + // Refresh the view to update the spinner and the notifications toaster. + this.refreshServiceView(); + }); + }); + } + public isAIEnrichInProgress(): boolean { + return this.aiCopilotTaskId != null; + } + + public openResources(): void { + const initialState = { + closeBtnName: 'Close', + service: this.resolvedServiceView.service, + }; + this.modalRef = this.modalService.show(GenericResourcesDialogComponent, { + initialState, + }); + } + + public openGenerateSamples(operationName: string): void { + const initialState = { + closeBtnName: 'Cancel', + service: this.resolvedServiceView.service, + operationName, + }; + this.modalRef = this.modalService.show(GenerateSamplesDialogComponent, { + initialState, + }); + this.modalRef.setClass('modal-lg'); + this.modalRef.content.saveSamplesAction.subscribe((exchanges: Exchange[]) => { + this.copilotSvc + .addSamplesSuggestions( + this.resolvedServiceView.service, + operationName, + exchanges + ) + .subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + this.resolvedServiceView.service.name, + 'Samples have been added to ' + operationName, + false + ); + // Then trigger view reevaluation to update the samples list and the notifications toaster. + this.refreshServiceView(); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + this.resolvedServiceView.service.name, + 'Samples cannot be added (' + err.message + ')', + false + ); + }, + complete: () => { + //console.log('Observer got a complete notification') + }, + }); + }); + } + + public openManageAISamples(): void { + const initialState = { + closeBtnName: 'Cancel', + serviceView: this.resolvedServiceView, + }; + this.modalRef = this.modalService.show(ManageSamplesDialogComponent, { + initialState, + }); + this.modalRef.setClass('modal-lg'); + this.modalRef.content.cleanupSelectionAction.subscribe((selectedExchanges: Record>) => { + let exchangeSelection: { serviceId: string; exchanges: Record } = { + serviceId: this.resolvedServiceView.service.id, + exchanges: {} + }; + Object.keys(selectedExchanges).forEach((operationName) => { + exchangeSelection.exchanges[operationName] = []; + Object.keys(selectedExchanges[operationName]).forEach((exchangeName) => { + exchangeSelection.exchanges[operationName]!.push(exchangeName); + }); + }); + this.copilotSvc + .removeExchanges(this.resolvedServiceView.service, exchangeSelection) + .subscribe({ + next: (res) => { + this.notificationService.message( + NotificationType.SUCCESS, + this.resolvedServiceView.service.name, + 'AI Copilot Samples have been removed from Service', + false + ); + // Then trigger view reevaluation to update the samples list and the notifications toaster. + this.refreshServiceView(); + }, + error: (err) => { + this.notificationService.message( + NotificationType.DANGER, + this.resolvedServiceView.service.name, + 'Selected Samples cannot be removed (' + err.message + ')', + false + ); + }, + complete: () => { + //console.log('Observer got a complete notification') + }, + }); + }); + } + + public getExchangeName(exchange: Exchange): string { + if (this.isEventTypeService()) { + return (exchange as UnidirectionalEvent).eventMessage.name; + } else { + return (exchange as RequestResponsePair).request.name; + } + } + public getExchangeSourceArtifact(exchange: Exchange): string { + if (this.isEventTypeService()) { + return (exchange as UnidirectionalEvent).eventMessage.sourceArtifact; + } else { + return (exchange as RequestResponsePair).request.sourceArtifact; + } + } + + public displayParameterConstraint(constraint: ParameterConstraint): string { + let result = 'Parameter '; + if (constraint.required) { + result += ' is required'; + } + if (constraint.recopy) { + if (result != 'Parameter ') { + result += ', '; + } + result += ' will be recopied as response header'; + } + if (constraint.mustMatchRegexp) { + if (result != 'Parameter ') { + result += ', '; + } + result += + ' must match the ' + + constraint.mustMatchRegexp + + ' regular expression'; + } + return result; + } + + public getBindingsList(operation: Operation): string | null { + // console.log("[ServiceDetailPageComponent.getBindingsList()]"); + if (operation.bindings != null) { + let result = ''; + const bindings = Object.keys(operation.bindings); + for (let i = 0; i < bindings.length; i++) { + const b = bindings[i]; + switch (b) { + case 'KAFKA': + result += 'Kafka'; + break; + case 'NATS': + result += 'NATS'; + break; + case 'MQTT': + result += 'MQTT'; + break; + case 'WS': + result += 'WebSocket'; + break; + case 'AMQP': + result += 'AMQP'; + break; + case 'AMQP1': + result += 'AMQP 1.0'; + break; + case 'GOOGLEPUBSUB': + result += 'Google PubSub'; + break; + case 'SNS': + result += 'Amazon SNS'; + break; + case 'SQS': + result += 'Amazon SQS'; + break; + } + if (i + 1 < bindings.length) { + result += ', '; + } + } + return result; + } + return null; + } + public hasBinding(operation: Operation, binding: string): boolean { + if (operation.bindings != null) { + return operation.bindings.hasOwnProperty(binding); + } + return false; + } + public getBindingProperty( + operation: Operation, + binding: string, + property: string + ): string | null { + // console.log("[ServiceDetailPageComponent.getBindingProperty()]"); + if (operation.bindings != null) { + const b = operation.bindings[binding]; + if (b.hasOwnProperty(property)) { + return (b as any)[property]; + } + } + return null; + } + + public isMCPAvailable(): boolean { + return this.resolvedServiceView.service.type === ServiceType.REST + || this.resolvedServiceView.service.type === ServiceType.GRPC + || this.resolvedServiceView.service.type === ServiceType.GRAPHQL + } + public formatMCPUrl(suffix: string = ''): string { + let result = document.location.origin; + + // Manage dev mode. + if (result.endsWith('localhost:4200')) { + result = 'http://localhost:8080'; + } + + result += '/mcp/'; + result += this.encodeUrl(this.resolvedServiceView.service.name) + '/'; + result += this.resolvedServiceView.service.version; + result += suffix; + + return result; + } + + public formatMockUrl(operation: Operation, dispatchCriteria: string | null, queryParameters: Parameter[] | null): string { + // console.log("[ServiceDetailPageComponent.formatMockUrl()]"); + let result = document.location.origin; + + // Manage dev mode. + if (result.endsWith('localhost:4200')) { + result = 'http://localhost:8080'; + } + + if (this.resolvedServiceView.service.type === ServiceType.REST) { + if (this.urlType === 'raw') { + result += '/rest/'; + } else { + result += '/rest-valid/'; + } + result += + this.encodeUrl(this.resolvedServiceView.service.name) + + '/' + + this.resolvedServiceView.service.version; + + const parts: Record = {}; + const params = {}; + let operationName = operation.name; + + if (dispatchCriteria != null) { + let partsCriteria = + dispatchCriteria.indexOf('?') == -1 + ? dispatchCriteria + : dispatchCriteria.substring(0, dispatchCriteria.indexOf('?')); + const paramsCriteria = + dispatchCriteria.indexOf('?') == -1 + ? null + : dispatchCriteria.substring(dispatchCriteria.indexOf('?') + 1); + + partsCriteria = this.encodeUrl(partsCriteria); + partsCriteria.split('/').forEach((element) => { + if (element) { + parts[element.split('=')[0]] = element.split('=')[1]; + } + }); + + // operationName = operationName.replace(/{(\w+)}/g, function(match, p1, string) { + operationName = operationName.replace( + /{([a-zA-Z0-9-_]+)}/g, + (_, p1) => { + return parts[p1]; + } + ); + // Support also Postman syntax with /:part + operationName = operationName.replace(/:([a-zA-Z0-9-_]+)/g, (_, p1) => { + return parts[p1]; + }); + if (paramsCriteria != null && operation.dispatcher != 'QUERY_HEADER') { + operationName += '?' + paramsCriteria.replace(/\?/g, '&'); + } + } + + // Remove leading VERB in Postman import case. + operationName = this.removeVerbInUrl(operationName); + result += operationName; + + // Result may still contain {} if no dispatchCriteria (because of SCRIPT) + if (result.indexOf('{') != -1 && queryParameters != null) { + console.log('queryParameters: ' + queryParameters); + queryParameters.forEach((param) => { + result = result.replace('{' + param.name + '}', param.value); + }); + } + + } else if (this.resolvedServiceView.service.type === ServiceType.GRAPHQL) { + result += '/graphql/'; + result += + this.encodeUrl(this.resolvedServiceView.service.name) + + '/' + + this.resolvedServiceView.service.version; + } else if ( + this.resolvedServiceView.service.type === ServiceType.SOAP_HTTP + ) { + result += '/soap/'; + result += + this.encodeUrl(this.resolvedServiceView.service.name) + + '/' + + this.resolvedServiceView.service.version; + if (this.urlType === 'valid') { + result += '?validate=true'; + } + } else if ( + this.resolvedServiceView.service.type === ServiceType.GENERIC_REST + ) { + result += '/dynarest/'; + const resourceName = this.removeVerbInUrl(operation.name); + result += + this.encodeUrl(this.resolvedServiceView.service.name) + + '/' + + this.resolvedServiceView.service.version + + resourceName; + } else if (this.resolvedServiceView.service.type === ServiceType.GRPC) { + // Change port in Dev mode of add '-grpc' service name suffix. + if (result === 'http://localhost:8080') { + result = 'http://localhost:9090'; + } else { + result = result.replace(/^([^.-]+)(.*)/, '$1-grpc$2'); + } + } + + return result; + } + + public formatAsyncDestination( + operation: Operation, + eventMessage: EventMessage, + binding: string + ): string { + let serviceName = this.resolvedServiceView.service.name; + let versionName = this.resolvedServiceView.service.version; + let operationName = operation.name; + + if (binding === 'WS') { + // Specific encoding for urls. + serviceName = serviceName.replace(/\s/g, '+'); + versionName = versionName.replace(/\s/g, '+'); + + // Remove verb and templatized part if any. + operationName = this.getDestinationOperationPart(operation, eventMessage); + + return ( + this.asyncAPIFeatureEndpoint('WS') + + '/api/ws/' + + serviceName + + '/' + + versionName + + '/' + + operationName + ); + } + + // Remove ' ', '-' in service name. + serviceName = serviceName.replace(/\s/g, ''); + serviceName = serviceName.replace(/-/g, ''); + + // Remove verb and templatized part if any. + operationName = this.getDestinationOperationPart(operation, eventMessage); + + // Sanitize operation name depending on protocol. + if ( + 'KAFKA' === binding || + 'GOOGLEPUBSUB' === binding || + 'SQS' === binding || + 'SNS' === binding + ) { + operationName = operationName.replace(/\//g, '-'); + } + if ('SQS' === binding || 'SNS' === binding) { + versionName = versionName.replace(/\./g, ''); + } + + return serviceName + '-' + versionName + '-' + operationName; + } + + protected getDestinationOperationPart( + operation: Operation, + eventMessage: EventMessage + ): string { + // In AsyncAPI v2, channel address is directly the operation name. + let operationPart = this.removeVerbInUrl(operation.name); + + // Take care of templatized address for URI_PART dispatcher style. + if (operation.dispatcher === 'URI_PARTS') { + // In AsyncAPI v3, operation is different from channel and channel templatized address may be in resourcePaths. + for (const resourcePath of operation.resourcePaths) { + if (resourcePath.indexOf('{') != -1) { + operationPart = resourcePath; + break; + } + } + + // No replace the part placeholders with their values. + if (eventMessage.dispatchCriteria != null) { + const parts: Record = {}; + const partsCriteria = this.encodeUrl(eventMessage.dispatchCriteria); + partsCriteria.split('/').forEach((element) => { + if (element) { + parts[element.split('=')[0]] = element.split('=')[1]; + } + }); + operationPart = operationPart.replace( + /{([a-zA-Z0-9-_]+)}/g, + (match, p1) => { + return parts[p1] != null ? parts[p1] : match; + } + ); + } + } + return operationPart; + } + + public formatRequestContent(requestContent: string): string { + if (this.resolvedServiceView.service.type === ServiceType.GRAPHQL) { + try { + const request = JSON.parse(requestContent); + return request.query; + } catch (error) { + console.log( + 'Error while parsing GraphQL request content: ' + (error! as any)['message'] + ); + return requestContent; + } + } + return this.prettyPrintIfJSON(requestContent); + } + public formatGraphQLVariables(requestContent: string): string { + try { + const request = JSON.parse(requestContent); + if (request.variables) { + return JSON.stringify(request.variables, null, 2); + } + } catch (error) { + console.log( + 'Error while parsing GraphQL request content: ' + (error! as any)['message'] + ); + } + return ''; + } + public prettyPrintIfJSON(content: string): string { + if ( + (content.startsWith('[') || content.startsWith('{')) && + content.indexOf('\n') == -1 + ) { + try { + const jsonContent = JSON.parse(content); + return JSON.stringify(jsonContent, null, 2); + } catch (error) { + return content; + } + } + return content; + } + + public formatCurlCmd( + operation: Operation, + exchange: RequestResponsePair + ): string { + let mockUrl = this.formatMockUrl( + operation, + exchange.response.dispatchCriteria, + exchange.request.queryParameters + ); + + let cmd; + + if (this.resolvedServiceView.service.type != ServiceType.GRPC) { + let verb = operation.method != undefined ? operation.method.toUpperCase() : 'POST'; + if (this.resolvedServiceView.service.type === ServiceType.GRAPHQL) { + verb = 'POST'; + } + + cmd = 'curl -X ' + verb + ' \'' + mockUrl + '\''; + + // Add request headers if any. + if (exchange.request.headers != null) { + for (const header of exchange.request.headers) { + cmd += ` -H '${header.name}: ${header.values.join(', ')}'`; + } + } + + // Add a content-type header if missing and obvious we need one. + if ( + exchange.request.content != null && + !cmd.toLowerCase().includes('-h \'content-type:') + ) { + if ( + exchange.request.content.startsWith('[') || + exchange.request.content.startsWith('{') + ) { + cmd += ' -H \'Content-Type: application/json\''; + } else if (exchange.request.content.startsWith('<')) { + cmd += ' -H \'Content-Type: application/xml\''; + } + } + } else { + cmd = 'grpcurl -plaintext'; + } + + if (exchange.request.content != null + && exchange.request.content != undefined + && exchange.request.content != '') { + cmd += ' -d \'' + exchange.request.content.replace(/\n/g, '') + '\''; + } + + if (this.resolvedServiceView.service.type === ServiceType.GRPC) { + // Add empty request body. + if (exchange.request.content == null + || exchange.request.content == undefined + || exchange.request.content == '') { + cmd += ' -d \'{}\''; + } + if (mockUrl.indexOf('://') != -1) { + mockUrl = mockUrl.substring(mockUrl.indexOf('://') + 3); + } + cmd += ' ' + mockUrl + ' ' + this.resolvedServiceView.service.name + '/' + operation.name; + } + + return cmd; + } + + public copyToClipboard(url: string, what: string = 'Mock URL'): void { + const selBox = document.createElement('textarea'); + selBox.style.position = 'fixed'; + selBox.style.left = '0'; + selBox.style.top = '0'; + selBox.style.opacity = '0'; + selBox.value = url; + document.body.appendChild(selBox); + selBox.focus(); + selBox.select(); + document.execCommand('copy'); + document.body.removeChild(selBox); + this.notificationService.message( + NotificationType.INFO, + this.resolvedServiceView.service.name, + what + ' has been copied to clipboard', + false + ); + } + + protected removeVerbInUrl(operationName: string): string { + if ( + operationName.startsWith('GET ') || + operationName.startsWith('PUT ') || + operationName.startsWith('POST ') || + operationName.startsWith('DELETE ') || + operationName.startsWith('OPTIONS ') || + operationName.startsWith('PATCH ') || + operationName.startsWith('HEAD ') || + operationName.startsWith('TRACE ') || + operationName.startsWith('SUBSCRIBE ') || + operationName.startsWith('PUBLISH ') || + operationName.startsWith('SEND ') || + operationName.startsWith('RECEIVE ') + ) { + operationName = operationName.slice(operationName.indexOf(' ') + 1); + } + return operationName; + } + protected encodeUrl(url: string): string { + return url.replace(/\s/g, '+'); + } + + handleCloseNotification($event: NotificationEvent): void { + this.notificationService.remove($event.notification); + } + + public hasRole(role: string): boolean { + return this.authService.hasRole(role); + } + + public hasRoleForService(role: string): boolean { + if ( + this.hasRepositoryTenancyFeatureEnabled() && + this.resolvedServiceView.service.metadata.labels + ) { + console.log('hasRepositoryTenancyFeatureEnabled'); + const tenant = + this.resolvedServiceView.service.metadata.labels[ + this.repositoryTenantLabel() + ]; + if (tenant !== undefined) { + return this.authService.hasRoleForResource(role, tenant); + } + } + return this.hasRole(role); + } + + public allowOperationsPropertiesEdit(): boolean { + return ( + (this.hasRoleForService('manager') || this.hasRole('admin')) && + (this.resolvedServiceView.service.type === 'REST' || + this.resolvedServiceView.service.type === 'GRPC' || + this.resolvedServiceView.service.type === 'GRAPHQL' || + ((this.resolvedServiceView.service.type === 'EVENT' || + this.resolvedServiceView.service.type === 'GENERIC_EVENT') && + this.hasAsyncAPIFeatureEnabled())) + ); + } + + public allowAICopilotOnSamples(): boolean { + return ( + this.hasAICopilotEnabled() && + (this.resolvedServiceView.service.type === 'REST' || + this.resolvedServiceView.service.type === 'GRAPHQL' || + this.resolvedServiceView.service.type === 'EVENT' || + this.resolvedServiceView.service.type === 'GRPC') + ); + } + public hasAICopilotSamples(): boolean { + return ( + this.hasAICopilotEnabled() && this.aiCopilotSamples + ); + } + + public hasRepositoryTenancyFeatureEnabled(): boolean { + return this.config.hasFeatureEnabled('repository-tenancy'); + } + + public repositoryTenantLabel(): string { + return this.config + .getFeatureProperty('repository-filter', 'label-key') + .toLowerCase(); + } + + public hasAsyncAPIFeatureEnabled(): boolean { + return this.config.hasFeatureEnabled('async-api'); + } + + public hasAICopilotEnabled(): boolean { + return this.hasRole('manager') && this.config.hasFeatureEnabled('ai-copilot'); + } + + public asyncAPIFeatureEndpoint(binding: string): string { + return this.config.getFeatureProperty('async-api', 'endpoint-' + binding); + } + + public isAsyncMockEnabled(operation: Operation): boolean { + return this.hasAsyncAPIFeatureEnabled() && operation.defaultDelay != 0; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.css new file mode 100644 index 000000000..dbed8329c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.css @@ -0,0 +1,63 @@ +.help-bar { + background-color: #f1f1f1; + border-left: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} +.key-value-editor-col { + padding-left: 0px; + padding-right: 5px; +} +.operation-headers { + padding-top: 5px; + padding-bottom: 5px; +} + +.key-value-editor-entry-header { + padding-right: 50px; +} +.key-value-editor-header { + float: left; + margin-bottom: 5px; + padding-right: 5px; + width: 50%; +} + +.key-value-editor .key-value-editor-entry { + display: table; + margin-bottom: 10px; + padding-right: 50px; + position: relative; + table-layout: fixed; + width: 100%; +} + +.key-value-editor .key-value-editor-input{ + float: left; + margin-bottom: 0; + padding-right: 5px; + width: 50%; +} + +.key-value-editor .key-value-editor-buttons { + position: absolute; + right: 0; + top: 0; + width: 50px; + vertical-align: middle; +} + +.key-value-editor .as-item-delete{ + display: inline-block; + font-size: 14px; + opacity: .65; + padding: 5px; + vertical-align: middle; + padding-top: 5px; + padding-bottom: 5px; + color: #333; +} + +.filter-toolbar { + display: flex; + gap: 5px; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.html new file mode 100644 index 000000000..220b923de --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.html @@ -0,0 +1,438 @@ +
+ + +
+ +
+ + +
+
+

New Test

+ +
+
+
+ +
+ +   +
+ v. + {{ service.version }} +
+

The service and its version we're going to test.

+
+
+
+ +
+ + +

A valid endpoint to use for testing (Http URL for API or WebServices).

+
+ + +

A valid endpoint to use for testing (Kafka/MQTT/NATS/AMQP/PubSub/SQS broker or WebSocket server + topic name for Asynchronous API).

+
+
+
+
+ +
+ +

Select the Asynchronous operation you want to test.

+
+
+
+ +
+ +

+ The runner to use for this test (test only HTTP return code, test also SOAP compliance, use SOAP UI assertions, POSTMAN tests or OPEN API SCHEMA compliance). + The runner to use for this test (for now we only have ASYNC API SCHEMA compliance). + + Learn More + + + +

+
+
+ + + +
+ This section allows to add timeout, security credentials for connecting endpoint and filter operations to test. +
+ +
+ +

Time to wait for the end of the test (value in milliseconds).

+
+
+
+ +
+ +

Pick the authentication secret that will be associated for connecting the test endpoint.

+
+
+
+ +
+ +

Select the OAuth2 grant type that allows retrieving a token for connecting the test endpoint.

+
+
+
+ +
+ +

The OAuth2 token URI that will be used for OAuth2 flow token retrieval.

+
+
+
+
+
+ +
+ +

Your OAuth2 client identifier.

+
+
+
+ +
+ +

Your OAuth2 client secret.

+
+
+
+ +
+ +

Optional OAuth2 scopes (separated using space). openid is always included.

+
+
+
+
+
+ +
+ +

The OAuth2 resource owner username.

+
+
+
+ +
+ +

The OAuth2 resource owner password.

+
+
+
+
+
+ +
+ +

An OAuth2 long-lived refresh token to get a new access token.

+
+
+
+
+
+ +
+
+ + +
+
    +
  • + +
  • +
+

Remove the operations you don't want to include in this test.

+
+
+ +
+ This section allows you to add/override requests headers with global or operation specific ones. Use comma-separated string for multiple values for same header. +
+
Globals headers
+
+
Name
+
Values
+
+
+
+
+ +
+
+ +
+
+ + + +
+
+
+ Add Header +
+
+
Headers for {{ operation.name }}
+
+
Name
+
Values
+
+
+
+
+ +
+
+ +
+
+ + + +
+
+
+ Add Header +
+
+
+
+ + +
+
+
+
+
+ + This sidebar provides some explanations and examples on test endpoints and advanced options. + +
+
+

Accessing Secured Endpoint

+

+ If access to the Test Endpoint requires security materials, you may need to specify and combine those different advanced options: +

+
    +
  • A Secret : allows you to store/reuse static informations for authentication and authorization. A Secret may hold basic authentication informations, token and additional certificates,
  • +
  • An OAuth2 Grant : allows Microcks to run a dynamic OAuth2 flow to retrieve an Access Token that will be presented to the tested endpoint,
  • +
  • Any Additional Operation Headers: allows you to specify any other form of materials that will be transfered as HTTP-header to the test endpoint.
  • +
+
+ +
+
+

Async protocols Test Endpoint

+ +

+ For Event based-API through AsyncAPI testing , the Test Endpoint pattern is depending on the protocole binding you’d like to test. +

+

+ Microcks supports kafka://, mqtt://, + ws://, amqp://, + nats://, googlepubsub://, + sns:// and sqs:// bindings. +

+ +
+
+
Kafka endpoint pattern
+ +

Kafka Test Endpoints have the following form with optional parameters placed just after a ? and separated using & character:

+ +
kafka://kafka.broker.url:port/kafka.topic.name[?param1=value1¶m2=value2]
+ +

Here are the optional parameters available:

+
    +
  • startOffset: The topic offset we start consuming mesages at
  • +
  • endOffset: The topic offset we end consuming mesages at
  • +
  • registryUrl: The URL of schema registry that is associated to the tested topic. This parameter can be required when using and testing Avro encoded messages.
  • +
  • registryUsername: The username used if access to the registry is secured.
  • +
  • registryAuthCredSource: The source for authentication credentials if any. Valid values are just USER_INFO
  • +
+ +

Here's below a sample endpoint with offsets:

+
+ +
+
kafka://mybroker.example.com:443/test-topic?startOffset=28&endOffset=30
+ +

Here's below a sample endpoint with access to a registry:

+
+ +
+
kafka://mybroker.example.com:443/test-topic?registryUrl=https://schema-registry.example.com®istryUsername=fred:letmein®istryAuthCredSource=USER_INFO
+
+
+ +
+
+
MQTT endpoint pattern
+ +

MQTT Test Endpoint have the following form with no optional parameters:

+ +
mqtt://mqtt.broker.url:port/mqtt.topic.name
+
+ +
+
+
WebSocket endpoint pattern
+ +

WebSocket Test Endpoint have the following form with no optional parameters:

+ +
ws://ws.endpoint.url:port/channel.name
+
+ +
+
+
AMQP endpoint pattern
+ +

AMQP 0.9.1 Test Endpoint have the following form with optional parameters placed just after a ? and separated using & character:

+ +
amqp://amqp.broker.url:port/[amqp.vhost/]amqp.destination.type/amqp.destination.name[?param1=value1¶m2=value2]
+ +

Here are the optional parameters available:

+
    +
  • routingKey: Used to specify a routing key for direct or topic exchanges. If not specified the * wildcard is used.
  • +
  • durable: Flag telling if exchange to connect to is durable or not. Default is false.
  • +
  • h.header-name: A bunch of headers where name starts with h. in order to deal with headers exchange. The x-match property is set to any to gather the most message as possible.
  • +
+ +

Here's below a sample endpoint on a virtual host, for topic exchange using the foo routing key:

+
+ +
+
amqp://rabbitmq.example.com:5672/my-vhost/t/my-exchange-topic?routingKey=foo
+ +

Here's below a sample endpoint for headers-typed exchange including 2 headers h1 and h2:

+
+ +
+
amqp://rabbitmq.example.com:5672/h/my-exchange-headers?h.h1=v1&h.h2=v2
+
+ +
+
+
NATS endpoint pattern
+ +

NATS Test Endpoint have the following form with no optional parameters:

+ +
nats://nats.endpoint.url:port/queue-or-subject.name
+
+ +
+
+
Google PubSub endpoint pattern
+ +

Google PubSub Test Endpoint have the following form with no optional parameters:

+ +
googlepubsub://google-platform-project.name/topic.name
+
+ +
+
+
Amazon SQS endpoint pattern
+ +

Amazon Simple Queue Service Test Endpoint have the following form with optional parameters placed just after a ? and separated using & character:

+ +
sqs://aws.region/sqs.queue.name[?param1=value1]
+ +

Here are the optional parameters available:

+
    +
  • overrideUrl: The AWS endpoint override URI used for API calls. Handy for using SQS via LocalStack.
  • +
+
+ +
+
+
Amazon SNS endpoint pattern
+ +

Amazon Simple Notification Service Test Endpoint have the following form with optional parameters placed just after a ? and separated using & character:

+ +
sns://aws.region/sns.topic.name[?param1=value1]
+ +

Here are the optional parameters available:

+
    +
  • overrideUrl: The AWS endpoint override URI used for API calls. Handy for using SNS via LocalStack.
  • +
+
+ +
+
+

Additional Operations Headers

+ +

+ Additional Operations Headers allows you to add or override requests headers with your own values. + They can be used to transfer additional security materials, traceability elements or any other needs. +

+

+ Those headers and their values are applied either at a global level (for each and every requests of every operation) + or only at the specified operation level. +

+

+ Note: The additional headers are applied just before sending a request so they may also overwrite security related headers + that may have been set by a Secret or an OAuth2 grant that are applied earlier in the process. Take care! +

+
+ +
+
+
diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.ts new file mode 100644 index 000000000..51958a2c9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/create/test-create.page.ts @@ -0,0 +1,278 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router, ParamMap, RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { + Notification, + NotificationService, + NotificationType, + ToastNotificationListComponent +} from '../../../components/patternfly-ng/notification'; + +import { ContractsService } from "../../../services/contracts.service"; +import { ServicesService } from '../../../services/services.service'; +import { TestsService } from '../../../services/tests.service'; +import { SecretsService } from '../../../services/secrets.service'; +import { Contract, Operation, Service } from '../../../models/service.model'; +import { TestRunnerType, OAuth2ClientContext } from '../../../models/test.model'; +import { Secret } from '../../../models/secret.model'; + +@Component({ + selector: 'app-test-create-page', + templateUrl: 'test-create.page.html', + styleUrls: ['test-create.page.css'], + imports: [ + CommonModule, + FormsModule, + RouterLink, + ToastNotificationListComponent + ] +}) +export class TestCreatePageComponent implements OnInit { + + service!: Observable; + resolvedService!: Service; + serviceId!: string; + testEndpoint!: string; + runnerType!: TestRunnerType; + contractTypes!: string[] + showAdvanced = false; + submitEnabled = false; + notifications: Notification[] = []; + timeout = 10000; + secretId: string | null = null; + secretName: string | null = null; + operationsHeaders: any = { + globals: [] + }; + secrets!: Secret[]; + oAuth2ClientContext: OAuth2ClientContext | undefined = {} as OAuth2ClientContext; + + filteredOperation?: string; + removedOperationsNames: string[] = []; + + constructor(private servicesSvc: ServicesService, private contractsSvc: ContractsService, public testsSvc: TestsService, private secretsSvc: SecretsService, + private notificationService: NotificationService, private route: ActivatedRoute, private router: Router) { + } + + ngOnInit() { + this.notifications = this.notificationService.getNotifications(); + let fromTestId: string | null = null; + this.service = this.route.paramMap.pipe( + switchMap((params: ParamMap) => { + // (+) before `params.get()` turns the string into a number + this.serviceId = params.get('serviceId')!; + if (params.has('fromTest')) { + fromTestId = params.get('fromTest'); + } + return this.servicesSvc.getService(this.serviceId); + }) + ); + this.service.subscribe( service => { + this.resolvedService = service; + if (fromTestId != null) { + this.initializeFromPreviousTestResult(fromTestId); + } + }); + this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.contractsSvc.listByServiceId(params.get('serviceId')!) + ) + ).subscribe((contracts: Contract[]) => { + this.contractTypes = contracts.map((contract : Contract) => contract.type.toString()); + }); + + } + + getSecrets(page: number = 1): void { + this.secretsSvc.getSecrets(page).subscribe(results => this.secrets = results); + } + + initializeFromPreviousTestResult(testId: string): void { + this.testsSvc.getTestResult(testId).subscribe( + { + next: res => { + this.notificationService.message(NotificationType.SUCCESS, + 'New Test', 'Test has been initialized from ' + testId, false); + this.testEndpoint = res.testedEndpoint; + this.runnerType = res.runnerType; + // Complete with optional properties. + if (res.operationsHeaders) { + this.operationsHeaders = res.operationsHeaders; + } + if (res.timeout) { + this.timeout = res.timeout; + } + if (res.secretRef) { + this.secretId = res.secretRef.secretId; + this.secretName = res.secretRef.name; + } + if (res.authorizedClient) { + this.oAuth2ClientContext = {} as OAuth2ClientContext; + this.oAuth2ClientContext.grantType = res.authorizedClient.grantType; + this.oAuth2ClientContext.tokenUri = res.authorizedClient.tokenUri; + if (res.authorizedClient.scopes && res.authorizedClient.scopes.length > 0) { + this.oAuth2ClientContext.scopes = res.authorizedClient.scopes.replace('openid', ' ').trim(); + } + } + // Finalize with filtered operations. + if (this.resolvedService.type === 'EVENT' && res.testCaseResults.length == 1) { + this.filteredOperation = res.testCaseResults[0].operationName; + } else { + for (const operation of this.resolvedService.operations) { + const foundOperation = res.testCaseResults.find(tc => tc.operationName === operation.name); + if (foundOperation == undefined || foundOperation == null) { + this.removedOperationsNames.push(operation.name); + } + } + } + this.checkForm(); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + 'New Test', 'Test cannot be initialized from ' + testId, false); + }, + complete: () => {} //console.log('Observer got a complete notification'), + } + ); + } + + public showAdvancedPanel(show: boolean) { + if (show && (this.secrets == undefined || this.secrets.length == 0)) { + this.getSecrets(); + } + this.showAdvanced = show; + } + public updateSecretProperties(event: any): void { + const secretId = event.target.value; + if ('undefined' != event.target.value) { + for (const secret of this.secrets) { + if (secretId === secret.id) { + this.secretName = secret.name; + break; + } + } + } else { + this.secretName = null; + } + } + public updateGrantType(event: any): void { + if ('undefined' === event.target.value) { + if (this.oAuth2ClientContext != undefined) { + this.oAuth2ClientContext.grantType = undefined; + } + this.checkForm(); + } + } + public filterOperation(operationName: string): void { + if (this.removedOperationsNames.includes(operationName)) { + this.removedOperationsNames.splice(this.removedOperationsNames.indexOf(operationName), 1); + } else { + this.removedOperationsNames.push(operationName); + } + } + + public resetOperations( + operations: Operation[] = this.resolvedService && + this.resolvedService.operations + ): void { + this.removedOperationsNames = operations.map((op) => op.name); + } + + public addHeaderValue(operationName: string) { + const operationHeaders = this.operationsHeaders[operationName]; + if (operationHeaders == null) { + this.operationsHeaders[operationName] = [ + { name: '', values: '' } + ]; + } else { + this.operationsHeaders[operationName].push({ name: '', values: '' }); + } + } + + public removeHeaderValue(operationName: string, headerIndex: number) { + const operationHeaders = this.operationsHeaders[operationName]; + if (operationHeaders != null) { + operationHeaders.splice(headerIndex, 1); + } + } + + public checkForm(): void { + this.submitEnabled = (this.testEndpoint !== undefined && this.testEndpoint.length > 0 && this.runnerType !== undefined) + && (this.resolvedService.type != 'EVENT' || (this.filteredOperation !== undefined)); + // Check also the OAuth2 parameters. + if (this.submitEnabled && this.oAuth2ClientContext != undefined && this.oAuth2ClientContext.grantType !== undefined) { + this.submitEnabled = (this.oAuth2ClientContext.tokenUri !== undefined && this.oAuth2ClientContext.tokenUri.length > 0 + && this.oAuth2ClientContext.clientId !== undefined && this.oAuth2ClientContext.clientId.length > 0 + && this.oAuth2ClientContext.clientSecret !== undefined && this.oAuth2ClientContext.clientSecret.length > 0); + } + //console.log('[createTest] submitEnabled: ' + this.submitEnabled); + } + + public cancel(): void { + this.router.navigate(['/services', this.serviceId]); + } + + public createTest(): void { + // Build filtered operations array first. + const filteredOperations = []; + if (this.filteredOperation !== undefined) { + filteredOperations.push(this.filteredOperation); + } else { + if (this.removedOperationsNames.length > 0) { + this.resolvedService.operations.forEach(op => { + if (!this.removedOperationsNames.includes(op.name)) { + filteredOperations.push(op.name); + } + }); + } + } + // Reset OAuth2 parameters if not set. + if (this.oAuth2ClientContext?.grantType === undefined) { + this.oAuth2ClientContext = undefined; + } + // Then, create thee test invoking the API. + const test = {serviceId: this.serviceId, testEndpoint: this.testEndpoint, runnerType: this.runnerType, + timeout: this.timeout, secretName: this.secretName, + filteredOperations, operationsHeaders: this.operationsHeaders, + oAuth2Context: this.oAuth2ClientContext}; + //console.log('[createTest] test: ' + JSON.stringify(test)); + this.testsSvc.create(test).subscribe( + { + next: res => { + this.notificationService.message(NotificationType.SUCCESS, + String(res.id), 'Test #' + res.id + ' has been launched', false); + this.router.navigate(['/tests/runner', res.id]); + }, + error: err => { + this.notificationService.message(NotificationType.DANGER, + 'New test', 'New test cannot be launched (' + err.message + ')', false); + }, + complete: () => {} //console.log('Observer got a complete notification') + } + ); + } + + public isContractAvailable(contractTye: string) : boolean{ + return this.contractTypes.includes(contractTye) + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.css new file mode 100644 index 000000000..3d4efb678 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.css @@ -0,0 +1,19 @@ +span.label { + margin-left: 4px !important; + margin-right: 4px !important; + padding-top: 0.6em; + padding-bottom: 0.6em; +} + +.fa-times-circle { + content: "\f057"; + color: #c00; +} +.fa-check-circle { + content: "\f058"; + color: #3f9c35; +} +.fa-refresh { + content: "\f021"; + color: #0088ce; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.html new file mode 100644 index 000000000..cb9287bb5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.html @@ -0,0 +1,108 @@ +
+ + +
+ +
+
+ + +

Test {{ test.id }}

+ Created {{ test.testDate.toString() | timeAgo }} + + + +
+
+
+
Test Number:
+
#{{ test.testNumber }}
+
Tested API or Service:
+
{{ service.name }} - {{ service.version }}
+
Tested Endpoint:
+
{{ test.testedEndpoint }}
+
Test Results:
+
Full results
+
+
+
+ + + + {{ test.runnerType }} +
+
+ +
+ +
+ +
+ + + +
+
+
+
+
{{ item.operationName }}
+
+
+ + {{ item.testStepResults.length }} test(s) +
+
+
+
+
+ + + + in {{ item.elapsedTime }} ms +
+
+
+
+ + +
+
+
+
+
+
+ {{ testStep.requestName || testStep.eventMessageName }} +
+
+
+
+
+
+ + + + in {{ testStep.elapsedTime }} ms +
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.ts new file mode 100644 index 000000000..04b7e7032 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/runner/test-runner.page.ts @@ -0,0 +1,101 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, ParamMap, Router, RouterLink } from '@angular/router'; + +import { Observable, Subscription, interval } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { + Notification, + NotificationService, + ToastNotificationListComponent, +} from '../../../components/patternfly-ng/notification'; +import { ListConfig, ListModule } from '../../../components/patternfly-ng/list'; + +import { TimeAgoPipe } from '../../../components/time-ago.pipe'; + +import { Service } from '../../../models/service.model'; +import { TestResult } from '../../../models/test.model'; +import { ServicesService } from '../../../services/services.service'; +import { TestsService } from '../../../services/tests.service'; + +@Component({ + selector: 'app-test-runner-page', + templateUrl: 'test-runner.page.html', + styleUrls: ['test-runner.page.css'], + imports: [ + CommonModule, + ListModule, + RouterLink, + TimeAgoPipe, + ToastNotificationListComponent + ] +}) +export class TestRunnerPageComponent implements OnInit, OnDestroy { + + testId!: string; + test!: Observable; + service!: Observable; + notifications: Notification[] = []; + poller!: Subscription; + resultsListConfig!: ListConfig; + + constructor(private servicesSvc: ServicesService, public testsSvc: TestsService, private notificationService: NotificationService, + private route: ActivatedRoute, private router: Router) { + } + + ngOnInit() { + this.notifications = this.notificationService.getNotifications(); + this.test = this.route.paramMap.pipe( + switchMap((params: ParamMap) => { + // (+) before `params.get()` turns the string into a number + this.testId = params.get('testId')!; + return this.testsSvc.getTestResult(this.testId); + }) + ); + this.test.subscribe(res => { + this.service = this.servicesSvc.getService(res.serviceId); + }); + + this.poller = interval(2000).pipe( + switchMap(() => this.test = this.testsSvc.getTestResult(this.testId)) + ).subscribe(res => { + if (!res.inProgress) { + this.poller.unsubscribe(); + } + }); + + this.resultsListConfig = { + dblClick: false, + //emptyStateConfig: null, + multiSelect: false, + selectItems: false, + selectionMatchProp: 'name', + showCheckbox: false, + showRadioButton: false, + useExpandItems: true, + hideClose: true + } as ListConfig; + } + + ngOnDestroy(): void { + if (this.poller) { + this.poller.unsubscribe(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.css new file mode 100644 index 000000000..83e92a35a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.css @@ -0,0 +1,46 @@ +.test-bar-chart { + margin-top: 20px; + padding-bottom: 20px; +} + +.list-group-item-text { + line-height: 1.8; +} +.list-view-pf-description { + flex-basis: 75%; +} +.list-view-pf-additional-info { + flex-basis: 25%; +} +.list-view-pf-additional-info-item .fa-2x { + font-size: 2em !important; +} + +span.label { + margin-left: 4px !important; + margin-right: 4px !important; + padding-top: 0.6em; + padding-bottom: 0.6em; +} + +.fa-times-circle { + content: "\f057"; + color: #c00; +} +.fa-check-circle { + content: "\f058"; + color: #3f9c35; +} +.fa-refresh { + content: "\f021"; + color: #0088ce; +} +.fa-clock-o { + color: #ec7a08; +} +.test-timestamp { + padding-top: 2px; + font-size: 91%; + font-weight: 400; + color: #757575; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.html new file mode 100644 index 000000000..ef900da78 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.html @@ -0,0 +1,71 @@ + + +

+ Test History for {{ service.name }} - {{ service.version }} +

+These are the conformance tests that have been run against this service implementations on different + endpoints. + +
+ +
+ +
+ +
+
+
+
+
+
+ + + {{ test.testDate.toString() | timeAgo }} +
+
+ Endpoint {{ test.testedEndpoint }}
+ {{ test.testCaseResults.length }} Operations tested | + {{ numberOfTestSteps(test) }} Samples tested +
+
+
+
+ + + + + {{ displayTestType(test.runnerType.toString()) }} +
+
+
+
+ +
+ +
+
+ + + +
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.ts new file mode 100644 index 000000000..226b87b3e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/tests.page.ts @@ -0,0 +1,119 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router, ParamMap, RouterLink } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { PaginationConfig, PaginationEvent, PaginationModule } from '../../components/patternfly-ng/pagination'; + +import { TestBarChartComponent } from '../../components/test-bar-chart/test-bar-chart.component'; +import { TimeAgoPipe } from '../../components/time-ago.pipe'; + +import { ServicesService } from '../../services/services.service'; +import { TestsService } from '../../services/tests.service'; +import { Service } from '../../models/service.model'; +import { TestResult } from '../../models/test.model'; + +@Component({ + selector: 'app-tests-page', + templateUrl: './tests.page.html', + styleUrls: ['./tests.page.css'], + imports: [ + CommonModule, + PaginationModule, + RouterLink, + TestBarChartComponent, + TimeAgoPipe + ] +}) +export class TestsPageComponent implements OnInit { + + now!: number; + service!: Service; + testResults!: Observable; + testResultsCount: number = 0; + + resolvedTestResults!: TestResult[]; + paginationConfig: PaginationConfig = new PaginationConfig; + + constructor(private servicesSvc: ServicesService, public testsSvc: TestsService, private route: ActivatedRoute) { + } + + ngOnInit() { + this.now = Date.now(); + const serviceViewObs = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.servicesSvc.getService(params.get('serviceId')!)) + ); + serviceViewObs.subscribe(result => { + this.service = result; + }); + + this.testResults = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.testsSvc.listByServiceId(params.get('serviceId')!)) + ); + const testResultsCountObs = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.testsSvc.countByServiceId(params.get('serviceId')!)) + ); + testResultsCountObs.subscribe(result => { + this.testResultsCount = result.counter; + this.paginationConfig.totalItems = this.testResultsCount; + }); + + this.testResults.subscribe( results => { + this.resolvedTestResults = results; + }); + + this.paginationConfig = { + pageNumber: 1, + pageSize: 20, + pageSizeIncrements: [], + totalItems: 20 + } as PaginationConfig; + } + + listByServiceId(page: number = 1): void { + this.testResults = this.testsSvc.listByServiceId(this.service.id, page); + this.testResults.subscribe( results => { + this.resolvedTestResults = results; + }); + } + + handlePageSize($event: PaginationEvent) { + // this.updateItems(); + } + + handlePageNumber($event: PaginationEvent) { + this.listByServiceId($event.pageNumber); + } + + numberOfTestSteps(testResult: TestResult): number { + return testResult.testCaseResults.map( tc => tc.testStepResults.length ).reduce( (acc, cur) => acc + cur); + } + + public timedOut(test: TestResult): boolean { + return (test.inProgress && this.now > (test.testDate + test.timeout)); + } + + public displayTestType(type: string): string { + return type.replace(/_/g, ' '); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.css new file mode 100644 index 000000000..26a653dae --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.css @@ -0,0 +1,21 @@ +.instructions { + padding-top: 0.8em; + padding-bottom: 0.8em; +} + +.instructions ol { + padding-inline-start: 20px; +} + +.instructions li { + padding-top: 0.4em; + padding-bottom: 0.4em; +} + +.instructions pre { + margin-top: 5px; +} + +.instructions .btn { + margin-top: 5px; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.html new file mode 100644 index 000000000..663ed13c2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.html @@ -0,0 +1,137 @@ + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.ts new file mode 100644 index 000000000..ed5631c03 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/_components/add-to-ci.dialog.ts @@ -0,0 +1,257 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { Highlight } from 'ngx-highlightjs'; + +import { + NotificationService, + NotificationType +} from '../../../../components/patternfly-ng/notification'; + +import { TestResult } from '../../../../models/test.model'; +import { Service } from '../../../../models/service.model'; + + +@Component({ + selector: 'app-add-to-ci-dialog', + templateUrl: './add-to-ci.dialog.html', + styleUrls: ['./add-to-ci.dialog.css'], + imports: [ + CommonModule, + FormsModule, + Highlight + ] +}) +export class AddToCIDialogComponent implements OnInit { + + closeBtnName!: string; + test!: TestResult; + service!: Service; + + ciType?: string; + + constructor( + public bsModalRef: BsModalRef, + private notificationService: NotificationService + ) {} + + ngOnInit() {} + + private getMicrocksURL(): string { + let microcksURL = document.location.origin; + // Manage dev mode. + if (microcksURL.endsWith('localhost:4200')) { + microcksURL = 'http://localhost:8080'; + } + return microcksURL; + } + + public getGitHubActionCode(): string { + let yaml = '- uses: microcks/test-github-action@v1\n'; + yaml += ' with:\n'; + yaml += ` apiNameAndVersion: '${this.service.name}:${this.service.version}'\n`; + yaml += ` testEndpoint: '${this.test.testedEndpoint}'\n`; + yaml += ` runner: '${this.test.runnerType}'\n`; + yaml += ` microcksUrl: ${this.getMicrocksURL()}/api\n`; + yaml += ' keycloakClientId: ${{ secrets.MICROCKS_SERVICE_ACCOUNT }}\n'; + yaml += + ' keycloakClientSecret: ${{ secrets.MICROCKS_SERVICE_ACCOUNT_SECRET }}\n'; + + // Adding optional items. + if (this.test.secretRef) { + yaml += ` secretName: '${this.test.secretRef.name}'\n`; + } + if (this.test.operationsHeaders) { + yaml += ` operationsHeaders: '${JSON.stringify( + this.test.operationsHeaders + )}'\n`; + } + // Check for filtered operations. + if (this.test.testCaseResults.length != this.service.operations.length) { + const operationNames = this.test.testCaseResults.map( + (testCaseResult) => testCaseResult.operationName + ); + yaml += ` filteredOperations: '${JSON.stringify(operationNames)}'\n`; + } + + yaml += ` waitFor: '${this.test.timeout / 1000}sec'\n`; + return yaml; + } + + public getGitLabCICode(): string { + let yaml = 'test-api\n'; + yaml += ' stage: integration-test\n'; + yaml += ' image: quay.io/microcks/microcks-cli:latest\n'; + yaml += ' variables:\n'; + yaml += ` apiNameAndVersion: "${this.service.name}:${this.service.version}"\n`; + yaml += ` testEndpoint: ${this.test.testedEndpoint}\n`; + yaml += ` runner: ${this.test.runnerType}\n`; + yaml += ` microcksURL: ${this.getMicrocksURL()}/api\n`; + + // Adding optional items. + if (this.test.secretRef) { + yaml += ` secretName: '${this.test.secretRef.name}'\n`; + } + if (this.test.operationsHeaders) { + yaml += ` operationsHeaders: '${JSON.stringify( + this.test.operationsHeaders + )}'\n`; + } + // Check for filtered operations. + if (this.test.testCaseResults.length != this.service.operations.length) { + const operationNames = this.test.testCaseResults.map( + (testCaseResult) => testCaseResult.operationName + ); + yaml += ` filteredOperations: '${JSON.stringify(operationNames)}'\n`; + } + + yaml += ` waitFor: ${this.test.timeout / 1000}sec\n`; + yaml += ' script:\n'; + yaml += ' - >-\n'; + yaml += + ' microcks-cli test "$apiNameAndVersion" $testEndpoint $runner\n'; + yaml += ' --microcksURL=$microcksURL\n'; + yaml += + ' --keycloakClientId=$MICROCKS_CLIENT_ID --keycloakClientSecret=$MICROCKS_CLIENT_SECRET\n'; + + // Adding optional items. + if (this.test.secretRef) { + yaml += ' --secretName=$secretName\n'; + } + if (this.test.operationsHeaders) { + yaml += ` --operationsHeaders='$operationsHeaders'\n`; + } + // Check for filtered operations. + if (this.test.testCaseResults.length != this.service.operations.length) { + yaml += ` --filteredOperations='$filteredOperations'\n`; + } + yaml += ' --waitFor=$waitFor\n'; + + return yaml; + } + + public getJenkinsGroovyCode(): string { + let groovy = 'microcksTest(server: \'microcks-production\',\n'; + groovy += ` serviceId: '${this.service.name}:${this.service.version}',\n`; + groovy += ` testEndpoint: '${this.test.testedEndpoint}',\n`; + groovy += ` runner: '${this.test.runnerType}',\n`; + + // Adding optional items. + if (this.test.secretRef) { + groovy += ` secretName: '${this.test.secretRef.name}',\n`; + } + if (this.test.operationsHeaders) { + groovy += ` operationsHeaders: '${JSON.stringify( + this.test.operationsHeaders + )}',\n`; + } + + groovy += ` waitTime: '${this.test.timeout / 1000}', waitUnit: 'sec')\n`; + + return groovy; + } + + public getTektonCode(): string { + let yaml = '- name: test-api\n'; + yaml += ' taskRef:\n'; + yaml += ' name: microcks-test\n'; + yaml += ' params:\n'; + yaml += ' - name: apiNameAndVersion\n'; + yaml += ` value: "${this.service.name}:${this.service.version}"\n`; + yaml += ' - name: testEndpoint\n'; + yaml += ` value: ${this.test.testedEndpoint}\n`; + yaml += ' - name: runner\n'; + yaml += ` value: ${this.test.runnerType}\n`; + yaml += ' - name: microcksURL\n'; + yaml += ` value: ${this.getMicrocksURL()}/api\n`; + + // Adding optional items. + if (this.test.secretRef) { + yaml += ' - name: secretName\n'; + yaml += ` value: ${this.test.secretRef.name}\n`; + } + if (this.test.operationsHeaders) { + yaml += ' - name: operationsHeaders\n'; + yaml += ` value: '${JSON.stringify(this.test.operationsHeaders)}'\n`; + } + // Check for filtered operations. + if (this.test.testCaseResults.length != this.service.operations.length) { + const operationNames = this.test.testCaseResults.map( + (testCaseResult) => testCaseResult.operationName + ); + yaml += ' - name: filteredOperations\n'; + yaml += ` value: '${JSON.stringify(operationNames)}'\n`; + } + + yaml += ' - name: waitFor\n'; + yaml += ` value: ${this.test.timeout / 1000}sec\n`; + + return yaml; + } + + public getCLICode(): string { + let cmd = `./microcks-cli test '${this.service.name}:${this.service.version}' ${this.test.testedEndpoint} ${this.test.runnerType} \\ \n`; + cmd += ` --microcksURL=${this.getMicrocksURL()}/api \\ \n`; + cmd += ` --keycloakClientId=microcks-serviceaccount \\ \n`; + cmd += ` --keycloakClientSecret=7deb71e8-8c80-4376-95ad-00a399ee3ca1 \\ \n`; + + // Adding optional items. + if (this.test.secretRef) { + cmd += ` --secretName: '${this.test.secretRef.name}' \\ \n`; + } + if (this.test.operationsHeaders) { + cmd += ` --operationsHeaders: '${JSON.stringify( + this.test.operationsHeaders + )}' \\ \n`; + } + // Check for filtered operations. + if (this.test.testCaseResults.length != this.service.operations.length) { + const operationNames = this.test.testCaseResults.map( + (testCaseResult) => testCaseResult.operationName + ); + cmd += ` --filteredOperations: '${JSON.stringify( + operationNames + )}' \\ \n`; + } + + cmd += ` --waitFor=${this.test.timeout / 1000}sec`; + return cmd; + } + + public copyToClipboard(url: string): void { + const selBox = document.createElement('textarea'); + selBox.style.position = 'fixed'; + selBox.style.left = '0'; + selBox.style.top = '0'; + selBox.style.opacity = '0'; + selBox.value = url; + document.body.appendChild(selBox); + selBox.focus(); + selBox.select(); + document.execCommand('copy'); + document.body.removeChild(selBox); + this.notificationService.message( + NotificationType.INFO, + this.ciType!.toUpperCase(), + 'Code has been copied to clipboard', + false + ); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.css new file mode 100644 index 000000000..e6c0ffbdd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.css @@ -0,0 +1,38 @@ +span.label { + margin-left: 4px !important; + margin-right: 4px !important; + padding-top: 0.6em; + padding-bottom: 0.6em; +} + +.fa-times-circle { + content: "\f057"; + color: #c00; + vertical-align: middle; +} +.fa-check-circle { + content: "\f058"; + color: #3f9c35; + vertical-align: middle; +} +.fa-refresh { + content: "\f021"; + color: #0088ce; + vertical-align: middle; +} +.fa-clock-o { + color: #ec7a08; + vertical-align: middle; +} + +.test-timestamp { + padding-top: 2px; + font-size: 91%; + font-weight: 400; + color: #757575; +} + +.code-block { + display: inline-block; + white-space: pre-wrap; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.html new file mode 100644 index 000000000..b6aff93d1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.html @@ -0,0 +1,199 @@ +
+
+ + +

Test {{ test.id }} +
+ +
+

+ Created {{ test.testDate.toString() | timeAgo }}, ran on {{ test.testDate | date:'medium' }} + + + +
+
+
+
Test Number:
+
#{{ test.testNumber }}
+
Tested API or Service:
+
{{ service.name }} - {{ service.version }}
+
Tested Endpoint:
+
{{ test.testedEndpoint }}
+ +
Secret:
+
{{ test.secretRef.name }}
+
+ +
OAuth2 client:
+
+ {{ test.authorizedClient.principalName }} principal
+ {{ test.authorizedClient.tokenUri }} tokenUri
+ {{ test.authorizedClient.grantType }} grant
+ {{ test.authorizedClient.scopes }} scopes +
+
+
+
+
+ + + + + {{ displayTestType(test.runnerType) }} +
+
+ +
+ +
+ +
+ + + +
+
+
+
+
{{ item.operationName }}
+
+
+ + {{ item.testStepResults.length }} test(s) +
+
+
+
+
+ + + + + in {{ item.elapsedTime }} ms +
+
+
+
+ + + + + + + + + +
+
+
Request
+ +
+ + + + + + + + + + + + + +
Header nameValues
{{ header.name }}{{ v }}{{ last ? '':', '}}
+
+
+
Response
+ +
+
Response Time:
+
{{ testStep.elapsedTime }} ms.
+ +
Response Code and Type:
+
+ {{ messagePairFor(item.operationName, testStep.requestName).response.status }}: {{messagePairFor(item.operationName, testStep.requestName).response.mediaType}} +
+
+ +
Error:
+
+ {{ formatErrorMessage(testStep.message) }} +
+
+
+
+ + + + + + + + + + + + + +
Header nameValues
{{ header.name }}{{ v }}{{ last ? '':', '}}
+
+
+
Event Message
+ +
+
Consumption Time:
+
{{ testStep.elapsedTime }} ms.
+ +
Event Type:
+
+ {{ eventMessageFor(item.operationName, testStep.eventMessageName).eventMessage.mediaType }} +
+
+ +
Error:
+
+ {{ formatErrorMessage(testStep.message) }} +
+
+
+
+ + + + + + + + + + + + + +
Header nameValues
{{ header.name }}{{ v }}{{ last ? '':', '}}
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.ts new file mode 100644 index 000000000..9a449ffe8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/pages/tests/{testId}/test-detail.page.ts @@ -0,0 +1,163 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, ParamMap, RouterLink } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { TabsModule } from 'ngx-bootstrap/tabs'; +import { HighlightAuto } from 'ngx-highlightjs'; + +import { ListConfig, ListModule } from '../../../components/patternfly-ng/list'; + +import { ServicesService } from '../../../services/services.service'; +import { TestsService } from '../../../services/tests.service'; +import { Exchange, RequestResponsePair, Service, ServiceType, UnidirectionalEvent } from '../../../models/service.model'; +import { TestResult } from '../../../models/test.model'; +import { AddToCIDialogComponent } from './_components/add-to-ci.dialog'; +import { TimeAgoPipe } from '../../../components/time-ago.pipe'; + +@Component({ + selector: 'app-test-detail-page', + templateUrl: './test-detail.page.html', + styleUrls: ['./test-detail.page.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + HighlightAuto, + ListModule, + RouterLink, + TabsModule, + TimeAgoPipe + ] +}) +export class TestDetailPageComponent implements OnInit { + readonly hlLang: string[] = ['json', 'xml', 'yaml']; + + now!: number; + test!: Observable; + service!: Observable; + testMessages: Record = {}; + resultsListConfig!: ListConfig; + + resolvedTest!: TestResult; + resolvedService!: Service; + modalRef?: BsModalRef; + + constructor(private servicesSvc: ServicesService, private testsSvc: TestsService, + private modalService: BsModalService, private route: ActivatedRoute) { + } + + ngOnInit() { + this.now = Date.now(); + this.test = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.testsSvc.getTestResult(params.get('testId')!)) + ); + this.test.subscribe(res => { + this.resolvedTest = res; + this.service = this.servicesSvc.getService(res.serviceId); + this.service.subscribe(svc => { + this.resolvedService = svc; + if (svc.type != ServiceType.EVENT) { + res.testCaseResults.forEach(testCase => { + const opName = this.encode(testCase.operationName); + this.testsSvc.getMessages(res, testCase.operationName).subscribe(pairs => { + this.testMessages[opName] = pairs; + }); + }); + } else { + res.testCaseResults.forEach(testCase => { + const opName = this.encode(testCase.operationName); + this.testsSvc.getEventMessages(res, testCase.operationName).subscribe(pairs => { + this.testMessages[opName] = pairs; + }); + }); + } + }); + }); + + this.resultsListConfig = { + dblClick: false, + //emptyStateConfig: null, + multiSelect: false, + selectItems: false, + selectionMatchProp: 'name', + showCheckbox: false, + showRadioButton: false, + useExpandItems: true, + hideClose: true + } as ListConfig; + } + + public openAddToCI(): void { + const initialState = { + closeBtnName: 'Close', + test: this.resolvedTest, + service: this.resolvedService + }; + this.modalRef = this.modalService.show(AddToCIDialogComponent, {initialState}); + } + + public messagePairFor(operationName: string, requestName: string): any { + const pairs = this.testMessages[this.encode(operationName)]; + if (pairs == undefined) { + return undefined; + } + const result = pairs.filter(function(item, index, array) { + return (item as RequestResponsePair).request.name == requestName; + })[0]; + return result; + } + public eventMessageFor(operationName: string, eventMessageName: string): any { + const events = this.testMessages[this.encode(operationName)]; + if (events == undefined) { + return undefined; + } + const result = events.filter(function(item, index, array) { + return (item as UnidirectionalEvent).eventMessage.name == eventMessageName; + })[0]; + return result; + } + + public encode(operation: string): string { + operation = operation.replace(/\//g, ''); + operation = operation.replace(/\s/g, ''); + operation = operation.replace(/:/g, ''); + operation = operation.replace(/{/g, ''); + operation = operation.replace(/}/g, ''); + return encodeURIComponent(operation); + } + + public formatErrorMessage(message: string): string { + if (message != undefined) { + const result = message.replace(/\\n/g, '\n'); + return result.replace(//g, '\n'); + } + return ''; + } + + public timedOut(test: TestResult): boolean { + return (test.inProgress && this.now > (test.testDate + test.timeout)); + } + + public displayTestType(type: string): string { + return type.replace(/_/g, ' '); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/aicopilot.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/aicopilot.service.ts new file mode 100644 index 000000000..d5f8024d1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/aicopilot.service.ts @@ -0,0 +1,50 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { Service, Exchange } from '../models/service.model'; + +@Injectable({ providedIn: 'root' }) +export class AICopilotService { + + private rootUrl = '/api'; + + constructor(private http: HttpClient) { } + + public getSamplesSuggestions(service: Service, operationName: string, numberOfSamples: number = 2): Observable { + const options = { params: new HttpParams().set('operation', operationName) }; + return this.http.get(this.rootUrl + '/copilot/samples/' + service.id, options); + } + + public launchSamplesGeneration(service: Service): Observable { + return this.http.get(this.rootUrl + '/copilot/samples/' + service.id); + } + + public getGenerationTaskStatus(taskId: string): Observable { + return this.http.get(this.rootUrl + '/copilot/samples/task/' + taskId + '/status'); + } + + public addSamplesSuggestions(service: Service, operationName: string, exchanges: Exchange[]): Observable { + const options = { params: new HttpParams().set('operation', operationName) }; + return this.http.post(this.rootUrl + '/copilot/samples/' + service.id, exchanges, options); + } + + public removeExchanges(service: Service, exchangeSelection: any): Observable { + return this.http.post(this.rootUrl + '/copilot/samples/' + service.id + '/cleanup', exchangeSelection); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth-anonymous.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth-anonymous.service.ts new file mode 100644 index 000000000..53bf764d5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth-anonymous.service.ts @@ -0,0 +1,132 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IAuthenticationService } from './auth.service'; +import { Observable, of } from 'rxjs'; +import { User } from '../models/user.model'; +import { ConfigService } from './config.service'; +import { HttpClient } from '@angular/common/http'; + +/** + * A version of the authentication service that uses keycloak.js to provide + * authentication services. + */ +export class AnonymousAuthenticationService extends IAuthenticationService { + + private user: User; + + /** + * Constructor. + */ + constructor(private http: HttpClient, private config: ConfigService) { + super(); + this.user = new User(); + this.user.login = 'admin'; + this.user.username = 'Anonymous Admin'; + this.user.name = 'Anonymous Admin'; + this.user.email = 'anonymous.admin@microcks.io'; + } + + /** + * Returns the observable for is/isnot authenticated. + */ + public isAuthenticated(): Observable { + return of(true); + } + + /** + * Returns an observable over the currently authenticated User (or null if not logged in). + */ + public getAuthenticatedUser(): Observable { + return of(this.user); + } + + /** + * Returns the currently authenticated user. + */ + public getAuthenticatedUserNow(): User { + return this.user; + } + + /** + * Not supported. + */ + public login(user: string, credential: any): Promise { + throw new Error('Not supported.'); + } + + /** + * Called to check that user can endorse a role. + */ + public hasRole(role: string): boolean { + return true; + } + + /** + * Called to check that user can endorse role for at least one resource. + */ + public hasRoleForAnyResource(role: string): boolean { + return true; + } + + /** + * Called to check that user can endorse role for a specific resource. + */ + public hasRoleForResource(role: string, resource: string): boolean { + return true; + } + + /** + * Logout. + */ + public logout(): void { + // Nothing to to here. + } + + /** + * Called to inject authentication headers into a remote API call. + */ + public injectAuthHeaders(headers: { [header: string]: string }): void { + // Nothing to do here. + } + + /** + * Called to return the keycloak access token. + */ + public getAuthenticationSecret(): string { + return 'admin'; + } + + /** + * Return the Keycloak realm name. + */ + public getRealmName(): string { + return 'microcks'; + } + + /** + * Return the Keycloak realm url. + */ + public getRealmUrl(): string { + throw new Error('Not supported.'); + } + + /** + * Return the Keycloak administration realm url. + */ + public getAdminRealmUrl(): string { + throw new Error('Not supported.'); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth-keycloak.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth-keycloak.service.ts new file mode 100644 index 000000000..be8682dcf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth-keycloak.service.ts @@ -0,0 +1,178 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IAuthenticationService } from './auth.service'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { User } from '../models/user.model'; +import { ConfigService } from './config.service'; +import { HttpClient } from '@angular/common/http'; + +/** + * A version of the authentication service that uses keycloak.js to provide + * authentication services. + */ +export class KeycloakAuthenticationService extends IAuthenticationService { + + private authenticated: BehaviorSubject = new BehaviorSubject(false); + public authenticated$: Observable = this.authenticated.asObservable(); + + private authenticatedUser: BehaviorSubject = new BehaviorSubject(new User()); + public authenticatedUser$: Observable = this.authenticatedUser.asObservable(); + + private keycloak: any; + + /** + * Constructor. + */ + constructor(private http: HttpClient, private config: ConfigService) { + super(); + const w: any = window; + this.keycloak = w.keycloak; + + // console.info("Token: %s", JSON.stringify(this.keycloak.tokenParsed, null, 2)); + // console.info("ID Token: %s", JSON.stringify(this.keycloak.idTokenParsed, null, 2)); + // console.info("Access Token: %s", this.keycloak.token); + + const user: User = new User(); + user.name = this.keycloak.tokenParsed.name; + user.login = this.keycloak.tokenParsed.preferred_username; + user.email = this.keycloak.tokenParsed.email; + + this.authenticated.next(true); + this.authenticatedUser.next(user); + + // Periodically refresh + // TODO run this outsize NgZone using zone.runOutsideAngular() : https://angular.io/api/core/NgZone + setInterval(() => { + this.keycloak.updateToken(30); + }, 30000); + } + + /** + * Returns the observable for is/isnot authenticated. + */ + public isAuthenticated(): Observable { + return this.authenticated$; + } + + /** + * Returns an observable over the currently authenticated User (or null if not logged in). + */ + public getAuthenticatedUser(): Observable { + return this.authenticatedUser$; + } + + /** + * Returns the currently authenticated user. + */ + public getAuthenticatedUserNow(): User { + return this.authenticatedUser.getValue(); + } + + /** + * Not supported. + */ + public login(user: string, credential: any): Promise { + throw new Error('Not supported.'); + } + + /** + * Called to check that user can endorse a role. + */ + public hasRole(role: string): boolean { + // console.log("[KeycloakAuthenticationService] hasRole called with " + role); + // Now default to a resource role for 'microcks-app' + // return this.keycloak.hasRealmRole(role); + + if (!this.keycloak.resourceAccess) { + return false; + } + const access = this.keycloak.resourceAccess['microcks-app'] || this.keycloak.resourceAccess[this.keycloak.clientId]; + return !!access && access.roles.indexOf(role) >= 0; + + // Don't know why but this fail as the code above is just copy-pasted from implementations... + // return this.keycloak.hasResourceRole('microcks-app', role); + } + + /** + * Called to check that user can endorse role for at least one resource. + */ + public hasRoleForAnyResource(role: string): boolean { + const rolePathPrefix = '/microcks/' + role + '/'; + const groups = this.keycloak.tokenParsed['microcks-groups']; + + return !!groups && groups.filter((element: string) => element.startsWith(rolePathPrefix)).length > 0; + } + + /** + * Called to check that user can endorse role for a specific resource. + */ + public hasRoleForResource(role: string, resource: string): boolean { + const rolePath = '/microcks/' + role + '/' + resource; + const groups = this.keycloak.tokenParsed['microcks-groups']; + + return !!groups && groups.indexOf(rolePath) >= 0; + } + + /** + * Logout. + */ + public logout(): void { + this.keycloak.logout({ redirectUri: location.href }); + } + + /** + * Called to inject authentication headers into a remote API call. + */ + public injectAuthHeaders(headers: { [header: string]: string }): void { + const authHeader: string = 'Bearer ' + this.keycloak.token; + headers['Authorization'] = authHeader; + } + + /** + * Called to return the keycloak access token. + */ + public getAuthenticationSecret(): string { + return this.keycloak.token; + } + + /** + * Return the Keycloak realm name. + */ + public getRealmName(): string { + return this.keycloak.realm; + } + + /** + * Return the Keycloak realm url. + */ + public getRealmUrl(): string { + const realmUrl = this.keycloak.createLogoutUrl(); + return realmUrl.slice(0, realmUrl.indexOf('/protocol/')); + } + + /** + * Return the Keycloak administration realm url. + */ + public getAdminRealmUrl(): string { + const realmUrl = this.getRealmUrl(); + if (realmUrl.indexOf('/auth/') != -1) { + // Pre-Keycloak-X url scheme. + return realmUrl.replace('/auth/', '/auth/admin/'); + } + // Keycloak-X url scheme. + return realmUrl.replace('/realms/', '/admin/realms/'); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.http-interceptor.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.http-interceptor.ts new file mode 100644 index 000000000..770f90a49 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.http-interceptor.ts @@ -0,0 +1,44 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { IAuthenticationService } from './auth.service'; + +@Injectable() +export class AuthenticationHttpInterceptor implements HttpInterceptor { + + constructor(protected authService: IAuthenticationService) { } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + //console.log('[AuthenticationHttpInterceptor] intercept for ' + req.method); + + if (req.method === 'OPTIONS') { + return next.handle(req); + } + + // Build new set of headers for authentication purpose. + if (this.authService.isAuthenticated()) { + const authHeaders: {[header: string]: string} = {}; + this.authService.injectAuthHeaders(authHeaders); + const changedReq = req.clone({setHeaders: authHeaders}); + return next.handle(changedReq); + } + + return next.handle(req); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.service.provider.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.service.provider.ts new file mode 100644 index 000000000..550f9a91c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.service.provider.ts @@ -0,0 +1,43 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IAuthenticationService } from './auth.service'; +import { ConfigService } from './config.service'; +import { KeycloakAuthenticationService } from './auth-keycloak.service'; +import { AnonymousAuthenticationService } from './auth-anonymous.service'; +import { HttpClient } from '@angular/common/http'; + + +export function AuthenticationServiceFactory(http: HttpClient, config: ConfigService): IAuthenticationService { + console.info('[AuthenticationServiceFactory] Creating AuthenticationService...'); + if (config.authType() === 'keycloakjs') { + console.info('[AuthenticationServiceFactory] Creating keycloak.js auth service.'); + return new KeycloakAuthenticationService(http, config); + } else if (config.authType() === 'anonymous') { + console.info('[AuthenticationServiceFactory] Creating Anonymous auth service.'); + return new AnonymousAuthenticationService(http, config); + } else { + console.error('[AuthenticationServiceFactory] Unsupported auth type: %s', config.authType()); + //return null; + return new AnonymousAuthenticationService(http, config); + } +} + + +export let AuthenticationServiceProvider = { + provide: IAuthenticationService, + useFactory: AuthenticationServiceFactory, + deps: [HttpClient, ConfigService] +}; diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.service.ts new file mode 100644 index 000000000..79f301652 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/auth.service.ts @@ -0,0 +1,73 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Observable } from 'rxjs'; + +import { User } from '../models/user.model'; + + +export abstract class IAuthenticationService { + + /** + * A way for consumers to subscribe to the current authentication status of the user/app. + */ + abstract isAuthenticated(): Observable; + + /** + * Get the currently authenticated user. May be null if the user is not currently authenticated. + */ + abstract getAuthenticatedUser(): Observable; + + /** + * Immediately gets the current authenticated user (if any). Returns null if no user is + * currently authenticated. + */ + abstract getAuthenticatedUserNow(): User; + + /** + * Called to authenticate a user. + */ + abstract login(user: string, credential: any): Promise; + + /** + * Called to check that user can endorse a role. + */ + abstract hasRole(role: string): boolean; + + /** + * Called to check that user can endorse role for at least one resource. + */ + abstract hasRoleForAnyResource(role: string): boolean; + + /** + * Called to check that user can endorse role for a specific resource. + */ + abstract hasRoleForResource(role: string, resource: string): boolean; + + /** + * Called to log out the current user. + */ + abstract logout(): void; + + /** + * Called to inject authentication headers into an API REST call. + */ + abstract injectAuthHeaders(headers: { [header: string]: string }): void; + + /** + * Called to return an authentication secret (e.g. the auth access token). + */ + abstract getAuthenticationSecret(): string; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/config.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/config.service.ts new file mode 100644 index 000000000..268d2c430 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/config.service.ts @@ -0,0 +1,122 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { User } from '../models/user.model'; + +const DEFAULT_CONFIG: any = { + mode: 'dev', + auth: { + type: 'keycloakjs' + } +}; + +const ANONYMOUS_AUTH_TYPE = 'anonymous'; + +/** + * A base service holding configuration of Microcks App. + */ +@Injectable({ providedIn: 'root' }) +export class ConfigService { + + private config: any; + + + constructor(private http: HttpClient) { + const w: any = window; + if (w.MicrocksConfig) { + this.config = w.MicrocksConfig; + console.info('[ConfigService] Found app config.'); + } else { + console.info('[ConfigService] App config not found!'); + this.config = DEFAULT_CONFIG; + + // Check Keycloak realm configuration. + const keycloak = w.keycloak; + //console.log('[ConfigService] w[\'keycloak\']: ' + JSON.stringify(w.keycloak)); + if (!keycloak || !keycloak.realm) { + console.info('[ConfigService] No Keycloak realm found. Switching to anonymous auth type.'); + this.config.auth.type = ANONYMOUS_AUTH_TYPE; + } + } + } + + public authType(): string { + if (!this.config.auth) { + return ''; + } + return this.config.auth.type; + } + + public authToken(): string { + if (!this.config.auth) { + return ''; + } + return this.config.auth.token; + } + + public authRefreshPeriod(): number { + if (!this.config.auth) { + return 10000; + } + return this.config.auth.tokenRefreshPeriod; + } + + public authData(): any { + if (!this.config.auth) { + return ''; + } + return this.config.auth.data; + } + + public logoutUrl(): string { + if (!this.config.auth) { + return ''; + } + return this.config.auth.logoutUrl; + } + + public user(): User { + return this.config.user as any; + } + + public loadConfiguredFeatures(): Promise { + console.info('[ConfigService] Completing config with additional features...'); + const featurePromise = this.http.get('/api/features/config') + .toPromise().then(results => { + this.config.features = results; + //console.info('[ConfigService] Got config: ' + JSON.stringify(this.config.features)); + return results; + }); + return featurePromise; + } + + public hasFeatureEnabled(feature: string): boolean { + if (this.config.features) { + const featureConfig = this.config.features[feature]; + return featureConfig.enabled === 'true'; + } + return false; + } + + public getFeatureProperty(feature: string, property: string): string { + if (this.config.features) { + const featureConfig = this.config.features[feature]; + return featureConfig[property]; + } + return ''; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/contracts.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/contracts.service.ts new file mode 100644 index 000000000..2d2be2abb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/contracts.service.ts @@ -0,0 +1,32 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { Contract } from '../models/service.model'; + +@Injectable({ providedIn: 'root' }) +export class ContractsService { + + private rootUrl = '/api'; + + constructor(private http: HttpClient) { } + + public listByServiceId(serviceId: string): Observable { + return this.http.get(this.rootUrl + '/resources/service/' + serviceId); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/hub.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/hub.service.ts new file mode 100644 index 000000000..5a9adc0e4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/hub.service.ts @@ -0,0 +1,65 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ConfigService } from './config.service'; +import { APIPackage, APIVersion } from '../models/hub.model'; + +@Injectable({ providedIn: 'root' }) +export class HubService { + + private rootUrl: string | null = null; + // private rootUrl: string = 'http://localhost:4000/api'; + // private rootUrl: string = 'http://microcks-hub/api'; + + constructor(private http: HttpClient, private config: ConfigService) { + this.rootUrl = this.config.getFeatureProperty('microcks-hub', 'endpoint'); + } + + public getPackages(): Observable { + this.ensureRootUrl(); + return this.http.get(this.rootUrl + '/mocks'); + } + + public getPackage(name: string): Observable { + this.ensureRootUrl(); + return this.http.get(this.rootUrl + '/mocks/' + name); + } + + public getLatestAPIVersions(packageName: string): Observable { + this.ensureRootUrl(); + return this.http.get(this.rootUrl + '/mocks/' + packageName + '/apis'); + } + + public getAPIVersion(packageName: string, apiVersionName: string): Observable { + this.ensureRootUrl(); + return this.http.get(this.rootUrl + '/mocks/' + packageName + '/apis/' + apiVersionName); + } + + public importAPIVersionContractContent(contractUrl: string, mainArtifact: boolean = true): Observable { + this.ensureRootUrl(); + const options = { params: new HttpParams().set('url', contractUrl).set('mainArtifact', String(mainArtifact)) }; + return this.http.post('/api/artifact/download', {}, options); + } + + private ensureRootUrl(): void { + if (this.rootUrl == null) { + this.rootUrl = this.config.getFeatureProperty('microcks-hub', 'endpoint'); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/importers.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/importers.service.ts new file mode 100644 index 000000000..5fef076b9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/importers.service.ts @@ -0,0 +1,77 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ImportJob } from '../models/importer.model'; + + +@Injectable({ providedIn: 'root' }) +export class ImportersService { + + private rootUrl = '/api'; + + constructor(private http: HttpClient) { } + + getImportJobs(page: number = 1, pageSize: number = 20): Observable { + const options = { params: new HttpParams().set('page', String(page - 1)).set('size', String(pageSize)) }; + return this.http.get(this.rootUrl + '/jobs', options); + } + + filterImportJobs(labelsFilter: Map, nameFilter: string): Observable { + let httpParams: HttpParams = new HttpParams(); + if (nameFilter != null) { + httpParams = httpParams.set('name', nameFilter); + } + if (labelsFilter != null) { + for (const key of Array.from( labelsFilter.keys() )) { + httpParams = httpParams.set('labels.' + key, labelsFilter.get(key) as string); + } + } + + const options = { params: httpParams }; + return this.http.get(this.rootUrl + '/jobs/search', options); + } + + countImportJobs(): Observable { + return this.http.get(this.rootUrl + '/jobs/count'); + } + + createImportJob(job: ImportJob): Observable { + return this.http.post(this.rootUrl + '/jobs', job); + } + + updateImportJob(job: ImportJob): Observable { + return this.http.post(this.rootUrl + '/jobs/' + job.id, job); + } + + deleteImportJob(job: ImportJob): Observable { + return this.http.delete(this.rootUrl + '/jobs/' + job.id); + } + + activateImportJob(job: ImportJob): Observable { + return this.http.put(this.rootUrl + '/jobs/' + job.id + '/activate', job); + } + + startImportJob(job: ImportJob): Observable { + return this.http.put(this.rootUrl + '/jobs/' + job.id + '/start', job); + } + + stopImportJob(job: ImportJob): Observable { + return this.http.put(this.rootUrl + '/jobs/' + job.id + '/stop', job); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/metrics.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/metrics.service.ts new file mode 100644 index 000000000..d07a942f4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/metrics.service.ts @@ -0,0 +1,86 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { DailyInvocations, TestConformanceMetric, TestResultSummary, WeightedMetricValue, } from '../models/metric.model'; + +@Injectable({ providedIn: 'root' }) +export class MetricsService { + + private rootUrl = '/api'; + + constructor(private http: HttpClient) { } + + public getInvocationStats(day: Date): Observable { + if (day != null) { + const dayStr = this.formatDayDate(day); + const options = { params: new HttpParams().set('day', dayStr) }; + return this.http.get(this.rootUrl + '/metrics/invocations/global', options); + } + return this.http.get(this.rootUrl + '/metrics/invocations/global'); + } + + public getTopInvocations(day: Date): Observable { + if (day != null) { + const dayStr = this.formatDayDate(day); + const options = { params: new HttpParams().set('day', dayStr) }; + return this.http.get(this.rootUrl + '/metrics/invocations/top', options); + } + return this.http.get(this.rootUrl + '/metrics/invocations/top'); + } + + public getServiceInvocationStats(serviceName: string, serviceVersion: string, day: Date): Observable { + if (day != null) { + const dayStr = this.formatDayDate(day); + const options = { params: new HttpParams().set('day', dayStr) }; + return this.http.get(this.rootUrl + '/metrics/invocations/' + serviceName + '/' + serviceVersion, options); + } + return this.http.get(this.rootUrl + '/metrics/invocations/' + serviceName + '/' + serviceVersion); + } + + public getInvocationsStatsTrend(limit: number): Observable { + if (limit != null) { + const options = { params: new HttpParams().set('limit', limit.toString()) }; + return this.http.get(this.rootUrl + '/metrics/invocations/global/latest', options); + } + return this.http.get(this.rootUrl + '/metrics/invocations/global/latest'); + } + + public getServiceTestConformanceMetric(serviceId: string): Observable { + return this.http.get(this.rootUrl + '/metrics/conformance/service/' + serviceId); + } + + public getAggregatedTestConformanceMetrics(): Observable { + return this.http.get(this.rootUrl + '/metrics/conformance/aggregate'); + } + + public getLatestTestsTrend(limit: number): Observable { + if (limit != null) { + const options = { params: new HttpParams().set('limit', limit.toString()) }; + return this.http.get(this.rootUrl + '/metrics/tests/latest', options); + } + return this.http.get(this.rootUrl + '/metrics/tests/latest'); + } + + public formatDayDate(day: Date): string { + let result = day.getFullYear().toString(); + result += day.getMonth() < 9 ? '0' + (day.getMonth() + 1).toString() : (day.getMonth() + 1).toString(); + result += day.getDate() < 10 ? '0' + day.getDate().toString() : day.getDate().toString(); + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/secrets.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/secrets.service.ts new file mode 100644 index 000000000..57a8f97eb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/secrets.service.ts @@ -0,0 +1,56 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { Secret } from '../models/secret.model'; +import { IAuthenticationService } from './auth.service'; + + +@Injectable({ providedIn: 'root' }) +export class SecretsService { + + private rootUrl = '/api'; + + constructor(private http: HttpClient) { } + + getSecrets(page: number = 1, pageSize: number = 20): Observable { + const options = { params: new HttpParams().set('page', String(page - 1)).set('size', String(pageSize)) }; + return this.http.get(this.rootUrl + '/secrets', options); + } + + filterSecrets(filter: string): Observable { + const options = { params: new HttpParams().set('name', filter) }; + return this.http.get(this.rootUrl + '/secrets', options); + } + + countSecrets(): Observable { + return this.http.get(this.rootUrl + '/secrets/count'); + } + + createSecret(secret: Secret): Observable { + return this.http.post(this.rootUrl + '/secrets', secret); + } + + updateSecret(secret: Secret): Observable { + return this.http.put(this.rootUrl + '/secrets/' + secret.id, secret); + } + + deleteSecret(secret: Secret): Observable { + return this.http.delete(this.rootUrl + '/secrets/' + secret.id); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/services.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/services.service.ts new file mode 100644 index 000000000..32b6a8c59 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/services.service.ts @@ -0,0 +1,154 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { Metadata } from '../models/commons.model'; +import { + Service, + ServiceView, + Api, + GenericResource, + OperationMutableProperties, +} from '../models/service.model'; + +@Injectable({ providedIn: 'root' }) +export class ServicesService { + private rootUrl = '/api'; + + constructor(private http: HttpClient) {} + + public getServices( + page: number = 1, + pageSize: number = 20 + ): Observable { + const options = { + params: new HttpParams() + .set('page', String(page - 1)) + .set('size', String(pageSize)), + }; + return this.http.get(this.rootUrl + '/services', options); + } + + public filterServices( + labelsFilter: Map, + nameFilter: string + ): Observable { + let httpParams: HttpParams = new HttpParams(); + if (nameFilter != null) { + httpParams = httpParams.set('name', nameFilter); + } + if (labelsFilter != null) { + for (const key of Array.from(labelsFilter.keys())) { + httpParams = httpParams.set('labels.' + key, labelsFilter.get(key) as string); + } + } + + const options = { params: httpParams }; + return this.http.get(this.rootUrl + '/services/search', options); + } + + public countServices(): Observable { + return this.http.get(this.rootUrl + '/services/count'); + } + + public getServicesMap(): Observable { + return this.http.get(this.rootUrl + '/services/map'); + } + + public getServicesLabels(): Observable { + return this.http.get(this.rootUrl + '/services/labels'); + } + + public getServiceView(serviceId: string): Observable { + const options = { params: new HttpParams().set('messages', 'true') }; + return this.http.get( + this.rootUrl + '/services/' + serviceId, + options + ); + } + + public getService(serviceId: string): Observable { + const options = { params: new HttpParams().set('messages', 'false') }; + return this.http.get( + this.rootUrl + '/services/' + serviceId, + options + ); + } + + public createDirectResourceAPI(api: Api): Observable { + return this.http.post(this.rootUrl + '/services/generic', api); + } + + public createDirectEventAPI(api: Api): Observable { + return this.http.post( + this.rootUrl + '/services/generic/event', + api + ); + } + + public deleteService(service: Service): Observable { + return this.http.delete(this.rootUrl + '/services/' + service.id); + } + + public getGenericResources( + service: Service, + page: number = 1, + pageSize: number = 20 + ): Observable { + const options = { + params: new HttpParams() + .set('page', String(page - 1)) + .set('size', String(pageSize)), + }; + return this.http.get( + this.rootUrl + '/genericresources/service/' + service.id, + options + ); + } + + public updateServiceMetadata( + service: Service, + metadata: Metadata + ): Observable { + return this.http.put( + this.rootUrl + '/services/' + service.id + '/metadata', + metadata + ); + } + + public updateServiceOperationProperties( + service: Service, + operationName: string, + properties: OperationMutableProperties + ): Observable { + const options = { + params: new HttpParams().set('operationName', operationName), + }; + return this.http.put( + this.rootUrl + '/services/' + service.id + '/operation', + properties, + options + ); + } + + public countGenericResources(service: Service): Observable { + return this.http.get( + this.rootUrl + '/genericresources/service/' + service.id + '/count' + ); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/tests.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/tests.service.ts new file mode 100644 index 000000000..d9f1ea77b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/tests.service.ts @@ -0,0 +1,66 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { TestRequest, TestResult } from '../models/test.model'; +import { RequestResponsePair, UnidirectionalEvent } from '../models/service.model'; + +@Injectable({ providedIn: 'root' }) +export class TestsService { + + private rootUrl = '/api'; + + constructor(private http: HttpClient) { } + + public listByServiceId(serviceId: string, page: number = 1, pageSize: number = 20): Observable { + const options = { params: new HttpParams().set('page', String(page - 1)).set('size', String(pageSize)) }; + return this.http.get(this.rootUrl + '/tests/service/' + serviceId, options); + } + + public countByServiceId(serviceId: string): Observable { + return this.http.get(this.rootUrl + '/tests/service/' + serviceId + '/count'); + } + + public getTestResult(resultId: string): Observable { + return this.http.get(this.rootUrl + '/tests/' + resultId); + } + + public create(testRequest: TestRequest): Observable { + return this.http.post(this.rootUrl + '/tests', testRequest); + } + + public getMessages(test: TestResult, operation: string): Observable { + // operation may contain / that are forbidden within encoded URI. + // Replace them by "!" and implement same protocole on server-side. + // Switched from _ to ! in replacement as less commonly used in URL parameters, in line with other frameworks e.g. Drupal + operation = operation.replace(/\//g, '!'); + const testCaseId = test.id + '-' + test.testNumber + '-' + encodeURIComponent(operation); + //console.log('[getMessages] called for ' + testCaseId); + return this.http.get(this.rootUrl + '/tests/' + test.id + '/messages/' + testCaseId); + } + + public getEventMessages(test: TestResult, operation: string): Observable { + // operation may contain / that are forbidden within encoded URI. + // Replace them by "!" and implement same protocole on server-side. + // Switched from _ to ! in replacement as less commonly used in URL parameters, in line with other frameworks e.g. Drupal + operation = operation.replace(/\//g, '!'); + const testCaseId = test.id + '-' + test.testNumber + '-' + encodeURIComponent(operation); + //console.log('[getEventMessages] called for ' + testCaseId); + return this.http.get(this.rootUrl + '/tests/' + test.id + '/events/' + testCaseId); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/users.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/users.service.ts new file mode 100644 index 000000000..fb90a084f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/users.service.ts @@ -0,0 +1,136 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { User } from '../models/user.model'; +import { IAuthenticationService } from './auth.service'; +import { KeycloakAuthenticationService } from './auth-keycloak.service'; + +@Injectable({ providedIn: 'root' }) +export class UsersService { + + private rootUrl = '/api'; + + private microcksAppClientId: string | null = null; + + constructor(private http: HttpClient, protected authService: IAuthenticationService) { + if (authService instanceof KeycloakAuthenticationService) { + this.rootUrl = (authService as KeycloakAuthenticationService).getAdminRealmUrl(); + this.loadClientId(); + } + } + + private loadClientId(): void { + this.http.get(this.rootUrl + '/clients?clientId=microcks-app&max=2&search=true').subscribe( + { + next: res => { + const client = res.find(c => c.clientId === 'microcks-app'); + if (client) { + this.microcksAppClientId = client.id; + } + }, + error: err => { + console.warn('Unable to retrieve microcksAppClientId from Keycloak. Maybe you do not have correct roles?'); + this.microcksAppClientId = null; + }, + } + ); + } + + getRealmName(): string | null { + if (this.authService instanceof KeycloakAuthenticationService) { + return (this.authService as KeycloakAuthenticationService).getRealmName(); + } + return null; + } + + getGroups(): Observable { + // 'search' for Pre-Keycloak-X, 'q' for 'Keycloak-X' + const options = { + params: new HttpParams().set('search', 'microcks').set('q', 'microcks') + .set('populateHierarchy', 'false') + }; + return this.http.get(this.rootUrl + '/groups', options); + } + createGroup(parentGroupId: string, name: string): Observable { + const group = {name}; + return this.http.post(this.rootUrl + '/groups/' + parentGroupId + '/children', group); + } + + getUsers(page: number = 1, pageSize: number = 20): Observable { + let first = 0; + if (page > 1) { + first += pageSize * (page - 1); + } + const options = { params: new HttpParams().set('first', String(first)).set('max', String(pageSize)) }; + return this.http.get(this.rootUrl + '/users', options); + } + + getMicrocksAppClientId(): string | null { + return this.microcksAppClientId; + } + + filterUsers(filter: string): Observable { + const options = { params: new HttpParams().set('search', filter) }; + return this.http.get(this.rootUrl + '/users', options); + } + + countUsers(): Observable { + return this.http.get(this.rootUrl + '/users/count'); + } + + getUserRealmRoles(userId: string): Observable { + return this.http.get(this.rootUrl + '/users/' + userId + '/role-mappings/realm'); + } + getUserRoles(userId: string): Observable { + return this.http.get(this.rootUrl + '/users/' + userId + '/role-mappings/clients/' + this.microcksAppClientId); + } + getUserGroups(userId: string): Observable { + return this.http.get(this.rootUrl + '/users/' + userId + '/groups'); + } + + assignRoleToUser(userId: string, roleName: string): Observable { + return this.getRoleByName(roleName).pipe( + switchMap((role: any) => { + return this.http.post(this.rootUrl + '/users/' + userId + '/role-mappings/clients/' + this.microcksAppClientId, [ role ]); + }) + ); + } + removeRoleFromUser(userId: string, roleName: string): Observable { + return this.getRoleByName(roleName).pipe( + switchMap((role: any) => { + return this.http.request('delete', + this.rootUrl + '/users/' + userId + '/role-mappings/clients/' + this.microcksAppClientId, { body: [ role ] }); + }) + ); + } + + putUserInGroup(userId: string, groupId: string): Observable { + const data = {realm: this.getRealmName(), userId, groupId}; + return this.http.put(this.rootUrl + '/users/' + userId + '/groups/' + groupId, { body: data }); + } + removeUserFromGroup(userId: string, groupId: string): Observable { + return this.http.request('delete', + this.rootUrl + '/users/' + userId + '/groups/' + groupId); + } + + private getRoleByName(role: string): Observable { + return this.http.get(this.rootUrl + '/clients/' + this.microcksAppClientId + '/roles/' + role); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/versioninfo.service.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/versioninfo.service.ts new file mode 100644 index 000000000..3311c1489 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/app/services/versioninfo.service.ts @@ -0,0 +1,30 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class VersionInfoService { + + private rootUrl = '/api'; + + constructor(private http: HttpClient) { } + + public getVersionInfo(): Observable { + return this.http.get(this.rootUrl + '/version/info'); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/.gitkeep b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/favicon.ico b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/favicon.ico new file mode 100644 index 000000000..e233ec562 Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/favicon.ico differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/hub-microcks.svg b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/hub-microcks.svg new file mode 100644 index 000000000..029f48a6d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/hub-microcks.svg @@ -0,0 +1,32 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-1.svg b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-1.svg new file mode 100644 index 000000000..4532400de --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-1.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-2.svg b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-2.svg new file mode 100644 index 000000000..38b0c03e4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-2.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-3.svg b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-3.svg new file mode 100644 index 000000000..0a1413823 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/images/mocks-level-3.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-big.png b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-big.png new file mode 100644 index 000000000..8d4fce993 Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-big.png differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-logo-blue-baseline-tweet.png b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-logo-blue-baseline-tweet.png new file mode 100644 index 000000000..aa97216c7 Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-logo-blue-baseline-tweet.png differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-logo-white-name.png b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-logo-white-name.png new file mode 100644 index 000000000..dd0d3804f Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks-logo-white-name.png differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks.png b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks.png new file mode 100644 index 000000000..62ac22899 Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/assets/microcks.png differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/favicon.ico b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/favicon.ico new file mode 100644 index 000000000..8081c7cea Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/favicon.ico differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/index.html b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/index.html new file mode 100644 index 000000000..1d3914a2a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/index.html @@ -0,0 +1,15 @@ + + + + + + Microcks-UI + + + + + + + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/lib/fonts/MaterialIcons.woff2 b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/lib/fonts/MaterialIcons.woff2 new file mode 100644 index 000000000..f1fd22ff1 Binary files /dev/null and b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/lib/fonts/MaterialIcons.woff2 differ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/lib/patternfly-ng.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/lib/patternfly-ng.css new file mode 100644 index 000000000..ff347424b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/lib/patternfly-ng.css @@ -0,0 +1,1364 @@ +/* stylelint-disable */ +.margin-20 { + margin: 20px !important; +} +.margin-top-20 { + margin-top: 20px !important; +} +.margin-left-20 { + margin-left: 20px !important; +} +.margin-bottom-20 { + margin-bottom: 20px !important; +} +.margin-right-20 { + margin-right: 20px !important; +} +.margin-19 { + margin: 19px !important; +} +.margin-top-19 { + margin-top: 19px !important; +} +.margin-left-19 { + margin-left: 19px !important; +} +.margin-bottom-19 { + margin-bottom: 19px !important; +} +.margin-right-19 { + margin-right: 19px !important; +} +.margin-18 { + margin: 18px !important; +} +.margin-top-18 { + margin-top: 18px !important; +} +.margin-left-18 { + margin-left: 18px !important; +} +.margin-bottom-18 { + margin-bottom: 18px !important; +} +.margin-right-18 { + margin-right: 18px !important; +} +.margin-17 { + margin: 17px !important; +} +.margin-top-17 { + margin-top: 17px !important; +} +.margin-left-17 { + margin-left: 17px !important; +} +.margin-bottom-17 { + margin-bottom: 17px !important; +} +.margin-right-17 { + margin-right: 17px !important; +} +.margin-16 { + margin: 16px !important; +} +.margin-top-16 { + margin-top: 16px !important; +} +.margin-left-16 { + margin-left: 16px !important; +} +.margin-bottom-16 { + margin-bottom: 16px !important; +} +.margin-right-16 { + margin-right: 16px !important; +} +.margin-15 { + margin: 15px !important; +} +.margin-top-15 { + margin-top: 15px !important; +} +.margin-left-15 { + margin-left: 15px !important; +} +.margin-bottom-15 { + margin-bottom: 15px !important; +} +.margin-right-15 { + margin-right: 15px !important; +} +.margin-14 { + margin: 14px !important; +} +.margin-top-14 { + margin-top: 14px !important; +} +.margin-left-14 { + margin-left: 14px !important; +} +.margin-bottom-14 { + margin-bottom: 14px !important; +} +.margin-right-14 { + margin-right: 14px !important; +} +.margin-13 { + margin: 13px !important; +} +.margin-top-13 { + margin-top: 13px !important; +} +.margin-left-13 { + margin-left: 13px !important; +} +.margin-bottom-13 { + margin-bottom: 13px !important; +} +.margin-right-13 { + margin-right: 13px !important; +} +.margin-12 { + margin: 12px !important; +} +.margin-top-12 { + margin-top: 12px !important; +} +.margin-left-12 { + margin-left: 12px !important; +} +.margin-bottom-12 { + margin-bottom: 12px !important; +} +.margin-right-12 { + margin-right: 12px !important; +} +.margin-11 { + margin: 11px !important; +} +.margin-top-11 { + margin-top: 11px !important; +} +.margin-left-11 { + margin-left: 11px !important; +} +.margin-bottom-11 { + margin-bottom: 11px !important; +} +.margin-right-11 { + margin-right: 11px !important; +} +.margin-10 { + margin: 10px !important; +} +.margin-top-10 { + margin-top: 10px !important; +} +.margin-left-10 { + margin-left: 10px !important; +} +.margin-bottom-10 { + margin-bottom: 10px !important; +} +.margin-right-10 { + margin-right: 10px !important; +} +.margin-9 { + margin: 9px !important; +} +.margin-top-9 { + margin-top: 9px !important; +} +.margin-left-9 { + margin-left: 9px !important; +} +.margin-bottom-9 { + margin-bottom: 9px !important; +} +.margin-right-9 { + margin-right: 9px !important; +} +.margin-8 { + margin: 8px !important; +} +.margin-top-8 { + margin-top: 8px !important; +} +.margin-left-8 { + margin-left: 8px !important; +} +.margin-bottom-8 { + margin-bottom: 8px !important; +} +.margin-right-8 { + margin-right: 8px !important; +} +.margin-7 { + margin: 7px !important; +} +.margin-top-7 { + margin-top: 7px !important; +} +.margin-left-7 { + margin-left: 7px !important; +} +.margin-bottom-7 { + margin-bottom: 7px !important; +} +.margin-right-7 { + margin-right: 7px !important; +} +.margin-6 { + margin: 6px !important; +} +.margin-top-6 { + margin-top: 6px !important; +} +.margin-left-6 { + margin-left: 6px !important; +} +.margin-bottom-6 { + margin-bottom: 6px !important; +} +.margin-right-6 { + margin-right: 6px !important; +} +.margin-5 { + margin: 5px !important; +} +.margin-top-5 { + margin-top: 5px !important; +} +.margin-left-5 { + margin-left: 5px !important; +} +.margin-bottom-5 { + margin-bottom: 5px !important; +} +.margin-right-5 { + margin-right: 5px !important; +} +.margin-4 { + margin: 4px !important; +} +.margin-top-4 { + margin-top: 4px !important; +} +.margin-left-4 { + margin-left: 4px !important; +} +.margin-bottom-4 { + margin-bottom: 4px !important; +} +.margin-right-4 { + margin-right: 4px !important; +} +.margin-3 { + margin: 3px !important; +} +.margin-top-3 { + margin-top: 3px !important; +} +.margin-left-3 { + margin-left: 3px !important; +} +.margin-bottom-3 { + margin-bottom: 3px !important; +} +.margin-right-3 { + margin-right: 3px !important; +} +.margin-2 { + margin: 2px !important; +} +.margin-top-2 { + margin-top: 2px !important; +} +.margin-left-2 { + margin-left: 2px !important; +} +.margin-bottom-2 { + margin-bottom: 2px !important; +} +.margin-right-2 { + margin-right: 2px !important; +} +.margin-1 { + margin: 1px !important; +} +.margin-top-1 { + margin-top: 1px !important; +} +.margin-left-1 { + margin-left: 1px !important; +} +.margin-bottom-1 { + margin-bottom: 1px !important; +} +.margin-right-1 { + margin-right: 1px !important; +} +.padding-20 { + padding: 20px !important; +} +.padding-top-20 { + padding-top: 20px !important; +} +.padding-left-20 { + padding-left: 20px !important; +} +.padding-bottom-20 { + padding-bottom: 20px !important; +} +.padding-right-20 { + padding-right: 20px !important; +} +.padding-19 { + padding: 19px !important; +} +.padding-top-19 { + padding-top: 19px !important; +} +.padding-left-19 { + padding-left: 19px !important; +} +.padding-bottom-19 { + padding-bottom: 19px !important; +} +.padding-right-19 { + padding-right: 19px !important; +} +.padding-18 { + padding: 18px !important; +} +.padding-top-18 { + padding-top: 18px !important; +} +.padding-left-18 { + padding-left: 18px !important; +} +.padding-bottom-18 { + padding-bottom: 18px !important; +} +.padding-right-18 { + padding-right: 18px !important; +} +.padding-17 { + padding: 17px !important; +} +.padding-top-17 { + padding-top: 17px !important; +} +.padding-left-17 { + padding-left: 17px !important; +} +.padding-bottom-17 { + padding-bottom: 17px !important; +} +.padding-right-17 { + padding-right: 17px !important; +} +.padding-16 { + padding: 16px !important; +} +.padding-top-16 { + padding-top: 16px !important; +} +.padding-left-16 { + padding-left: 16px !important; +} +.padding-bottom-16 { + padding-bottom: 16px !important; +} +.padding-right-16 { + padding-right: 16px !important; +} +.padding-15 { + padding: 15px !important; +} +.padding-top-15 { + padding-top: 15px !important; +} +.padding-left-15 { + padding-left: 15px !important; +} +.padding-bottom-15 { + padding-bottom: 15px !important; +} +.padding-right-15 { + padding-right: 15px !important; +} +.padding-14 { + padding: 14px !important; +} +.padding-top-14 { + padding-top: 14px !important; +} +.padding-left-14 { + padding-left: 14px !important; +} +.padding-bottom-14 { + padding-bottom: 14px !important; +} +.padding-right-14 { + padding-right: 14px !important; +} +.padding-13 { + padding: 13px !important; +} +.padding-top-13 { + padding-top: 13px !important; +} +.padding-left-13 { + padding-left: 13px !important; +} +.padding-bottom-13 { + padding-bottom: 13px !important; +} +.padding-right-13 { + padding-right: 13px !important; +} +.padding-12 { + padding: 12px !important; +} +.padding-top-12 { + padding-top: 12px !important; +} +.padding-left-12 { + padding-left: 12px !important; +} +.padding-bottom-12 { + padding-bottom: 12px !important; +} +.padding-right-12 { + padding-right: 12px !important; +} +.padding-11 { + padding: 11px !important; +} +.padding-top-11 { + padding-top: 11px !important; +} +.padding-left-11 { + padding-left: 11px !important; +} +.padding-bottom-11 { + padding-bottom: 11px !important; +} +.padding-right-11 { + padding-right: 11px !important; +} +.padding-10 { + padding: 10px !important; +} +.padding-top-10 { + padding-top: 10px !important; +} +.padding-left-10 { + padding-left: 10px !important; +} +.padding-bottom-10 { + padding-bottom: 10px !important; +} +.padding-right-10 { + padding-right: 10px !important; +} +.padding-9 { + padding: 9px !important; +} +.padding-top-9 { + padding-top: 9px !important; +} +.padding-left-9 { + padding-left: 9px !important; +} +.padding-bottom-9 { + padding-bottom: 9px !important; +} +.padding-right-9 { + padding-right: 9px !important; +} +.padding-8 { + padding: 8px !important; +} +.padding-top-8 { + padding-top: 8px !important; +} +.padding-left-8 { + padding-left: 8px !important; +} +.padding-bottom-8 { + padding-bottom: 8px !important; +} +.padding-right-8 { + padding-right: 8px !important; +} +.padding-7 { + padding: 7px !important; +} +.padding-top-7 { + padding-top: 7px !important; +} +.padding-left-7 { + padding-left: 7px !important; +} +.padding-bottom-7 { + padding-bottom: 7px !important; +} +.padding-right-7 { + padding-right: 7px !important; +} +.padding-6 { + padding: 6px !important; +} +.padding-top-6 { + padding-top: 6px !important; +} +.padding-left-6 { + padding-left: 6px !important; +} +.padding-bottom-6 { + padding-bottom: 6px !important; +} +.padding-right-6 { + padding-right: 6px !important; +} +.padding-5 { + padding: 5px !important; +} +.padding-top-5 { + padding-top: 5px !important; +} +.padding-left-5 { + padding-left: 5px !important; +} +.padding-bottom-5 { + padding-bottom: 5px !important; +} +.padding-right-5 { + padding-right: 5px !important; +} +.padding-4 { + padding: 4px !important; +} +.padding-top-4 { + padding-top: 4px !important; +} +.padding-left-4 { + padding-left: 4px !important; +} +.padding-bottom-4 { + padding-bottom: 4px !important; +} +.padding-right-4 { + padding-right: 4px !important; +} +.padding-3 { + padding: 3px !important; +} +.padding-top-3 { + padding-top: 3px !important; +} +.padding-left-3 { + padding-left: 3px !important; +} +.padding-bottom-3 { + padding-bottom: 3px !important; +} +.padding-right-3 { + padding-right: 3px !important; +} +.padding-2 { + padding: 2px !important; +} +.padding-top-2 { + padding-top: 2px !important; +} +.padding-left-2 { + padding-left: 2px !important; +} +.padding-bottom-2 { + padding-bottom: 2px !important; +} +.padding-right-2 { + padding-right: 2px !important; +} +.padding-1 { + padding: 1px !important; +} +.padding-top-1 { + padding-top: 1px !important; +} +.padding-left-1 { + padding-left: 1px !important; +} +.padding-bottom-1 { + padding-bottom: 1px !important; +} +.padding-right-1 { + padding-right: 1px !important; +} +/* stylelint-enable */ +.pfng-card .card-pf-footer { + min-height: 60px; +} +.pfng-card.pfng-card-no-padding.card-pf { + padding-left: 0; + padding-right: 0; +} +.pfng-card.pfng-card-no-padding .card-pf-body { + margin-top: 0; + padding-bottom: 0; +} +.pfng-card.pfng-card-no-padding .card-pf-heading { + margin-bottom: 0; + margin-left: 0; + margin-right: 0; +} +.pfng-card-heading-no-bottom { + margin: 0 -20px 0px; + padding: 0 20px; +} +.pfng-card-info-status { + display: flex; + margin: 0 10px; +} +.pfng-card-info-status .pfng-card-info-image { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + margin-right: 15px; +} +.pfng-card-info-status .pfng-card-info-image .info-icon { + font-size: 50px; +} +.pfng-card-info-status .pfng-card-info-image .info-img { + max-height: 50px; +} +.pfng-card-info-status .pfng-card-info-content { + margin: 10px 0; +} +.pfng-card-info-status .pfng-card-info-content .pfng-card-title { + margin-top: 10px; + margin-bottom: 15px; +} +.pfng-block-copy { + max-width: 100%; +} +.pfng-block-copy-inner-container { + background: #fff; + border: 1px solid #bbb; +} +.pfng-block-copy-preview { + display: flex; +} +.pfng-block-copy-preview.pf-is-open { + border-bottom: 1px solid #bbb; +} +.pfng-block-copy-preview-btn { + border: none; + border-right: 1px solid #bbb; + padding: 0; +} +.pfng-block-copy-preview-icon { + cursor: pointer; + padding: 0 0.679em; +} +.pfng-block-copy-preview-icon.fa-angle-right { + padding: 0 0.822em; +} +.pfng-block-copy-preview-txt-cont { + display: flex; + align-items: center; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; +} +.pfng-block-copy-preview-txt { + overflow: hidden; + text-overflow: ellipsis; + margin-right: 1em; + margin-left: 0.75em; + border: none; + width: 100%; +} +.pfng-block-copy-btn { + border: none; + border-left: 1px solid #bbb; + box-shadow: none; + font-size: 1em; +} +.pfng-block-copy-btn:active { + box-shadow: inset 0 2px 5px 0 rgba(0, 0, 0, 0.25); +} +.pfng-block-copy-body { + padding: 15px; + word-wrap: break-word; +} +.pfng-inline-copy { + display: inline-flex; + border: 1px solid #bbb; + background-color: #fff; + font-size: smaller; + margin-left: 0.25em; + margin-right: 0.25em; + max-width: 100%; +} +.pfng-inline-copy-body { + width: 100%; +} +.pfng-inline-copy-txt-cont { + flex-grow: 1; + padding: 2px 6px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + border: none; + width: 100%; +} +.pfng-inline-copy-btn { + border: none; + border-left: 1px solid #bbb; + box-shadow: none; + background-image: linear-gradient(to bottom, #fafafa, #ededed); +} +.pfng-inline-copy-btn:hover, +.pfng-inline-copy-btn:active, +.pfng-inline-copy-btn:focus { + background-color: #f1f1f1; +} +.blank-slate-pf { + margin-bottom: 0; +} +.blank-slate-pf button { + margin-right: 4px; +} +.filter-pf a { + cursor: pointer; +} +/* Fixes issue #130 */ +.filter-select .btn { + border-left: 0; +} +.dropdown-menu { + min-width: 176px; +} +.filter-pf a { + cursor: pointer; +} +.filter-pf.filter-fields .form-group { + padding-left: 0; + width: 275px; +} +.filter-pf.filter-fields .tooltip { + white-space: nowrap; +} +.filter-pf.filter-fields .typeahead-input-container { + position: relative; + padding-right: 0; +} +.filter-pf.filter-fields .typeahead-input-container.disabled { + width: 100%; +} +.filter-pf.filter-fields .typeahead-input-container.disabled .caret { + top: 8px; +} +.filter-pf.filter-fields .typeahead-input-container .caret { + color: #8b8d8f; + font-style: italic; + position: absolute; + top: 10px; + right: 12px; + z-index: 2; +} +.filter-select .btn-default { + background-color: #fff; + background-image: none; +} +.filter-select .btn-default .placeholder { + color: #8b8d8f; + font-style: italic; + font-weight: 400; +} +.filter-select .avatar { + height: 20px; + margin-right: 5px; +} +/* stylelint-disable */ +.input-group .input-group-btn .dropdown-menu > .selected > a { + background-color: #0088ce !important; + border-color: #0088ce !important; + color: #fff; +} +/* styleint-enable */ +.pfng-filter-delete { + font-size: 12px; + opacity: 0; + padding-top: 4px; +} +.pfng-filter-delete a { + color: #030303; + opacity: 0.7; +} +.pfng-filter-delete a:hover { + opacity: 1; +} +.dropdown-item:hover .pfng-filter-delete { + opacity: 1; + transition-duration: 0.5s; + transition-property: opacity; + transition-timing-function: ease-in; +} +.pfng-filter-delete-confirm { + font-size: 12px; + padding-bottom: 5px; + padding-right: 5px; + padding-top: 5px; +} +.pfng-filter-delete-confirm a { + color: #fff; + opacity: 0.9; +} +.pfng-filter-delete-confirm a .fa:before { + color: #fff; +} +.pfng-filter-delete-confirm a:hover { + opacity: 1; +} +.pfng-filter-delete-slide { + background-color: #cc0000; + right: -100%; + position: absolute; + transition-duration: 1s; + width: 100%; + z-index: 1000; +} +.pfng-filter-delete-slide.slide-in { + right: 0; +} +.pfng-filter-delete-slide.slide-in .close { + opacity: 0.9; + transition-delay: 0.5s; + transition-duration: 0.5s; + transition-property: opacity; + transition-timing-function: ease-in; +} +.pfng-filter-delete-text { + color: #fff; + padding-left: 10px; + padding-top: 2px; + position: absolute; +} +.pfng-filter-delete-wrapper { + position: relative; + overflow: hidden; +} +.table-view-pf-select-results { + padding-top: 10px; +} +.filter-pf a { + cursor: pointer; +} +.filter-pf .pficon-close { + cursor: pointer; +} +.pfng-save-filter-close { + font-size: 12px; + padding-top: 3px; +} +.pfng-save-filter input { + display: inline; +} +@media (min-width: 480px) { + .pfng-save-filter input { + width: 10em; + } +} +@media (min-width: 768px) { + .pfng-save-filter input { + width: 15em; + } +} +.pfng-save-filter .popover { + max-width: initial; +} +.pfng-save-filter-divider { + border-top: 1px solid #d1d1d1; + margin-left: -15px; + margin-right: -15px; + margin-top: 10px; +} +.pfng-save-filter-footer { + float: right; + margin-bottom: 10px; + margin-top: 10px; +} +.pfng-list-cb-placeholder { + width: 12px; +} +.pfng-list-heading { + display: flex; + flex-grow: 1; +} +.pfng-list-expand-placeholder { + width: 8px; +} +.pfng-list-expand .fa-angle-right { + padding-left: 5px; +} +.pfng-list-expand .fa-angle-right.fa-angle-down { + padding-left: 0; +} +.list-pf-container .list-pf-chevron { + cursor: pointer; +} +.list-pf-container .list-pf-chevron:hover { + color: #0088ce; +} +.pfng-list-expansion { + position: relative; +} +.pfng-list-expansion .list-pf-content { + flex-grow: 1; +} +.pfng-list-heading { + pointer-events: none; +} +@media (max-width: 992px) { + .pfng-list-heading { + display: none; + } +} +.pfng-list-heading:hover { + background-color: #fff; +} +.pfng-list-heading i { + pointer-events: auto; +} +.pfng-list-heading.list-pf-item { + border-top: none; +} +.pfng-list-heading .list-pf-title { + font-size: inherit; + font-weight: normal; +} +.pfng-list-heading .list-pf-chevron + .list-pf-content, +.pfng-list-heading .list-pf-select + .list-pf-content { + border-left: none; +} +.pfng-list-pin { + align-items: center; + align-self: stretch; + background-color: #f5f5f5; + box-shadow: -3px 1px 4px 0 #ededed inset; + display: flex; + margin-bottom: -20px; + margin-left: -20px; + margin-right: 20px; + margin-top: -20px; + padding-bottom: 20px; + padding-left: 5px; + padding-right: 5px; + padding-top: 20px; +} +.pfng-list-pin.multi-ctrls { + margin-right: 10px; +} +.pfng-list-pin a { + color: #030303; + opacity: 0.7; +} +.pfng-list-pin a:hover { + opacity: 1; +} +.pfng-list-pin-container { + align-self: stretch; + display: flex; +} +.pfng-list-pin-placeholder { + margin-left: -20px; + width: 39px; +} +.pfng-list-pin-placeholder.multi-ctrls { + width: 28px; +} +.pfng-vertical-hide-nav .nav-pf-vertical { + top: 2px; +} +.drawer-pf { + overflow-y: hidden; + display: flex; + flex-direction: column; +} +.drawer-pf .drawer-pf-title { + position: relative; +} +.drawer-pf .panel-group { + display: flex; + flex-direction: column; + position: initial; + bottom: initial; + top: initial; + overflow-y: auto; +} +.drawer-pf .panel-group .panel.panel-default.expanded { + min-height: 175px; + flex: 1 1 auto; + display: flex; + flex-direction: column; +} +.drawer-pf .panel-group .panel-collapse.in { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; +} +.drawer-pf .panel-group .panel-collapse.in .panel-body { + flex: 1 1 auto; + overflow-y: auto; +} +.drawer-pf .drawer-pf-action { + flex: none; +} +.toast-pf-action > a { + cursor: pointer; +} +.toast-pf .dropdown-menu > li > a { + cursor: pointer; +} +.sort-pf .btn-link { + margin-left: 10px; + padding: 4px 0; + min-width: 0; + color: #030303; + font-size: 16px; + line-height: 1; +} +.sort-pf .btn-link:hover { + color: #0088ce; +} +.pfng-table .blank-slate-pf { + border-bottom: 1px solid #d1d1d1; + border-left: 1px solid #d1d1d1; + border-right: 1px solid #d1d1d1; + border-top: 1px solid #d1d1d1; +} +.pfng-table .content-view-pf-pagination { + border-top: none; +} +.pfng-table .row.toolbar-pf { + margin-left: 0; + margin-right: 0; + background-color: #f5f5f5; + border: 1px solid #d1d1d1; + border-bottom: none; +} +.pfng-table .table-view-pf-select-results { + padding-bottom: 10px; +} +.pfng-table-dnd-container { + align-self: stretch; + display: flex; +} +.pfng-table-dnd-header:before { + background-image: linear-gradient(to bottom, #0088ce 60%, #fff 0%); + background-position: left; + background-repeat: repeat-y; + background-size: 2px 5px; + border: 4px solid #0088ce; + border-color: #00659c; + content: ''; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 10px; +} +.pfng-table-dnd-header:hover { + cursor: move; +} +.pfng-table-expand-container { + align-items: flex-start; + border-bottom: 1px solid #d1d1d1; + border-left: 1px solid #d1d1d1; + border-right: 1px solid #d1d1d1; + display: flex; + padding: 20px; +} +.pfng-table-expand-content { + flex-grow: 1; +} +.pfng-table-select .ng-untouched { + margin-top: 0; +} +.pfng-table .row.toolbar-pf { + background-color: #f5f5f5; + border-bottom: none; + border-left: 1px solid #d1d1d1; + border-right: 1px solid #d1d1d1; + border-top: 1px solid #d1d1d1; + box-shadow: none; + margin-left: 0; + margin-right: 0; +} +.pfng-table .table-view-pf-select-results { + padding-bottom: 10px; +} +/* +* PatternFly table theme for ngx-datatable +*/ +.ngx-datatable { + /* + * dragula style overwrites + */ +} +.ngx-datatable .gu-mirror { + /* stylelint-disable */ + position: fixed !important; + margin: 0 !important; + z-index: 9999 !important; + /* stylelint-enable */ + opacity: 0.5; + /* -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; */ + /* filter: alpha(opacity=50); */ +} +.ngx-datatable .gu-hide { + /* stylelint-disable */ + display: none !important; + /* stylelint-enable */ +} +.ngx-datatable .gu-unselectable { + /* stylelint-disable */ + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + /* stylelint-enable */ +} +.ngx-datatable .gu-transit { + display: inline-flex; + color: #fff; + background-color: #0088ce; + opacity: 0.5; + /* -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; */ + /* filter: alpha(opacity=50); */ +} +.ngx-datatable.patternfly { + margin-bottom: -5px; + border-top: 1px solid #d1d1d1; + box-shadow: none; +} +.ngx-datatable.patternfly .datatable-header { + /* stylelint-disable */ + background-image: -webkit-linear-gradient(top, #fafafa 0, #ededed 100%); + /* stylelint-enable */ + background-image: linear-gradient(to bottom, #fafafa 0, #ededed 100%); + background-repeat: repeat-x; + background-color: #f5f5f5; + /* stylelint-disable */ + height: unset !important; + /* stylelint-enable */ +} +.ngx-datatable.patternfly .datatable-header-cell { + border-right: 1px solid #d1d1d1; + border-bottom: 1px solid #d1d1d1; + font-weight: 600; + padding: 2px 10px 3px; + vertical-align: bottom; +} +.ngx-datatable.patternfly .datatable-header-cell:first-child { + border-left: 1px solid #d1d1d1; +} +.ngx-datatable.patternfly .datatable-header-cell.pfng-table-select { + padding-top: 6px; + text-align: center; +} +.ngx-datatable.patternfly .datatable-header-cell.pfng-table-dnd-only { + padding-left: 0; + padding-right: 0; +} +.ngx-datatable.patternfly .datatable-header-cell.sortable.sort-active { + color: #0088ce; +} +.ngx-datatable.patternfly .datatable-header-cell.sortable.sort-active.sort-asc, +.ngx-datatable.patternfly .datatable-header-cell.sortable.sort-active.sort-desc { + /* stylelint-disable */ + color: #0088ce !important; + /* stylelint-enable */ + position: relative; + /* stylelint-disable */ + background-image: none !important; + /* stylelint-enable */ + padding-top: 2px; +} +.ngx-datatable.patternfly .datatable-header-cell.sortable.sort-active.sort-asc:before, +.ngx-datatable.patternfly .datatable-header-cell.sortable.sort-active.sort-desc:before { + background: #0088ce; + content: ''; + height: 2px; + position: absolute; + left: 0; + top: 0; + width: 100%; +} +.ngx-datatable.patternfly .datatable-header-cell.sortable.sort-active.sort-asc .datatable-header-cell-label:after, +.ngx-datatable.patternfly .datatable-header-cell.sortable.sort-active.sort-desc .datatable-header-cell-label:after { + content: '\f107'; + color: #0088ce; + font-family: FontAwesome; + font-size: 10px; + font-weight: 400; + height: 9px; + margin-left: 5px; + line-height: 1.2; + position: absolute; + top: 7px; + vertical-align: baseline; + width: 12px; +} +.ngx-datatable.patternfly .datatable-header-cell.sortable.sort-active.sort-asc .datatable-header-cell-label:after { + content: '\f106'; +} +.ngx-datatable.patternfly .datatable-header .datatable-header-cell-label { + line-height: 24px; +} +.ngx-datatable.patternfly .datatable-header .datatable-header-cell-wrapper { + cursor: pointer; +} +.ngx-datatable.patternfly .datatable-body-row { + border-bottom: 1px solid #d1d1d1; + border-top: 0; + vertical-align: top; + /* stylelint-disable */ + /* stylelint-enable */ +} +.ngx-datatable.patternfly .datatable-body-row.datatable-row-even { + background-color: transparent !important; +} +.ngx-datatable.patternfly .datatable-body-row.datatable-row-even:hover, +.ngx-datatable.patternfly .datatable-body-row.datatable-row-even:active { + background-color: #def3ff !important; + border-bottom-color: #7dc3e8; +} +.ngx-datatable.patternfly .datatable-body-row.datatable-row-odd { + background-color: #f5f5f5 !important; +} +.ngx-datatable.patternfly .datatable-body-row.datatable-row-odd:hover, +.ngx-datatable.patternfly .datatable-body-row.datatable-row-odd:active { + background-color: #def3ff !important; + border-bottom-color: #7dc3e8; +} +.ngx-datatable.patternfly .datatable-body-row.active { + background-color: #0088ce !important; + border-bottom-color: #00659c !important; + color: #fff; +} +.ngx-datatable.patternfly .datatable-body-row.active:hover { + background-color: #0088ce !important; +} +.ngx-datatable.patternfly .datatable-body-row .datatable-body-cell { + padding: 2px 10px 3px; + text-align: left; + vertical-align: top; +} +.ngx-datatable.patternfly .datatable-body-row .datatable-body-cell.pfng-table-dnd-only { + padding-left: 0; + padding-right: 0; +} +.ngx-datatable.patternfly .datatable-body-cell { + border-right: 1px solid #d1d1d1; +} +.ngx-datatable.patternfly .datatable-body-cell:first-child { + border-left: 1px solid #d1d1d1; +} +.ngx-datatable.patternfly .datatable-body-cell-label .fa:hover { + cursor: pointer; +} +.ngx-datatable.patternfly .datatable-body .datatable-scroll, +.ngx-datatable.patternfly .datatable-body .datatable-row-wrapper { + /* stylelint-disable */ + width: 100% !important; + /* stylelint-enable */ +} +.ngx-datatable.patternfly .datatable-body .datatable-group-header { + background: #f5f5f5; + border-bottom: solid 1px #d1d1d1; + border-left: solid 1px #d1d1d1; + border-right: solid 1px #d1d1d1; + /* stylelint-disable */ + width: 100% !important; + /* stylelint-enable */ +} +.ngx-datatable.patternfly .datatable-body .empty-row { + border-bottom: 1px solid #d1d1d1; + border-left: 1px solid #d1d1d1; + border-right: 1px solid #d1d1d1; + margin-bottom: 5px; + padding: 2px 10px 1px; +} +.dropdown-kebab-pf.invisible { + opacity: 0; + pointer-events: none; +} +.toolbar-pf-actions .btn { + min-width: unset; +} +.toolbar-pf-actions .toolbar-pf-view-selector a { + cursor: pointer; +} +.toolbar-pf-actions .dropdown-menu a { + cursor: pointer; +} +.toolbar-pf-actions .dropdown-kebab-pf { + float: right; +} +.toolbar-pf-actions .toolbar-apf-filter { + /* stylelint-disable */ + padding-left: 0 !important; + /* stylelint-enable */ +} +@media (min-width: 768px) { + .toolbar-pf-actions .toolbar-apf-filter { + padding-left: 0; + } +} +.toolbar-pf-include-actions { + display: inline-block; + margin: 0 5px; +} +.toolbar-pf-actions.no-filter-results { + margin-bottom: 10px; +} +.pfng-wizard-cancel-inline { + margin-left: 25px; +} +.pfng-wizard-footer-inline { + text-align: left; +} +.pfng-wizard-main { + margin-left: 0; +} +.pfng-wizard-position-override { + position: relative; +} +.wizard-pf-footer .pfng-wizard-previous-btn.pfng-wizard-btn-no-back { + display: none; +} +.wizard-pf-steps-indicator li a.disabled { + cursor: default; +} +.wizard-pf-steps-indicator li a.disabled:hover .wizard-pf-step-number { + background-color: #fff; + border-color: #bbb; + color: #bbb; +} +.pfng-wizard-single-step { + margin-left: 0; +} +.wizard-pf-row { + height: inherit; +} + +/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL25vZGVfbW9kdWxlcy9wYXR0ZXJuZmx5L2Rpc3QvbGVzcy9jb2xvci12YXJpYWJsZXMubGVzcyIsIm1peGlucy5sZXNzIiwiLi4vLi4vYXBwL2NhcmQvYmFzaWMtY2FyZC9jYXJkLmNvbXBvbmVudC5sZXNzIiwiLi4vLi4vYXBwL2NhcmQvaW5mby1zdGF0dXMtY2FyZC9pbmZvLXN0YXR1cy1jYXJkLmNvbXBvbmVudC5sZXNzIiwiLi4vLi4vYXBwL2NvcHkvYmxvY2stY29weS9ibG9jay1jb3B5LmNvbXBvbmVudC5sZXNzIiwiLi4vLi4vYXBwL2NvcHkvaW5saW5lLWNvcHkvaW5saW5lLWNvcHkuY29tcG9uZW50Lmxlc3MiLCIuLi8uLi9hcHAvZW1wdHktc3RhdGUvZW1wdHktc3RhdGUuY29tcG9uZW50Lmxlc3MiLCIuLi8uLi9hcHAvZmlsdGVyL2ZpbHRlci5jb21wb25lbnQubGVzcyIsIi4uLy4uL2FwcC9maWx0ZXIvZmlsdGVyLWZpZWxkcy5jb21wb25lbnQubGVzcyIsIi4uLy4uL2FwcC9maWx0ZXIvZmlsdGVyLXJlc3VsdHMuY29tcG9uZW50Lmxlc3MiLCIuLi8uLi9hcHAvbGlzdC9iYXNpYy1saXN0L2xpc3QuY29tcG9uZW50Lmxlc3MiLCIuLi8uLi9hcHAvbmF2aWdhdGlvbi92ZXJ0aWNhbC1uYXZpZ2F0aW9uL3ZlcnRpY2FsLW5hdmlnYXRpb24uY29tcG9uZW50Lmxlc3MiLCIuLi8uLi9hcHAvbm90aWZpY2F0aW9uL25vdGlmaWNhdGlvbi1kcmF3ZXIvbm90aWZpY2F0aW9uLWRyYXdlci5jb21wb25lbnQubGVzcyIsIi4uLy4uL2FwcC9ub3RpZmljYXRpb24vdG9hc3Qtbm90aWZpY2F0aW9uL3RvYXN0LW5vdGlmaWNhdGlvbi5jb21wb25lbnQubGVzcyIsIi4uLy4uL2FwcC9zb3J0L3NvcnQuY29tcG9uZW50Lmxlc3MiLCIuLi8uLi9hcHAvdGFibGUvYmFzaWMtdGFibGUvdGFibGUuY29tcG9uZW50Lmxlc3MiLCIuLi8uLi9hcHAvdG9vbGJhci90b29sYmFyLmNvbXBvbmVudC5sZXNzIiwiLi4vLi4vYXBwL3dpemFyZC93aXphcmQuY29tcG9uZW50Lmxlc3MiLCIuLi8uLi9hcHAvd2l6YXJkL3dpemFyZC1zdGVwLmNvbXBvbmVudC5sZXNzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQztFQ3NCRyxZQUFBOztBRHRCSDtFQ3lCRyxnQkFBQTs7QUR6Qkg7RUM0QkcsaUJBQUE7O0FENUJIO0VDK0JHLG1CQUFBOztBRC9CSDtFQ2tDRyxrQkFBQTs7QURsQ0g7RUNzQkcsWUFBQTs7QUR0Qkg7RUN5QkcsZ0JBQUE7O0FEekJIO0VDNEJHLGlCQUFBOztBRDVCSDtFQytCRyxtQkFBQTs7QUQvQkg7RUNrQ0csa0JBQUE7O0FEbENIO0VDc0JHLFlBQUE7O0FEdEJIO0VDeUJHLGdCQUFBOztBRHpCSDtFQzRCRyxpQkFBQTs7QUQ1Qkg7RUMrQkcsbUJBQUE7O0FEL0JIO0VDa0NHLGtCQUFBOztBRGxDSDtFQ3NCRyxZQUFBOztBRHRCSDtFQ3lCRyxnQkFBQTs7QUR6Qkg7RUM0QkcsaUJBQUE7O0FENUJIO0VDK0JHLG1CQUFBOztBRC9CSDtFQ2tDRyxrQkFBQTs7QURsQ0g7RUNzQkcsWUFBQTs7QUR0Qkg7RUN5QkcsZ0JBQUE7O0FEekJIO0VDNEJHLGlCQUFBOztBRDVCSDtFQytCRyxtQkFBQTs7QUQvQkg7RUNrQ0csa0JBQUE7O0FEbENIO0VDc0JHLFlBQUE7O0FEdEJIO0VDeUJHLGdCQUFBOztBRHpCSDtFQzRCRyxpQkFBQTs7QUQ1Qkg7RUMrQkcsbUJBQUE7O0FEL0JIO0VDa0NHLGtCQUFBOztBRGxDSDtFQ3NCRyxZQUFBOztBRHRCSDtFQ3lCRyxnQkFBQTs7QUR6Qkg7RUM0QkcsaUJBQUE7O0FENUJIO0VDK0JHLG1CQUFBOztBRC9CSDtFQ2tDRyxrQkFBQTs7QURsQ0g7RUNzQkcsWUFBQTs7QUR0Qkg7RUN5QkcsZ0JBQUE7O0FEekJIO0VDNEJHLGlCQUFBOztBRDVCSDtFQytCRyxtQkFBQTs7QUQvQkg7RUNrQ0csa0JBQUE7O0FEbENIO0VDc0JHLFlBQUE7O0FEdEJIO0VDeUJHLGdCQUFBOztBRHpCSDtFQzRCRyxpQkFBQTs7QUQ1Qkg7RUMrQkcsbUJBQUE7O0FEL0JIO0VDa0NHLGtCQUFBOztBRGxDSDtFQ3NCRyxZQUFBOztBRHRCSDtFQ3lCRyxnQkFBQTs7QUR6Qkg7RUM0QkcsaUJBQUE7O0FENUJIO0VDK0JHLG1CQUFBOztBRC9CSDtFQ2tDRyxrQkFBQTs7QURsQ0g7RUNzQkcsWUFBQTs7QUR0Qkg7RUN5QkcsZ0JBQUE7O0FEekJIO0VDNEJHLGlCQUFBOztBRDVCSDtFQytCRyxtQkFBQTs7QUQvQkg7RUNrQ0csa0JBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDc0JHLFdBQUE7O0FEdEJIO0VDeUJHLGVBQUE7O0FEekJIO0VDNEJHLGdCQUFBOztBRDVCSDtFQytCRyxrQkFBQTs7QUQvQkg7RUNrQ0csaUJBQUE7O0FEbENIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csYUFBQTs7QURISDtFQ01HLGlCQUFBOztBRE5IO0VDU0csa0JBQUE7O0FEVEg7RUNZRyxvQkFBQTs7QURaSDtFQ2VHLG1CQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOztBRGZIO0VDR0csWUFBQTs7QURISDtFQ01HLGdCQUFBOztBRE5IO0VDU0csaUJBQUE7O0FEVEg7RUNZRyxtQkFBQTs7QURaSDtFQ2VHLGtCQUFBOzs7QUNmSixVQUNFO0VBQ0UsZ0JBQUE7O0FBR0EsVUFERCxxQkFDRTtFQUNDLGVBQUE7RUFDQSxnQkFBQTs7QUFISixVQUFDLHFCQUtDO0VBQ0UsYUFBQTtFQUNBLGlCQUFBOztBQVBKLFVBQUMscUJBU0M7RUFDRSxnQkFBQTtFQUNBLGNBQUE7RUFDQSxlQUFBOztBQUtOO0VBQ0UsbUJBQUE7RUFDQSxlQUFBOztBQ3ZCRjtFQUNFLGFBQUE7RUFDQSxjQUFBOztBQUZGLHNCQUdFO0VBQ0UsYUFBQTtFQUNBLG1CQUFBO0VBQ0EsdUJBQUE7RUFDQSxzQkFBQTtFQUNBLGtCQUFBOztBQVJKLHNCQUdFLHNCQU1FO0VBQ0UsZUFBQTs7QUFWTixzQkFHRSxzQkFTRTtFQUNFLGdCQUFBOztBQWJOLHNCQWdCRTtFQUNFLGNBQUE7O0FBakJKLHNCQWdCRSx3QkFFRTtFQUNFLGdCQUFBO0VBQ0EsbUJBQUE7O0FDcEJOO0VBQ0UsZUFBQTs7QUFDQSxnQkFBQztFQUNDLGdCQUFBO0VBQ0Esc0JBQUE7O0FBRUYsZ0JBQUM7RUFDQyxhQUFBOztBQUNBLGdCQUZELFFBRUU7RUFDQyw2QkFBQTs7QUFFRixnQkFMRCxRQUtFO0VBQ0MsWUFBQTtFQUNBLDRCQUFBO0VBQ0EsVUFBQTs7QUFFRixnQkFWRCxRQVVFO0VBQ0MsZUFBQTtFQUNBLGtCQUFBOztBQUNBLGdCQWJILFFBVUUsS0FHRTtFQUNDLGtCQUFBOztBQUdKLGdCQWpCRCxRQWlCRTtFQUNDLGFBQUE7RUFDQSxtQkFBQTtFQUNBLFlBQUE7RUFDQSxnQkFBQTtFQUNBLG1CQUFBOztBQUVGLGdCQXhCRCxRQXdCRTtFQUNDLGdCQUFBO0VBQ0EsdUJBQUE7RUFDQSxpQkFBQTtFQUNBLG1CQUFBO0VBQ0EsWUFBQTtFQUNBLFdBQUE7O0FBR0osZ0JBQUM7RUFDQyxZQUFBO0VBQ0EsMkJBQUE7RUFDQSxnQkFBQTtFQUNBLGNBQUE7O0FBQ0EsZ0JBTEQsSUFLRTtFQUNDLGlEQUFBOztBQUdKLGdCQUFDO0VBQ0MsYUFBQTtFQUNBLHFCQUFBOztBQ2xESjtFQUNFLG9CQUFBO0VBQ0Esc0JBQUE7RUFDQSxzQkFBQTtFQUNBLGtCQUFBO0VBQ0EsbUJBQUE7RUFDQSxvQkFBQTtFQUNBLGVBQUE7O0FBQ0EsaUJBQUM7RUFDQyxXQUFBOztBQUVGLGlCQUFDO0VBQ0MsWUFBQTtFQUNBLGdCQUFBO0VBQ0EsbUJBQUE7RUFDQSx1QkFBQTtFQUNBLGdCQUFBO0VBQ0EsWUFBQTtFQUNBLFdBQUE7O0FBRUYsaUJBQUM7RUFDQyxZQUFBO0VBQ0EsMkJBQUE7RUFDQSxnQkFBQTtFQUNBLGtCQUFrQiw0Q0FBbEI7O0FBQ0EsaUJBTEQsSUFLRTtBQUNELGlCQU5ELElBTUU7QUFDRCxpQkFQRCxJQU9FO0VBQ0MseUJBQUE7O0FDNUJOO0VBQ0UsZ0JBQUE7O0FBREYsZUFFRTtFQUNFLGlCQUFBOztBQ0hKLFVBQ0U7RUFBSSxlQUFBOzs7QUFHTixjQUNFO0VBQ0UsY0FBQTs7QUFHSjtFQUFpQixnQkFBQTs7QUNUakIsVUFDRTtFQUFJLGVBQUE7O0FBQ0osVUFBQyxjQUNDO0VBQ0UsZUFBQTtFQUNBLFlBQUE7O0FBSEosVUFBQyxjQUtDO0VBQ0UsbUJBQUE7O0FBTkosVUFBQyxjQVFDO0VBT0Usa0JBQUE7RUFDQSxnQkFBQTs7QUFQQSxVQVRILGNBUUMsMkJBQ0c7RUFDQyxXQUFBOztBQURGLFVBVEgsY0FRQywyQkFDRyxTQUVDO0VBQ0UsUUFBQTs7QUFaUixVQUFDLGNBUUMsMkJBU0U7RUFDRSxjQUFBO0VBQ0Esa0JBQUE7RUFDQSxrQkFBQTtFQUNBLFNBQUE7RUFDQSxXQUFBO0VBQ0EsVUFBQTs7QUFNUixjQUNFO0VBQ0Usc0JBQUE7RUFDQSxzQkFBQTs7QUFISixjQUNFLGFBR0U7RUFDRSxjQUFBO0VBQ0Esa0JBQUE7RUFDQSxnQkFBQTs7QUFQTixjQVVFO0VBQ0UsWUFBQTtFQUNBLGlCQUFBOzs7QUFLSixZQUNFLGlCQUNFLGVBQWMsWUFBVTtFQUN0Qix5QkFBQTtFQUNBLHFCQUFBO0VBQ0EsV0FBQTs7O0FBTU47RUFDRSxlQUFBO0VBQ0EsVUFBQTtFQUNBLGdCQUFBOztBQUhGLG1CQUlFO0VBQ0UsY0FBQTtFQUNBLFlBQUE7O0FBQ0EsbUJBSEYsRUFHRztFQUNDLFVBQUE7O0FBR0osY0FBYyxNQUFPO0VBQ25CLFVBQUE7RUFDQSx5QkFBQTtFQUNBLDRCQUFBO0VBQ0EsbUNBQUE7O0FBSUo7RUFDRSxlQUFBO0VBQ0EsbUJBQUE7RUFDQSxrQkFBQTtFQUNBLGdCQUFBOztBQUpGLDJCQUtFO0VBQ0UsV0FBQTtFQUNBLFlBQUE7O0FBUEosMkJBS0UsRUFHRSxJQUFHO0VBQ0QsV0FBQTs7QUFFRiwyQkFORixFQU1HO0VBQ0MsVUFBQTs7QUFLTjtFQUNFLHlCQUFBO0VBQ0EsWUFBQTtFQUNBLGtCQUFBO0VBQ0EsdUJBQUE7RUFDQSxXQUFBO0VBQ0EsYUFBQTs7QUFDQSx5QkFBQztFQUNDLFFBQUE7O0FBREYseUJBQUMsU0FFQztFQUNFLFlBQUE7RUFDQSxzQkFBQTtFQUNBLHlCQUFBO0VBQ0EsNEJBQUE7RUFDQSxtQ0FBQTs7QUFLTjtFQUNFLFdBQUE7RUFDQSxrQkFBQTtFQUNBLGdCQUFBO0VBQ0Esa0JBQUE7O0FBR0Y7RUFDRSxrQkFBQTtFQUNBLGdCQUFBOztBQUdGO0VBQ0UsaUJBQUE7O0FDL0hGLFVBQ0U7RUFBSSxlQUFBOztBQUROLFVBRUU7RUFBZSxlQUFBOztBQUdqQjtFQUNFLGVBQUE7RUFDQSxnQkFBQTs7QUFHRixpQkFDRTtFQUNFLGVBQUE7O0FBQ0EsUUFBbUM7RUFBbkMsaUJBRkY7SUFFdUMsV0FBQTs7O0FBQ3JDLFFBQW1DO0VBQW5DLGlCQUhGO0lBR3VDLFdBQUE7OztBQUp6QyxpQkFNRTtFQUNFLGtCQUFBOztBQUlKO0VBQ0UsNkJBQUE7RUFDQSxrQkFBQTtFQUNBLG1CQUFBO0VBQ0EsZ0JBQUE7O0FBR0Y7RUFDRSxZQUFBO0VBQ0EsbUJBQUE7RUFDQSxnQkFBQTs7QUM5QkY7RUFDRSxXQUFBOztBQUlGO0VBQ0UsYUFBQTtFQUNBLFlBQUE7O0FBSUY7RUFDRSxVQUFBOztBQUdGLGlCQUNFO0VBQ0UsaUJBQUE7O0FBQ0EsaUJBRkYsZ0JBRUc7RUFDQyxlQUFBOztBQUtOLGtCQUVFO0VBQ0UsZUFBQTs7QUFDQSxrQkFGRixpQkFFRztFQUNDLGNBQUE7O0FBTU47RUFDRSxrQkFBQTs7QUFERixvQkFFRTtFQUNFLFlBQUE7O0FBS0o7RUFNRSxvQkFBQTs7QUFKQSxRQUEwQjtFQUExQjtJQUNFLGFBQUE7OztBQU1GLGtCQUFDO0VBQ0Msc0JBQUE7O0FBVkosa0JBY0U7RUFDRSxvQkFBQTs7QUFJRixrQkFBQztFQUNDLGdCQUFBOztBQXBCSixrQkF3QkU7RUFDRSxrQkFBQTtFQUNBLG1CQUFBOztBQTFCSixrQkE4QkUsaUJBQ0U7QUEvQkosa0JBOEJvQixnQkFDaEI7RUFDRSxpQkFBQTs7QUFNTjtFQUNFLG1CQUFBO0VBQ0EsbUJBQUE7RUFDQSx5QkFBQTtFQUNBLHdDQUFBO0VBQ0EsYUFBQTtFQUNBLG9CQUFBO0VBQ0Esa0JBQUE7RUFDQSxrQkFBQTtFQUNBLGlCQUFBO0VBQ0Esb0JBQUE7RUFDQSxpQkFBQTtFQUNBLGtCQUFBO0VBQ0EsaUJBQUE7O0FBQ0EsY0FBQztFQUNDLGtCQUFBOztBQWZKLGNBaUJFO0VBQ0UsY0FBQTtFQUNBLFlBQUE7O0FBQ0EsY0FIRixFQUdHO0VBQ0MsVUFBQTs7QUFNTjtFQUNFLG1CQUFBO0VBQ0EsYUFBQTs7QUFJRjtFQUNFLGtCQUFBO0VBQ0EsV0FBQTs7QUFDQSwwQkFBQztFQUNDLFdBQUE7O0FDdkhKLHVCQUNFO0VBQ0UsUUFBQTs7QUNGSjtFQUNFLGtCQUFBO0VBQ0EsYUFBQTtFQUNBLHNCQUFBOztBQUhGLFVBSUU7RUFDRSxrQkFBQTs7QUFMSixVQU9FO0VBQ0UsYUFBQTtFQUNBLHNCQUFBO0VBQ0EsaUJBQUE7RUFDQSxlQUFBO0VBQ0EsWUFBQTtFQUNBLGdCQUFBOztBQWJKLFVBT0UsYUFPRSxPQUFNLGNBQWM7RUFDbEIsaUJBQUE7RUFDQSxjQUFBO0VBQ0EsYUFBQTtFQUNBLHNCQUFBOztBQWxCTixVQU9FLGFBYUUsZ0JBQWU7RUFDYixhQUFBO0VBQ0Esc0JBQUE7RUFDQSxjQUFBO0VBQ0EsYUFBQTs7QUF4Qk4sVUFPRSxhQWFFLGdCQUFlLEdBS2I7RUFDRSxjQUFBO0VBQ0EsZ0JBQUE7O0FBM0JSLFVBK0JFO0VBQ0UsVUFBQTs7QUNoQ0osZ0JBQWlCO0VBQU0sZUFBQTs7QUFFdkIsU0FDRSxlQUFlLEtBQUs7RUFBTSxlQUFBOztBQ0g1QixRQUNFO0VBQ0UsaUJBQUE7RUFDQSxjQUFBO0VBQ0EsWUFBQTtFQUNBLGNBQUE7RUFDQSxlQUFBO0VBQ0EsY0FBQTs7QUFDQSxRQVBGLFVBT0c7RUFBUyxjQUFBOztBQ1BaLEtBQUMsTUFDQztFQUNFLGdDQUFBO0VBQ0EsOEJBQUE7RUFDQSwrQkFBQTtFQUNBLDZCQUFBOztBQUxKLEtBQUMsTUFPQztFQUNFLGdCQUFBOztBQVJKLEtBQUMsTUFVQyxLQUFJO0VBQ0YsY0FBQTtFQUNBLGVBQUE7RUFDQSx5QkFBQTtFQUNBLHlCQUFBO0VBQ0EsbUJBQUE7O0FBZkosS0FBQyxNQWlCQztFQUNFLG9CQUFBOztBQUVGLEtBcEJELE1Bb0JFO0VBQ0MsbUJBQUE7RUFDQSxhQUFBOztBQUdBLEtBekJILE1Bd0JFLFdBQ0U7RUFDQyxrQkFBa0IsZ0RBQWxCO0VBQ0EseUJBQUE7RUFDQSwyQkFBQTtFQUNBLHdCQUFBO0VBQ0EseUJBQUE7RUFDQSxxQkFBQTtFQUNBLFNBQVMsRUFBVDtFQUNBLFlBQUE7RUFDQSxPQUFBO0VBQ0Esa0JBQUE7RUFDQSxNQUFBO0VBQ0EsV0FBQTs7QUFFRixLQXZDSCxNQXdCRSxXQWVFO0VBQ0MsWUFBQTs7QUFHSixLQTNDRCxNQTJDRTtFQUNDLHVCQUFBO0VBQ0EsZ0NBQUE7RUFDQSw4QkFBQTtFQUNBLCtCQUFBO0VBQ0EsYUFBQTtFQUNBLGFBQUE7O0FBRUYsS0FuREQsTUFtREU7RUFDQyxZQUFBOztBQUVGLEtBdERELE1Bc0RFLE9BQ0M7RUFDRSxhQUFBOztBQU1GLEtBRkgsTUFDQyxLQUNHO0VBQ0MseUJBQUE7RUFDQSxtQkFBQTtFQUNBLDhCQUFBO0VBQ0EsK0JBQUE7RUFDQSw2QkFBQTtFQUNBLGdCQUFBO0VBQ0EsY0FBQTtFQUNBLGVBQUE7O0FBVk4sS0FBQyxNQWFDO0VBQ0Usb0JBQUE7Ozs7O0FBT047Ozs7O0FBQUEsY0FJRTs7RUFFRSwwQkFBQTtFQUNBLG9CQUFBO0VBQ0Esd0JBQUE7O0VBRUEsWUFBQTs7OztBQVZKLGNBY0U7O0VBRUUsd0JBQUE7OztBQWhCSixjQW1CRTs7RUFFRSxvQ0FBQTtFQUNBLGlDQUFBO0VBQ0EsZ0NBQUE7RUFDQSw0QkFBQTs7O0FBeEJKLGNBMkJFO0VBQ0Usb0JBQUE7RUFDQSxXQUFBO0VBQ0EseUJBQUE7RUFDQSxZQUFBOzs7O0FBSUYsY0FBQztFQUNDLG1CQUFBO0VBQ0EsNkJBQUE7RUFDQSxnQkFBQTs7QUFIRixjQUFDLFdBSUM7O0VBRUUsa0JBQWtCLHFEQUFsQjs7RUFFQSxrQkFBa0IsbURBQWxCO0VBQ0EsMkJBQUE7RUFDQSx5QkFBQTs7RUFFQSx3QkFBQTs7O0FBRUEsY0FkSCxXQUlDLGtCQVVHO0VBQ0MsK0JBQUE7RUFDQSxnQ0FBQTtFQUNBLGdCQUFBO0VBQ0EscUJBQUE7RUFDQSxzQkFBQTs7QUFDQSxjQXBCTCxXQUlDLGtCQVVHLEtBTUU7RUFDQyw4QkFBQTs7QUFFRixjQXZCTCxXQUlDLGtCQVVHLEtBU0U7RUFDQyxnQkFBQTtFQUNBLGtCQUFBOztBQUVGLGNBM0JMLFdBSUMsa0JBVUcsS0FhRTtFQUNDLGVBQUE7RUFDQSxnQkFBQTs7QUFFRixjQS9CTCxXQUlDLGtCQVVHLEtBaUJFLFNBQVM7RUFDUixjQUFBOztBQUNBLGNBakNQLFdBSUMsa0JBVUcsS0FpQkUsU0FBUyxZQUVQO0FBQVcsY0FqQ25CLFdBSUMsa0JBVUcsS0FpQkUsU0FBUyxZQUVLOztFQUVYLGNBQUE7O0VBRUEsa0JBQUE7O0VBRUEsaUNBQUE7O0VBRUEsZ0JBQUE7O0FBQ0EsY0ExQ1QsV0FJQyxrQkFVRyxLQWlCRSxTQUFTLFlBRVAsU0FTRTtBQUFELGNBMUNULFdBSUMsa0JBVUcsS0FpQkUsU0FBUyxZQUVLLFVBU1Y7RUFDQyxtQkFBQTtFQUNBLFNBQVMsRUFBVDtFQUNBLFdBQUE7RUFDQSxrQkFBQTtFQUNBLE9BQUE7RUFDQSxNQUFBO0VBQ0EsV0FBQTs7QUFHQSxjQXBEWCxXQUlDLGtCQVVHLEtBaUJFLFNBQVMsWUFFUCxTQWtCQyw2QkFDRztBQUFELGNBcERYLFdBSUMsa0JBVUcsS0FpQkUsU0FBUyxZQUVLLFVBa0JYLDZCQUNHO0VBQ0MsU0FBUyxPQUFUO0VBQ0EsY0FBQTtFQUNBLHdCQUFBO0VBQ0EsZUFBQTtFQUNBLGdCQUFBO0VBQ0EsV0FBQTtFQUNBLGdCQUFBO0VBQ0EsZ0JBQUE7RUFDQSxrQkFBQTtFQUNBLFFBQUE7RUFDQSx3QkFBQTtFQUNBLFdBQUE7O0FBTUYsY0F0RVgsV0FJQyxrQkFVRyxLQWlCRSxTQUFTLFlBcUNQLFNBQ0MsNkJBQ0c7RUFDQyxTQUFTLE9BQVQ7O0FBdkVkLGNBQUMsV0FJQyxrQkF5RUU7RUFDRSxpQkFBQTs7QUE5RU4sY0FBQyxXQUlDLGtCQTRFRTtFQUNFLGVBQUE7O0FBSUYsY0FyRkgsV0FvRkMsZ0JBQ0c7RUFDQyxnQ0FBQTtFQUNBLGFBQUE7RUFDQSxtQkFBQTs7OztBQUVBLGNBMUZMLFdBb0ZDLGdCQUNHLElBS0U7RUFDQyx3Q0FBQTs7QUFDQSxjQTVGUCxXQW9GQyxnQkFDRyxJQUtFLG1CQUVFO0FBQVEsY0E1RmhCLFdBb0ZDLGdCQUNHLElBS0UsbUJBRVc7RUFDUix5QkFBQTtFQUNBLDRCQUFBOztBQUdKLGNBakdMLFdBb0ZDLGdCQUNHLElBWUU7RUFDQyx5QkFBQTs7QUFDQSxjQW5HUCxXQW9GQyxnQkFDRyxJQVlFLGtCQUVFO0FBQVEsY0FuR2hCLFdBb0ZDLGdCQUNHLElBWUUsa0JBRVc7RUFDUix5QkFBQTtFQUNBLDRCQUFBOztBQUdKLGNBeEdMLFdBb0ZDLGdCQUNHLElBbUJFO0VBQ0MseUJBQUE7RUFDQSw0QkFBQTtFQUNBLFdBQUE7O0FBQ0EsY0E1R1AsV0FvRkMsZ0JBQ0csSUFtQkUsT0FJRTtFQUNDLHlCQUFBOztBQXhCTixjQXJGSCxXQW9GQyxnQkFDRyxJQTRCQztFQUNFLHFCQUFBO0VBQ0EsZ0JBQUE7RUFDQSxtQkFBQTs7QUFDQSxjQXJIUCxXQW9GQyxnQkFDRyxJQTRCQyxxQkFJRztFQUNDLGVBQUE7RUFDQSxnQkFBQTs7QUFJTixjQTNISCxXQW9GQyxnQkF1Q0c7RUFDQywrQkFBQTs7QUFDQSxjQTdITCxXQW9GQyxnQkF1Q0csS0FFRTtFQUNDLDhCQUFBOztBQUlFLGNBbElULFdBb0ZDLGdCQXVDRyxLQUtFLE1BQ0MsSUFDRztFQUNDLGVBQUE7O0FBbklaLGNBQUMsV0FvRkMsZ0JBb0RFO0FBeElKLGNBQUMsV0FvRkMsZ0JBcURFOztFQUVFLHNCQUFBOzs7QUEzSU4sY0FBQyxXQW9GQyxnQkEwREU7RUFDRSxtQkFBQTtFQUNBLGdDQUFBO0VBQ0EsOEJBQUE7RUFDQSwrQkFBQTs7RUFFQSxzQkFBQTs7O0FBcEpOLGNBQUMsV0FvRkMsZ0JBbUVFO0VBQ0UsZ0NBQUE7RUFDQSw4QkFBQTtFQUNBLCtCQUFBO0VBQ0Esa0JBQUE7RUFDQSxxQkFBQTs7QUNqUlIsa0JBQWtCO0VBQ2hCLFVBQUE7RUFDQSxvQkFBQTs7QUFHRixtQkFDRTtFQUFPLGdCQUFBOztBQURULG1CQUVFLDBCQUNFO0VBQUksZUFBQTs7QUFIUixtQkFLRSxlQUNFO0VBQUksZUFBQTs7QUFOUixtQkFRRTtFQUFxQixZQUFBOztBQVJ2QixtQkFTRTs7RUFFRSwwQkFBQTs7O0FBRUEsUUFBMEI7RUFBMUIsbUJBSkY7SUFLSSxlQUFBOzs7QUFLTjtFQUNFLHFCQUFBO0VBQ0EsYUFBQTs7QUFHRixtQkFBbUI7RUFBcUIsbUJBQUE7O0FDN0J4QztFQUNFLGlCQUFBOztBQUVGO0VBQ0UsZ0JBQUE7O0FBRUY7RUFDRSxjQUFBOztBQUVGO0VBQ0Usa0JBQUE7O0FBSUUsaUJBREYsMEJBQ0c7RUFDQyxhQUFBOztBQUlOLDBCQUEyQixHQUFHLEVBQUM7RUFDN0IsZUFBQTs7QUFDQSwwQkFGeUIsR0FBRyxFQUFDLFNBRTVCLE1BQ0M7RUFDRSxzQkFBQTtFQUNBLGtCQUFBO0VBQ0EsV0FBQTs7QUN6Qk47RUFDRSxjQUFBOztBQUdGO0VBQ0UsZUFBQSIsInNvdXJjZXNDb250ZW50IjpbIkBjb2xvci1wZi1ibGFjay0xMDA6ICAgICAgICAgICAgICNmYWZhZmE7XG5AY29sb3ItcGYtYmxhY2stMTUwOiAgICAgICAgICAgICAjZjVmNWY1O1xuQGNvbG9yLXBmLWJsYWNrLTIwMDogICAgICAgICAgICAgI2VkZWRlZDtcbkBjb2xvci1wZi1ibGFjay0zMDA6ICAgICAgICAgICAgICNkMWQxZDE7XG5AY29sb3ItcGYtYmxhY2stNDAwOiAgICAgICAgICAgICAjYmJiO1xuQGNvbG9yLXBmLWJsYWNrLTUwMDogICAgICAgICAgICAgIzhiOGQ4ZjtcbkBjb2xvci1wZi1ibGFjay02MDA6ICAgICAgICAgICAgICM3Mjc2N2I7XG5AY29sb3ItcGYtYmxhY2stNzAwOiAgICAgICAgICAgICAjNGQ1MjU4O1xuQGNvbG9yLXBmLWJsYWNrLTgwMDogICAgICAgICAgICAgIzM5M2Y0NDtcbkBjb2xvci1wZi1ibGFjay05MDA6ICAgICAgICAgICAgICMyOTJlMzQ7XG5AY29sb3ItcGYtYmx1ZS0yNTogICAgICAgICAgICAgICAjZWRmOGZmO1xuQGNvbG9yLXBmLWJsdWUtNTA6ICAgICAgICAgICAgICAgI2RlZjNmZjtcbkBjb2xvci1wZi1ibHVlLTEwMDogICAgICAgICAgICAgICNiZWUxZjQ7XG5AY29sb3ItcGYtYmx1ZS0yMDA6ICAgICAgICAgICAgICAjN2RjM2U4O1xuQGNvbG9yLXBmLWJsdWUtMzAwOiAgICAgICAgICAgICAgIzM5YTVkYztcbkBjb2xvci1wZi1ibHVlLTQwMDogICAgICAgICAgICAgICMwMDg4Y2U7XG5AY29sb3ItcGYtYmx1ZS01MDA6ICAgICAgICAgICAgICAjMDA2NTljO1xuQGNvbG9yLXBmLWJsdWUtNjAwOiAgICAgICAgICAgICAgIzAwNDM2ODtcbkBjb2xvci1wZi1ibHVlLTcwMDogICAgICAgICAgICAgICMwMDIyMzU7XG5AY29sb3ItcGYtY3lhbi0xMDA6ICAgICAgICAgICAgICAjYmVkZWUxO1xuQGNvbG9yLXBmLWN5YW4tMjAwOiAgICAgICAgICAgICAgIzdkYmRjMztcbkBjb2xvci1wZi1jeWFuLTMwMDogICAgICAgICAgICAgICMzYTljYTY7XG5AY29sb3ItcGYtY3lhbi00MDA6ICAgICAgICAgICAgICAjMDA3YTg3O1xuQGNvbG9yLXBmLWN5YW4tNTAwOiAgICAgICAgICAgICAgIzAwNWM2NjtcbkBjb2xvci1wZi1jeWFuLTYwMDogICAgICAgICAgICAgICMwMDNkNDQ7XG5AY29sb3ItcGYtY3lhbi03MDA6ICAgICAgICAgICAgICAjMDAxZjIyO1xuQGNvbG9yLXBmLWdvbGQtMTAwOiAgICAgICAgICAgICAgI2ZiZWFiYztcbkBjb2xvci1wZi1nb2xkLTIwMDogICAgICAgICAgICAgICNmOWQ2N2E7XG5AY29sb3ItcGYtZ29sZC0zMDA6ICAgICAgICAgICAgICAjZjVjMTJlO1xuQGNvbG9yLXBmLWdvbGQtNDAwOiAgICAgICAgICAgICAgI2YwYWIwMDtcbkBjb2xvci1wZi1nb2xkLTUwMDogICAgICAgICAgICAgICNiNTgxMDA7XG5AY29sb3ItcGYtZ29sZC02MDA6ICAgICAgICAgICAgICAjNzk1NjAwO1xuQGNvbG9yLXBmLWdvbGQtNzAwOiAgICAgICAgICAgICAgIzNkMmMwMDtcbkBjb2xvci1wZi1ncmVlbi0xMDA6ICAgICAgICAgICAgICNjZmU3Y2Q7XG5AY29sb3ItcGYtZ3JlZW4tMjAwOiAgICAgICAgICAgICAjOWVjZjk5O1xuQGNvbG9yLXBmLWdyZWVuLTMwMDogICAgICAgICAgICAgIzZlYzY2NDtcbkBjb2xvci1wZi1ncmVlbi00MDA6ICAgICAgICAgICAgICMzZjljMzU7XG5AY29sb3ItcGYtZ3JlZW4tNTAwOiAgICAgICAgICAgICAjMmQ3NjIzO1xuQGNvbG9yLXBmLWdyZWVuLTYwMDogICAgICAgICAgICAgIzFlNGYxODtcbkBjb2xvci1wZi1ncmVlbi03MDA6ICAgICAgICAgICAgICMwZjI4MGQ7XG5AY29sb3ItcGYtbGlnaHQtYmx1ZS0xMDA6ICAgICAgICAjYmVlZGY5O1xuQGNvbG9yLXBmLWxpZ2h0LWJsdWUtMjAwOiAgICAgICAgIzdjZGJmMztcbkBjb2xvci1wZi1saWdodC1ibHVlLTMwMDogICAgICAgICMzNWNhZWQ7XG5AY29sb3ItcGYtbGlnaHQtYmx1ZS00MDA6ICAgICAgICAjMDBiOWU0O1xuQGNvbG9yLXBmLWxpZ2h0LWJsdWUtNTAwOiAgICAgICAgIzAwOGJhZDtcbkBjb2xvci1wZi1saWdodC1ibHVlLTYwMDogICAgICAgICMwMDVjNzM7XG5AY29sb3ItcGYtbGlnaHQtYmx1ZS03MDA6ICAgICAgICAjMDAyZDM5O1xuQGNvbG9yLXBmLWxpZ2h0LWdyZWVuLTEwMDogICAgICAgI2U0ZjViYztcbkBjb2xvci1wZi1saWdodC1ncmVlbi0yMDA6ICAgICAgICNjOGViNzk7XG5AY29sb3ItcGYtbGlnaHQtZ3JlZW4tMzAwOiAgICAgICAjYWNlMTJlO1xuQGNvbG9yLXBmLWxpZ2h0LWdyZWVuLTQwMDogICAgICAgIzkyZDQwMDtcbkBjb2xvci1wZi1saWdodC1ncmVlbi01MDA6ICAgICAgICM2Y2ExMDA7XG5AY29sb3ItcGYtbGlnaHQtZ3JlZW4tNjAwOiAgICAgICAjNDg2YjAwO1xuQGNvbG9yLXBmLWxpZ2h0LWdyZWVuLTcwMDogICAgICAgIzI1MzYwMDtcbkBjb2xvci1wZi1vcmFuZ2UtMTAwOiAgICAgICAgICAgICNmYmRlYmY7XG5AY29sb3ItcGYtb3JhbmdlLTIwMDogICAgICAgICAgICAjZjdiZDdmO1xuQGNvbG9yLXBmLW9yYW5nZS0zMDA6ICAgICAgICAgICAgI2YzOWQzYztcbkBjb2xvci1wZi1vcmFuZ2UtNDAwOiAgICAgICAgICAgICNlYzdhMDg7XG5AY29sb3ItcGYtb3JhbmdlLTUwMDogICAgICAgICAgICAjYjM1YzAwO1xuQGNvbG9yLXBmLW9yYW5nZS02MDA6ICAgICAgICAgICAgIzc3M2QwMDtcbkBjb2xvci1wZi1vcmFuZ2UtNzAwOiAgICAgICAgICAgICMzYjFmMDA7XG5AY29sb3ItcGYtcHVycGxlLTEwMDogICAgICAgICAgICAjYzdiZmZmO1xuQGNvbG9yLXBmLXB1cnBsZS0yMDA6ICAgICAgICAgICAgI2ExOGZmZjtcbkBjb2xvci1wZi1wdXJwbGUtMzAwOiAgICAgICAgICAgICM4NDYxZjc7XG5AY29sb3ItcGYtcHVycGxlLTQwMDogICAgICAgICAgICAjNzAzZmVjO1xuQGNvbG9yLXBmLXB1cnBsZS01MDA6ICAgICAgICAgICAgIzU4MmZjMDtcbkBjb2xvci1wZi1wdXJwbGUtNjAwOiAgICAgICAgICAgICM0MDE5OWE7XG5AY29sb3ItcGYtcHVycGxlLTcwMDogICAgICAgICAgICAjMWYwMDY2O1xuQGNvbG9yLXBmLXJlZC0xMDA6ICAgICAgICAgICAgICAgI2NjMDAwMDtcbkBjb2xvci1wZi1yZWQtMjAwOiAgICAgICAgICAgICAgICNhMzAwMDA7XG5AY29sb3ItcGYtcmVkLTMwMDogICAgICAgICAgICAgICAjOGIwMDAwO1xuQGNvbG9yLXBmLXJlZC00MDA6ICAgICAgICAgICAgICAgIzQ3MDAwMDtcbkBjb2xvci1wZi1yZWQtNTAwOiAgICAgICAgICAgICAgICMyYzAwMDA7XG5cbkBjb2xvci1wZi1ibGFjazogICAgICAgICAgICAgICAgICMwMzAzMDM7XG5AY29sb3ItcGYtYmx1ZTogICAgICAgICAgICAgICAgICBAY29sb3ItcGYtYmx1ZS00MDA7XG5AY29sb3ItcGYtY3lhbjogICAgICAgICAgICAgICAgICBAY29sb3ItcGYtY3lhbi00MDA7XG5AY29sb3ItcGYtZ29sZDogICAgICAgICAgICAgICAgICBAY29sb3ItcGYtZ29sZC00MDA7XG5AY29sb3ItcGYtZ3JlZW46ICAgICAgICAgICAgICAgICBAY29sb3ItcGYtZ3JlZW4tNDAwO1xuQGNvbG9yLXBmLWxpZ2h0LWJsdWU6ICAgICAgICAgICAgQGNvbG9yLXBmLWxpZ2h0LWJsdWUtNDAwO1xuQGNvbG9yLXBmLWxpZ2h0LWdyZWVuOiAgICAgICAgICAgQGNvbG9yLXBmLWxpZ2h0LWdyZWVuLTQwMDtcbkBjb2xvci1wZi1vcmFuZ2U6ICAgICAgICAgICAgICAgIEBjb2xvci1wZi1vcmFuZ2UtNDAwO1xuQGNvbG9yLXBmLXB1cnBsZTogICAgICAgICAgICAgICAgQGNvbG9yLXBmLXB1cnBsZS00MDA7XG5AY29sb3ItcGYtcmVkOiAgICAgICAgICAgICAgICAgICBAY29sb3ItcGYtcmVkLTEwMDtcbkBjb2xvci1wZi13aGl0ZTogICAgICAgICAgICAgICAgICNmZmY7XG4iLCIvKiBzdHlsZWxpbnQtZGlzYWJsZSAqL1xuLnBhZGRpbmctbG9vcCAoQGkpIHdoZW4gKEBpID4gMCkge1xuICAucGFkZGluZy1Ae2l9IHtcbiAgICBwYWRkaW5nOiB+J0B7aX1weCcgIWltcG9ydGFudDtcbiAgfVxuICAucGFkZGluZy10b3AtQHtpfSB7XG4gICAgcGFkZGluZy10b3A6IH4nQHtpfXB4JyAhaW1wb3J0YW50O1xuICB9XG4gIC5wYWRkaW5nLWxlZnQtQHtpfSB7XG4gICAgcGFkZGluZy1sZWZ0OiB+J0B7aX1weCcgIWltcG9ydGFudDtcbiAgfVxuICAucGFkZGluZy1ib3R0b20tQHtpfSB7XG4gICAgcGFkZGluZy1ib3R0b206IH4nQHtpfXB4JyAhaW1wb3J0YW50O1xuICB9XG4gIC5wYWRkaW5nLXJpZ2h0LUB7aX0ge1xuICAgIHBhZGRpbmctcmlnaHQ6IH4nQHtpfXB4JyAhaW1wb3J0YW50O1xuICB9XG4gIC5wYWRkaW5nLWxvb3AoQGkgLSAxKTtcbn1cblxuLm1hcmdpbi1sb29wIChAaSkgd2hlbiAoQGkgPiAwKSB7XG4gIC5tYXJnaW4tQHtpfSB7XG4gICAgbWFyZ2luOiB+J0B7aX1weCcgIWltcG9ydGFudDtcbiAgfVxuICAubWFyZ2luLXRvcC1Ae2l9IHtcbiAgICBtYXJnaW4tdG9wOiB+J0B7aX1weCcgIWltcG9ydGFudDtcbiAgfVxuICAubWFyZ2luLWxlZnQtQHtpfSB7XG4gICAgbWFyZ2luLWxlZnQ6IH4nQHtpfXB4JyAhaW1wb3J0YW50O1xuICB9XG4gIC5tYXJnaW4tYm90dG9tLUB7aX0ge1xuICAgIG1hcmdpbi1ib3R0b206IH4nQHtpfXB4JyAhaW1wb3J0YW50O1xuICB9XG4gIC5tYXJnaW4tcmlnaHQtQHtpfSB7XG4gICAgbWFyZ2luLXJpZ2h0OiB+J0B7aX1weCcgIWltcG9ydGFudDtcbiAgfVxuICAubWFyZ2luLWxvb3AoQGkgLSAxKTtcbn1cblxuLm1hcmdpbi1sb29wKDIwKTtcbi5wYWRkaW5nLWxvb3AoMjApO1xuLyogc3R5bGVsaW50LWVuYWJsZSAqL1xuIiwiLnBmbmctY2FyZCB7XG4gIC5jYXJkLXBmLWZvb3RlciB7XG4gICAgbWluLWhlaWdodDogNjBweDtcbiAgfVxuICAmLnBmbmctY2FyZC1uby1wYWRkaW5nIHtcbiAgICAmLmNhcmQtcGYge1xuICAgICAgcGFkZGluZy1sZWZ0OiAwO1xuICAgICAgcGFkZGluZy1yaWdodDogMDtcbiAgICB9XG4gICAgLmNhcmQtcGYtYm9keSB7XG4gICAgICBtYXJnaW4tdG9wOiAwO1xuICAgICAgcGFkZGluZy1ib3R0b206IDA7XG4gICAgfVxuICAgIC5jYXJkLXBmLWhlYWRpbmcge1xuICAgICAgbWFyZ2luLWJvdHRvbTogMDtcbiAgICAgIG1hcmdpbi1sZWZ0OiAwO1xuICAgICAgbWFyZ2luLXJpZ2h0OiAwO1xuICAgIH1cbiAgfVxufVxuXG4ucGZuZy1jYXJkLWhlYWRpbmctbm8tYm90dG9tIHtcbiAgbWFyZ2luOiAwIC0yMHB4IDBweDtcbiAgcGFkZGluZzogMCAyMHB4O1xufVxuIiwiLnBmbmctY2FyZC1pbmZvLXN0YXR1cyB7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIG1hcmdpbjogMCAxMHB4O1xuICAucGZuZy1jYXJkLWluZm8taW1hZ2Uge1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgICBmbGV4LWRpcmVjdGlvbjpjb2x1bW47XG4gICAgbWFyZ2luLXJpZ2h0OiAxNXB4O1xuICAgIC5pbmZvLWljb24ge1xuICAgICAgZm9udC1zaXplOiA1MHB4O1xuICAgIH1cbiAgICAuaW5mby1pbWcge1xuICAgICAgbWF4LWhlaWdodDogNTBweDtcbiAgICB9XG4gIH1cbiAgLnBmbmctY2FyZC1pbmZvLWNvbnRlbnQge1xuICAgIG1hcmdpbjogMTBweCAwO1xuICAgIC5wZm5nLWNhcmQtdGl0bGUge1xuICAgICAgbWFyZ2luLXRvcDogMTBweDtcbiAgICAgIG1hcmdpbi1ib3R0b206IDE1cHg7XG4gICAgfVxuICB9XG59XG4iLCIucGZuZy1ibG9jay1jb3B5IHtcbiAgbWF4LXdpZHRoOiAxMDAlO1xuICAmLWlubmVyLWNvbnRhaW5lciB7XG4gICAgYmFja2dyb3VuZDogI2ZmZjtcbiAgICBib3JkZXI6IDFweCBzb2xpZCAjYmJiO1xuICB9XG4gICYtcHJldmlldyB7XG4gICAgZGlzcGxheTogZmxleDtcbiAgICAmLnBmLWlzLW9wZW4ge1xuICAgICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkICNiYmI7XG4gICAgfVxuICAgICYtYnRuIHtcbiAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgIGJvcmRlci1yaWdodDogMXB4IHNvbGlkICNiYmI7XG4gICAgICBwYWRkaW5nOiAwO1xuICAgIH1cbiAgICAmLWljb24ge1xuICAgICAgY3Vyc29yOiBwb2ludGVyO1xuICAgICAgcGFkZGluZzogMCAuNjc5ZW07XG4gICAgICAmLmZhLWFuZ2xlLXJpZ2h0IHtcbiAgICAgICAgcGFkZGluZzogMCAuODIyZW07XG4gICAgICB9XG4gICAgfVxuICAgICYtdHh0LWNvbnQge1xuICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gICAgICBmbGV4LWdyb3c6IDE7XG4gICAgICBvdmVyZmxvdzogaGlkZGVuO1xuICAgICAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgICB9XG4gICAgJi10eHQge1xuICAgICAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgICAgIHRleHQtb3ZlcmZsb3c6IGVsbGlwc2lzO1xuICAgICAgbWFyZ2luLXJpZ2h0OiAxZW07XG4gICAgICBtYXJnaW4tbGVmdDogLjc1ZW07XG4gICAgICBib3JkZXI6IG5vbmU7XG4gICAgICB3aWR0aDogMTAwJTtcbiAgICB9XG4gIH1cbiAgJi1idG4ge1xuICAgIGJvcmRlcjogbm9uZTtcbiAgICBib3JkZXItbGVmdDogMXB4IHNvbGlkICNiYmI7XG4gICAgYm94LXNoYWRvdzogbm9uZTtcbiAgICBmb250LXNpemU6IDFlbTtcbiAgICAmOmFjdGl2ZSB7XG4gICAgICBib3gtc2hhZG93OiBpbnNldCAwIDJweCA1cHggMCByZ2JhKDAsIDAsIDAsIC4yNSk7XG4gICAgfVxuICB9XG4gICYtYm9keSB7XG4gICAgcGFkZGluZzogMTVweDtcbiAgICB3b3JkLXdyYXA6IGJyZWFrLXdvcmQ7XG4gIH1cbn1cbiIsIi5wZm5nLWlubGluZS1jb3B5IHtcbiAgZGlzcGxheTogaW5saW5lLWZsZXg7XG4gIGJvcmRlcjogMXB4IHNvbGlkICNiYmI7XG4gIGJhY2tncm91bmQtY29sb3I6ICNmZmY7XG4gIGZvbnQtc2l6ZTogc21hbGxlcjtcbiAgbWFyZ2luLWxlZnQ6IC4yNWVtO1xuICBtYXJnaW4tcmlnaHQ6IC4yNWVtO1xuICBtYXgtd2lkdGg6IDEwMCU7XG4gICYtYm9keSB7XG4gICAgd2lkdGg6IDEwMCU7XG4gIH1cbiAgJi10eHQtY29udCB7XG4gICAgZmxleC1ncm93OiAxO1xuICAgIHBhZGRpbmc6IDJweCA2cHg7XG4gICAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgICB0ZXh0LW92ZXJmbG93OiBlbGxpcHNpcztcbiAgICBvdmVyZmxvdzogaGlkZGVuO1xuICAgIGJvcmRlcjogbm9uZTtcbiAgICB3aWR0aDogMTAwJTtcbiAgfVxuICAmLWJ0biB7XG4gICAgYm9yZGVyOiBub25lO1xuICAgIGJvcmRlci1sZWZ0OiAxcHggc29saWQgI2JiYjtcbiAgICBib3gtc2hhZG93OiBub25lO1xuICAgIGJhY2tncm91bmQtaW1hZ2U6IGxpbmVhci1ncmFkaWVudCh0byBib3R0b20sICNmYWZhZmEsICNlZGVkZWQpO1xuICAgICY6aG92ZXIsXG4gICAgJjphY3RpdmUsXG4gICAgJjpmb2N1cyB7XG4gICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZjFmMWYxO1xuICAgIH1cbiAgfVxufVxuIiwiLmJsYW5rLXNsYXRlLXBmIHtcbiAgbWFyZ2luLWJvdHRvbTogMDtcbiAgYnV0dG9uIHtcbiAgICBtYXJnaW4tcmlnaHQ6IDRweDtcbiAgfVxufVxuIiwiLmZpbHRlci1wZiB7XG4gIGEgeyBjdXJzb3I6IHBvaW50ZXI7IH1cbn1cbi8qIEZpeGVzIGlzc3VlICMxMzAgKi9cbi5maWx0ZXItc2VsZWN0IHtcbiAgLmJ0biB7XG4gICAgYm9yZGVyLWxlZnQ6IDA7XG4gIH1cbn1cbi5kcm9wZG93bi1tZW51IHsgbWluLXdpZHRoOiAxNzZweDsgfVxuIiwiLmZpbHRlci1wZiB7XG4gIGEgeyBjdXJzb3I6IHBvaW50ZXI7IH1cbiAgJi5maWx0ZXItZmllbGRzIHtcbiAgICAuZm9ybS1ncm91cCB7XG4gICAgICBwYWRkaW5nLWxlZnQ6IDA7XG4gICAgICB3aWR0aDogMjc1cHg7XG4gICAgfVxuICAgIC50b29sdGlwIHtcbiAgICAgIHdoaXRlLXNwYWNlOiBub3dyYXA7XG4gICAgfVxuICAgIC50eXBlYWhlYWQtaW5wdXQtY29udGFpbmVyIHtcbiAgICAgICYuZGlzYWJsZWQge1xuICAgICAgICB3aWR0aDogMTAwJTtcbiAgICAgICAgLmNhcmV0IHtcbiAgICAgICAgICB0b3A6IDhweDtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgICAgcGFkZGluZy1yaWdodDogMDtcbiAgICAgIC5jYXJldCB7XG4gICAgICAgIGNvbG9yOiBAY29sb3ItcGYtYmxhY2stNTAwO1xuICAgICAgICBmb250LXN0eWxlOiBpdGFsaWM7XG4gICAgICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICAgICAgdG9wOiAxMHB4O1xuICAgICAgICByaWdodDogMTJweDtcbiAgICAgICAgei1pbmRleDogMjtcbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cblxuLmZpbHRlci1zZWxlY3Qge1xuICAuYnRuLWRlZmF1bHQge1xuICAgIGJhY2tncm91bmQtY29sb3I6IEBjb2xvci1wZi13aGl0ZTtcbiAgICBiYWNrZ3JvdW5kLWltYWdlOiBub25lO1xuICAgIC5wbGFjZWhvbGRlciB7XG4gICAgICBjb2xvcjogQGNvbG9yLXBmLWJsYWNrLTUwMDtcbiAgICAgIGZvbnQtc3R5bGU6IGl0YWxpYztcbiAgICAgIGZvbnQtd2VpZ2h0OiA0MDA7XG4gICAgfVxuICB9XG4gIC5hdmF0YXIge1xuICAgIGhlaWdodDogMjBweDtcbiAgICBtYXJnaW4tcmlnaHQ6IDVweDtcbiAgfVxufVxuXG4vKiBzdHlsZWxpbnQtZGlzYWJsZSAqL1xuLmlucHV0LWdyb3VwIHtcbiAgLmlucHV0LWdyb3VwLWJ0biB7XG4gICAgLmRyb3Bkb3duLW1lbnU+LnNlbGVjdGVkPmEge1xuICAgICAgYmFja2dyb3VuZC1jb2xvcjogQGNvbG9yLXBmLWJsdWUgIWltcG9ydGFudDtcbiAgICAgIGJvcmRlci1jb2xvcjogQGNvbG9yLXBmLWJsdWUtNDAwICFpbXBvcnRhbnQ7IC8vIHdhcyAjMDA3NmI3XG4gICAgICBjb2xvcjogQGNvbG9yLXBmLXdoaXRlO1xuICAgIH1cbiAgfVxufVxuLyogc3R5bGVpbnQtZW5hYmxlICovXG5cbi5wZm5nLWZpbHRlci1kZWxldGUge1xuICBmb250LXNpemU6IDEycHg7XG4gIG9wYWNpdHk6IDA7XG4gIHBhZGRpbmctdG9wOiA0cHg7XG4gIGEge1xuICAgIGNvbG9yOiBAY29sb3ItcGYtYmxhY2s7XG4gICAgb3BhY2l0eTogLjc7XG4gICAgJjpob3ZlciB7XG4gICAgICBvcGFjaXR5OiAxO1xuICAgIH1cbiAgfVxuICAuZHJvcGRvd24taXRlbTpob3ZlciAmIHtcbiAgICBvcGFjaXR5OiAxO1xuICAgIHRyYW5zaXRpb24tZHVyYXRpb246IC41cztcbiAgICB0cmFuc2l0aW9uLXByb3BlcnR5OiBvcGFjaXR5O1xuICAgIHRyYW5zaXRpb24tdGltaW5nLWZ1bmN0aW9uOiBlYXNlLWluO1xuICB9XG59XG5cbi5wZm5nLWZpbHRlci1kZWxldGUtY29uZmlybSB7XG4gIGZvbnQtc2l6ZTogMTJweDtcbiAgcGFkZGluZy1ib3R0b206IDVweDtcbiAgcGFkZGluZy1yaWdodDogNXB4O1xuICBwYWRkaW5nLXRvcDogNXB4O1xuICBhIHtcbiAgICBjb2xvcjogQGNvbG9yLXBmLXdoaXRlO1xuICAgIG9wYWNpdHk6IC45O1xuICAgIC5mYTpiZWZvcmUge1xuICAgICAgY29sb3I6IEBjb2xvci1wZi13aGl0ZTtcbiAgICB9XG4gICAgJjpob3ZlciB7XG4gICAgICBvcGFjaXR5OiAxO1xuICAgIH1cbiAgfVxufVxuXG4ucGZuZy1maWx0ZXItZGVsZXRlLXNsaWRlIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogQGNvbG9yLXBmLXJlZDtcbiAgcmlnaHQ6IC0xMDAlO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRyYW5zaXRpb24tZHVyYXRpb246IDFzO1xuICB3aWR0aDogMTAwJTtcbiAgei1pbmRleDogMTAwMDtcbiAgJi5zbGlkZS1pbiB7XG4gICAgcmlnaHQ6IDA7XG4gICAgLmNsb3NlIHtcbiAgICAgIG9wYWNpdHk6IC45O1xuICAgICAgdHJhbnNpdGlvbi1kZWxheTogLjVzO1xuICAgICAgdHJhbnNpdGlvbi1kdXJhdGlvbjogLjVzO1xuICAgICAgdHJhbnNpdGlvbi1wcm9wZXJ0eTogb3BhY2l0eTtcbiAgICAgIHRyYW5zaXRpb24tdGltaW5nLWZ1bmN0aW9uOiBlYXNlLWluO1xuICAgIH1cbiAgfVxufVxuXG4ucGZuZy1maWx0ZXItZGVsZXRlLXRleHQge1xuICBjb2xvcjogQGNvbG9yLXBmLXdoaXRlO1xuICBwYWRkaW5nLWxlZnQ6IDEwcHg7XG4gIHBhZGRpbmctdG9wOiAycHg7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbn1cblxuLnBmbmctZmlsdGVyLWRlbGV0ZS13cmFwcGVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBvdmVyZmxvdzogaGlkZGVuO1xufVxuXG4udGFibGUtdmlldy1wZi1zZWxlY3QtcmVzdWx0cyB7XG4gIHBhZGRpbmctdG9wOiAxMHB4O1xufVxuIiwiLmZpbHRlci1wZiB7XG4gIGEgeyBjdXJzb3I6IHBvaW50ZXI7IH1cbiAgLnBmaWNvbi1jbG9zZSB7Y3Vyc29yOiBwb2ludGVyOyB9XG59XG5cbi5wZm5nLXNhdmUtZmlsdGVyLWNsb3NlIHtcbiAgZm9udC1zaXplOiAxMnB4O1xuICBwYWRkaW5nLXRvcDogM3B4O1xufVxuXG4ucGZuZy1zYXZlLWZpbHRlciB7XG4gIGlucHV0IHtcbiAgICBkaXNwbGF5OiBpbmxpbmU7XG4gICAgQG1lZGlhIChtaW4td2lkdGg6IEBzY3JlZW4teHMtbWluKSB7IHdpZHRoOiAxMGVtOyB9XG4gICAgQG1lZGlhIChtaW4td2lkdGg6IEBzY3JlZW4tc20tbWluKSB7IHdpZHRoOiAxNWVtOyB9XG4gIH1cbiAgLnBvcG92ZXIge1xuICAgIG1heC13aWR0aDogaW5pdGlhbDtcbiAgfVxufVxuXG4ucGZuZy1zYXZlLWZpbHRlci1kaXZpZGVyIHtcbiAgYm9yZGVyLXRvcDogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gIG1hcmdpbi1sZWZ0OiAtMTVweDtcbiAgbWFyZ2luLXJpZ2h0OiAtMTVweDtcbiAgbWFyZ2luLXRvcDogMTBweDtcbn1cblxuLnBmbmctc2F2ZS1maWx0ZXItZm9vdGVyIHtcbiAgZmxvYXQ6IHJpZ2h0O1xuICBtYXJnaW4tYm90dG9tOiAxMHB4O1xuICBtYXJnaW4tdG9wOiAxMHB4O1xufVxuIiwiLy8gQ2hlY2tib3ggcGxhY2Vob2xkZXIgZm9yIGhlYWRpbmdcbi5wZm5nLWxpc3QtY2ItcGxhY2Vob2xkZXIge1xuICB3aWR0aDogMTJweDtcbn1cblxuLy8gQ29udGFpbmVyIGZvciBpdGVtIGhlYWRpbmdcbi5wZm5nLWxpc3QtaGVhZGluZyB7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIGZsZXgtZ3JvdzogMTtcbn1cblxuLy8gUm93IGV4cGFuc2lvbiB0b2dnbGUgcGxhY2Vob2xkZXIgZm9yIGhlYWRpbmcgYW5kIGhpZGRlbiB0b2dnbGVzXG4ucGZuZy1saXN0LWV4cGFuZC1wbGFjZWhvbGRlciB7XG4gIHdpZHRoOiA4cHg7XG59XG5cbi5wZm5nLWxpc3QtZXhwYW5kIHtcbiAgLmZhLWFuZ2xlLXJpZ2h0IHtcbiAgICBwYWRkaW5nLWxlZnQ6IDVweDtcbiAgICAmLmZhLWFuZ2xlLWRvd24ge1xuICAgICAgcGFkZGluZy1sZWZ0OiAwO1xuICAgIH1cbiAgfVxufVxuXG4ubGlzdC1wZi1jb250YWluZXIge1xuICAvLyBBZGQgaG92ZXIgc3R5bGUgYW5kIG1vZGlmeSBjdXJzb3JcbiAgLmxpc3QtcGYtY2hldnJvbiB7XG4gICAgY3Vyc29yOiBwb2ludGVyO1xuICAgICY6aG92ZXIge1xuICAgICAgY29sb3I6IEBjb2xvci1wZi1ibHVlLTQwMDtcbiAgICB9XG4gIH1cbn1cblxuLy8gRm9yIGRpc3BsYXlpbmcgY2xvc2UgYnV0dG9uIGluIGV4cGFuc2lvbiBhcmVhXG4ucGZuZy1saXN0LWV4cGFuc2lvbiB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgLmxpc3QtcGYtY29udGVudCB7XG4gICAgZmxleC1ncm93OiAxO1xuICB9XG59XG5cbi8vIEZvciBkaXNwbGF5aW5nIGEgaGVhZGluZyBhYm92ZSB0aGUgbGlzdFxuLnBmbmctbGlzdC1oZWFkaW5nIHtcbiAgLy8gSGlkZSBoZWFkaW5nIGZvciBzbWFsbCBzY3JlZW5zXG4gIEBtZWRpYSAobWF4LXdpZHRoOiA5OTJweCkge1xuICAgIGRpc3BsYXk6IG5vbmU7XG4gIH1cbiAgLy8gSGVhZGluZyBzaG91bGQgbm90IGJlIGNsaWNrYWJsZVxuICBwb2ludGVyLWV2ZW50czogbm9uZTtcblxuICAvLyBIZWFkaW5nIHNob3VsZCBub3QgaGlnaGxpZ2h0IG9uIG1vdXNlIGhvdmVyXG4gICY6aG92ZXIge1xuICAgIGJhY2tncm91bmQtY29sb3I6IEBjb2xvci1wZi13aGl0ZTtcbiAgfVxuXG4gIC8vIEFsbG93IGluZm8gaWNvbnMgdG8gZ2VuZXJhdGUgZXZlbnRzXG4gIGkge1xuICAgIHBvaW50ZXItZXZlbnRzOiBhdXRvO1xuICB9XG5cbiAgLy8gT3ZlcnJpZGUgdG9wIGJvcmRlciBmb3IgaGVhZGluZ1xuICAmLmxpc3QtcGYtaXRlbSB7XG4gICAgYm9yZGVyLXRvcDogbm9uZTtcbiAgfVxuXG4gIC8vIE92ZXJyaWRlIGZvbnQgZm9yIG5vcm1hbCBoZWFkaW5nIHRleHRcbiAgLmxpc3QtcGYtdGl0bGUge1xuICAgIGZvbnQtc2l6ZTogaW5oZXJpdDtcbiAgICBmb250LXdlaWdodDogbm9ybWFsO1xuICB9XG5cbiAgLy8gUmVtb3ZlIHRoZSBkaXZpZGVyIGxpbmUgZm9yIGhlYWRpbmdcbiAgLmxpc3QtcGYtY2hldnJvbiwgLmxpc3QtcGYtc2VsZWN0IHtcbiAgICArIC5saXN0LXBmLWNvbnRlbnQge1xuICAgICAgYm9yZGVyLWxlZnQ6IG5vbmU7XG4gICAgfVxuICB9XG59XG5cbi8vIFBpblxuLnBmbmctbGlzdC1waW4ge1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICBhbGlnbi1zZWxmOiBzdHJldGNoO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiBAY29sb3ItcGYtYmxhY2stMTUwO1xuICBib3gtc2hhZG93OiAtM3B4IDFweCA0cHggMCBAY29sb3ItcGYtYmxhY2stMjAwIGluc2V0O1xuICBkaXNwbGF5OiBmbGV4O1xuICBtYXJnaW4tYm90dG9tOiAtMjBweDtcbiAgbWFyZ2luLWxlZnQ6IC0yMHB4O1xuICBtYXJnaW4tcmlnaHQ6IDIwcHg7XG4gIG1hcmdpbi10b3A6IC0yMHB4O1xuICBwYWRkaW5nLWJvdHRvbTogMjBweDtcbiAgcGFkZGluZy1sZWZ0OiA1cHg7XG4gIHBhZGRpbmctcmlnaHQ6IDVweDtcbiAgcGFkZGluZy10b3A6IDIwcHg7XG4gICYubXVsdGktY3RybHMge1xuICAgIG1hcmdpbi1yaWdodDogMTBweDtcbiAgfVxuICBhIHtcbiAgICBjb2xvcjogQGNvbG9yLXBmLWJsYWNrO1xuICAgIG9wYWNpdHk6IC43O1xuICAgICY6aG92ZXIge1xuICAgICAgb3BhY2l0eTogMTtcbiAgICB9XG4gIH1cbn1cblxuLy8gUGluIGNvbnN0YWluZXIgdG8gaGlkZS9zaG93IGZlYXR1cmVcbi5wZm5nLWxpc3QtcGluLWNvbnRhaW5lciB7XG4gIGFsaWduLXNlbGY6IHN0cmV0Y2g7XG4gIGRpc3BsYXk6IGZsZXg7XG59XG5cbi8vIFBpbiBwbGFjZWhvbGRlciBmb3IgaGVhZGluZyBhbmQgbm9uLXBpbm5lZCBpdGVtc1xuLnBmbmctbGlzdC1waW4tcGxhY2Vob2xkZXIge1xuICBtYXJnaW4tbGVmdDogLTIwcHg7XG4gIHdpZHRoOiAzOXB4O1xuICAmLm11bHRpLWN0cmxzIHtcbiAgICB3aWR0aDogMjhweDtcbiAgfVxufVxuIiwiLnBmbmctdmVydGljYWwtaGlkZS1uYXYge1xuICAubmF2LXBmLXZlcnRpY2FsIHtcbiAgICB0b3A6IDJweDtcbiAgfVxufVxuIiwiLmRyYXdlci1wZiB7XG4gIG92ZXJmbG93LXk6IGhpZGRlbjtcbiAgZGlzcGxheTogZmxleDtcbiAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjtcbiAgLmRyYXdlci1wZi10aXRsZSB7XG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB9XG4gIC5wYW5lbC1ncm91cCB7XG4gICAgZGlzcGxheTogZmxleDtcbiAgICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICAgIHBvc2l0aW9uOiBpbml0aWFsO1xuICAgIGJvdHRvbTogaW5pdGlhbDtcbiAgICB0b3A6IGluaXRpYWw7XG4gICAgb3ZlcmZsb3cteTogYXV0bztcbiAgICAucGFuZWwucGFuZWwtZGVmYXVsdC5leHBhbmRlZCB7XG4gICAgICBtaW4taGVpZ2h0OiAxNzVweDtcbiAgICAgIGZsZXg6IDEgMSBhdXRvO1xuICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47XG4gICAgfVxuICAgIC5wYW5lbC1jb2xsYXBzZS5pbiB7XG4gICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjtcbiAgICAgIGZsZXg6IDEgMSBhdXRvO1xuICAgICAgbWluLWhlaWdodDogMDtcbiAgICAgIC5wYW5lbC1ib2R5IHtcbiAgICAgICAgZmxleDogMSAxIGF1dG87XG4gICAgICAgIG92ZXJmbG93LXk6IGF1dG87XG4gICAgICB9XG4gICAgfVxuICB9XG4gIC5kcmF3ZXItcGYtYWN0aW9uIHtcbiAgICBmbGV4OiBub25lO1xuICB9XG59XG4iLCIudG9hc3QtcGYtYWN0aW9uID4gYSB7IGN1cnNvcjogcG9pbnRlcjsgfVxuXG4udG9hc3QtcGYge1xuICAuZHJvcGRvd24tbWVudSA+IGxpID4gYSB7IGN1cnNvcjogcG9pbnRlcjsgfVxufVxuIiwiLnNvcnQtcGYge1xuICAuYnRuLWxpbmsge1xuICAgIG1hcmdpbi1sZWZ0OiAxMHB4O1xuICAgIHBhZGRpbmc6IDRweCAwO1xuICAgIG1pbi13aWR0aDogMDtcbiAgICBjb2xvcjogQGNvbG9yLXBmLWJsYWNrOyAvLyB3YXMgIzI1MjUyNTtcbiAgICBmb250LXNpemU6IDE2cHg7XG4gICAgbGluZS1oZWlnaHQ6IDE7XG4gICAgJjpob3ZlciB7IGNvbG9yOiBAY29sb3ItcGYtYmx1ZS00MDA7IH1cbiAgfVxufVxuIiwiLnBmbmcge1xuICAmLXRhYmxlIHtcbiAgICAuYmxhbmstc2xhdGUtcGYge1xuICAgICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICBib3JkZXItbGVmdDogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICBib3JkZXItcmlnaHQ6IDFweCBzb2xpZCBAY29sb3ItcGYtYmxhY2stMzAwO1xuICAgICAgYm9yZGVyLXRvcDogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgfVxuICAgIC5jb250ZW50LXZpZXctcGYtcGFnaW5hdGlvbiB7XG4gICAgICBib3JkZXItdG9wOiBub25lO1xuICAgIH1cbiAgICAucm93LnRvb2xiYXItcGYge1xuICAgICAgbWFyZ2luLWxlZnQ6IDA7XG4gICAgICBtYXJnaW4tcmlnaHQ6IDA7XG4gICAgICBiYWNrZ3JvdW5kLWNvbG9yOiBAY29sb3ItcGYtYmxhY2stMTUwO1xuICAgICAgYm9yZGVyOiAxcHggc29saWQgQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgIGJvcmRlci1ib3R0b206IG5vbmU7XG4gICAgfVxuICAgIC50YWJsZS12aWV3LXBmLXNlbGVjdC1yZXN1bHRzIHtcbiAgICAgIHBhZGRpbmctYm90dG9tOiAxMHB4O1xuICAgIH1cbiAgICAmLWRuZC1jb250YWluZXIge1xuICAgICAgYWxpZ24tc2VsZjogc3RyZXRjaDtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgfVxuICAgICYtZG5kLWhlYWRlciB7XG4gICAgICAmOmJlZm9yZSB7XG4gICAgICAgIGJhY2tncm91bmQtaW1hZ2U6IGxpbmVhci1ncmFkaWVudCh0byBib3R0b20sIEBjb2xvci1wZi1ibHVlLTQwMCA2MCUsIEBjb2xvci1wZi13aGl0ZSAwJSk7XG4gICAgICAgIGJhY2tncm91bmQtcG9zaXRpb246IGxlZnQ7XG4gICAgICAgIGJhY2tncm91bmQtcmVwZWF0OiByZXBlYXQteTtcbiAgICAgICAgYmFja2dyb3VuZC1zaXplOiAycHggNXB4O1xuICAgICAgICBib3JkZXI6IDRweCBzb2xpZCBAY29sb3ItcGYtYmx1ZS00MDA7XG4gICAgICAgIGJvcmRlci1jb2xvcjogQGNvbG9yLXBmLWJsdWUtNTAwO1xuICAgICAgICBjb250ZW50OiAnJztcbiAgICAgICAgaGVpZ2h0OiAxMDAlO1xuICAgICAgICBsZWZ0OiAwO1xuICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gICAgICAgIHRvcDogMDtcbiAgICAgICAgd2lkdGg6IDEwcHg7XG4gICAgICB9XG4gICAgICAmOmhvdmVyIHtcbiAgICAgICAgY3Vyc29yOiBtb3ZlO1xuICAgICAgfVxuICAgIH1cbiAgICAmLWV4cGFuZC1jb250YWluZXIge1xuICAgICAgYWxpZ24taXRlbXM6IGZsZXgtc3RhcnQ7XG4gICAgICBib3JkZXItYm90dG9tOiAxcHggc29saWQgQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgIGJvcmRlci1sZWZ0OiAxcHggc29saWQgQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgIGJvcmRlci1yaWdodDogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAgcGFkZGluZzogMjBweDtcbiAgICB9XG4gICAgJi1leHBhbmQtY29udGVudCB7XG4gICAgICBmbGV4LWdyb3c6IDE7XG4gICAgfVxuICAgICYtc2VsZWN0IHtcbiAgICAgIC5uZy11bnRvdWNoZWQge1xuICAgICAgICBtYXJnaW4tdG9wOiAwO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICAmLXRhYmxlIHtcbiAgICAucm93IHtcbiAgICAgICYudG9vbGJhci1wZiB7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6IEBjb2xvci1wZi1ibGFjay0xNTA7XG4gICAgICAgIGJvcmRlci1ib3R0b206IG5vbmU7XG4gICAgICAgIGJvcmRlci1sZWZ0OiAxcHggc29saWQgQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgICAgYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgICAgYm9yZGVyLXRvcDogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICAgIGJveC1zaGFkb3c6IG5vbmU7XG4gICAgICAgIG1hcmdpbi1sZWZ0OiAwO1xuICAgICAgICBtYXJnaW4tcmlnaHQ6IDA7XG4gICAgICB9XG4gICAgfVxuICAgIC50YWJsZS12aWV3LXBmLXNlbGVjdC1yZXN1bHRzIHtcbiAgICAgIHBhZGRpbmctYm90dG9tOiAxMHB4O1xuICAgIH1cbiAgfVxufVxuLypcbiogUGF0dGVybkZseSB0YWJsZSB0aGVtZSBmb3Igbmd4LWRhdGF0YWJsZVxuKi9cbi5uZ3gtZGF0YXRhYmxlIHtcbiAgLypcbiAgKiBkcmFndWxhIHN0eWxlIG92ZXJ3cml0ZXNcbiAgKi9cbiAgLmd1LW1pcnJvciB7XG4gICAgLyogc3R5bGVsaW50LWRpc2FibGUgKi9cbiAgICBwb3NpdGlvbjogZml4ZWQgIWltcG9ydGFudDtcbiAgICBtYXJnaW46IDAgIWltcG9ydGFudDtcbiAgICB6LWluZGV4OiA5OTk5ICFpbXBvcnRhbnQ7XG4gICAgLyogc3R5bGVsaW50LWVuYWJsZSAqL1xuICAgIG9wYWNpdHk6IC41O1xuICAgIC8qIC1tcy1maWx0ZXI6ICdwcm9naWQ6RFhJbWFnZVRyYW5zZm9ybS5NaWNyb3NvZnQuQWxwaGEoT3BhY2l0eT01MCknOyAqL1xuICAgIC8qIGZpbHRlcjogYWxwaGEob3BhY2l0eT01MCk7ICovXG4gIH1cbiAgLmd1LWhpZGUge1xuICAgIC8qIHN0eWxlbGludC1kaXNhYmxlICovXG4gICAgZGlzcGxheTogbm9uZSAhaW1wb3J0YW50O1xuICAgIC8qIHN0eWxlbGludC1lbmFibGUgKi9cbiAgfVxuICAuZ3UtdW5zZWxlY3RhYmxlIHtcbiAgICAvKiBzdHlsZWxpbnQtZGlzYWJsZSAqL1xuICAgIC13ZWJraXQtdXNlci1zZWxlY3Q6IG5vbmUgIWltcG9ydGFudDtcbiAgICAtbW96LXVzZXItc2VsZWN0OiBub25lICFpbXBvcnRhbnQ7XG4gICAgLW1zLXVzZXItc2VsZWN0OiBub25lICFpbXBvcnRhbnQ7XG4gICAgdXNlci1zZWxlY3Q6IG5vbmUgIWltcG9ydGFudDtcbiAgICAvKiBzdHlsZWxpbnQtZW5hYmxlICovXG4gIH1cbiAgLmd1LXRyYW5zaXQge1xuICAgIGRpc3BsYXk6IGlubGluZS1mbGV4O1xuICAgIGNvbG9yOiAjZmZmO1xuICAgIGJhY2tncm91bmQtY29sb3I6IEBjb2xvci1wZi1ibHVlLTQwMDtcbiAgICBvcGFjaXR5OiAuNTtcbiAgICAvKiAtbXMtZmlsdGVyOiAncHJvZ2lkOkRYSW1hZ2VUcmFuc2Zvcm0uTWljcm9zb2Z0LkFscGhhKE9wYWNpdHk9NTApJzsgKi9cbiAgICAvKiBmaWx0ZXI6IGFscGhhKG9wYWNpdHk9NTApOyAqL1xuICB9XG4gICYucGF0dGVybmZseSB7XG4gICAgbWFyZ2luLWJvdHRvbTogLTVweDtcbiAgICBib3JkZXItdG9wOiAxcHggc29saWQgQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICBib3gtc2hhZG93OiBub25lO1xuICAgIC5kYXRhdGFibGUtaGVhZGVyIHtcbiAgICAgIC8qIHN0eWxlbGludC1kaXNhYmxlICovXG4gICAgICBiYWNrZ3JvdW5kLWltYWdlOiAtd2Via2l0LWxpbmVhci1ncmFkaWVudCh0b3AsIEBjb2xvci1wZi1ibGFjay0xMDAgMCwgQGNvbG9yLXBmLWJsYWNrLTIwMCAxMDAlKTtcbiAgICAgIC8qIHN0eWxlbGludC1lbmFibGUgKi9cbiAgICAgIGJhY2tncm91bmQtaW1hZ2U6IGxpbmVhci1ncmFkaWVudCh0byBib3R0b20sIEBjb2xvci1wZi1ibGFjay0xMDAgMCwgQGNvbG9yLXBmLWJsYWNrLTIwMCAxMDAlKTtcbiAgICAgIGJhY2tncm91bmQtcmVwZWF0OiByZXBlYXQteDtcbiAgICAgIGJhY2tncm91bmQtY29sb3I6IEBjb2xvci1wZi1ibGFjay0xNTA7XG4gICAgICAvKiBzdHlsZWxpbnQtZGlzYWJsZSAqL1xuICAgICAgaGVpZ2h0OiB1bnNldCAhaW1wb3J0YW50O1xuICAgICAgLyogc3R5bGVsaW50LWVuYWJsZSAqL1xuICAgICAgJi1jZWxsIHtcbiAgICAgICAgYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICAgIGZvbnQtd2VpZ2h0OiA2MDA7XG4gICAgICAgIHBhZGRpbmc6IDJweCAxMHB4IDNweDtcbiAgICAgICAgdmVydGljYWwtYWxpZ246IGJvdHRvbTtcbiAgICAgICAgJjpmaXJzdC1jaGlsZCB7XG4gICAgICAgICAgYm9yZGVyLWxlZnQ6IDFweCBzb2xpZCBAY29sb3ItcGYtYmxhY2stMzAwO1xuICAgICAgICB9XG4gICAgICAgICYucGZuZy10YWJsZS1zZWxlY3Qge1xuICAgICAgICAgIHBhZGRpbmctdG9wOiA2cHg7XG4gICAgICAgICAgdGV4dC1hbGlnbjogY2VudGVyO1xuICAgICAgICB9XG4gICAgICAgICYucGZuZy10YWJsZS1kbmQtb25seSB7XG4gICAgICAgICAgcGFkZGluZy1sZWZ0OiAwO1xuICAgICAgICAgIHBhZGRpbmctcmlnaHQ6IDA7XG4gICAgICAgIH1cbiAgICAgICAgJi5zb3J0YWJsZS5zb3J0LWFjdGl2ZSB7XG4gICAgICAgICAgY29sb3I6IEBjb2xvci1wZi1ibHVlLTQwMDtcbiAgICAgICAgICAmLnNvcnQtYXNjLCAmLnNvcnQtZGVzYyB7XG4gICAgICAgICAgICAvKiBzdHlsZWxpbnQtZGlzYWJsZSAqL1xuICAgICAgICAgICAgY29sb3I6IEBjb2xvci1wZi1ibHVlLTQwMCAhaW1wb3J0YW50O1xuICAgICAgICAgICAgLyogc3R5bGVsaW50LWVuYWJsZSAqL1xuICAgICAgICAgICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgICAgICAgICAgLyogc3R5bGVsaW50LWRpc2FibGUgKi9cbiAgICAgICAgICAgIGJhY2tncm91bmQtaW1hZ2U6IG5vbmUgIWltcG9ydGFudDtcbiAgICAgICAgICAgIC8qIHN0eWxlbGludC1lbmFibGUgKi9cbiAgICAgICAgICAgIHBhZGRpbmctdG9wOiAycHg7XG4gICAgICAgICAgICAmOmJlZm9yZSB7XG4gICAgICAgICAgICAgIGJhY2tncm91bmQ6IEBjb2xvci1wZi1ibHVlLTQwMDtcbiAgICAgICAgICAgICAgY29udGVudDogJyc7XG4gICAgICAgICAgICAgIGhlaWdodDogMnB4O1xuICAgICAgICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gICAgICAgICAgICAgIGxlZnQ6IDA7XG4gICAgICAgICAgICAgIHRvcDogMDtcbiAgICAgICAgICAgICAgd2lkdGg6IDEwMCU7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAuZGF0YXRhYmxlLWhlYWRlci1jZWxsLWxhYmVsIHtcbiAgICAgICAgICAgICAgJjphZnRlciB7XG4gICAgICAgICAgICAgICAgY29udGVudDogJ1xcZjEwNyc7XG4gICAgICAgICAgICAgICAgY29sb3I6IEBjb2xvci1wZi1ibHVlLTQwMDtcbiAgICAgICAgICAgICAgICBmb250LWZhbWlseTogRm9udEF3ZXNvbWU7XG4gICAgICAgICAgICAgICAgZm9udC1zaXplOiAxMHB4O1xuICAgICAgICAgICAgICAgIGZvbnQtd2VpZ2h0OiA0MDA7XG4gICAgICAgICAgICAgICAgaGVpZ2h0OiA5cHg7XG4gICAgICAgICAgICAgICAgbWFyZ2luLWxlZnQ6IDVweDtcbiAgICAgICAgICAgICAgICBsaW5lLWhlaWdodDogMS4yO1xuICAgICAgICAgICAgICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICAgICAgICAgICAgICB0b3A6IDdweDtcbiAgICAgICAgICAgICAgICB2ZXJ0aWNhbC1hbGlnbjogYmFzZWxpbmU7XG4gICAgICAgICAgICAgICAgd2lkdGg6IDEycHg7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgICAgJi5zb3J0LWFzYyB7XG4gICAgICAgICAgICAuZGF0YXRhYmxlLWhlYWRlci1jZWxsLWxhYmVsIHtcbiAgICAgICAgICAgICAgJjphZnRlciB7XG4gICAgICAgICAgICAgICAgY29udGVudDogJ1xcZjEwNic7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIC5kYXRhdGFibGUtaGVhZGVyLWNlbGwtbGFiZWwge1xuICAgICAgICBsaW5lLWhlaWdodDogMjRweDtcbiAgICAgIH1cbiAgICAgIC5kYXRhdGFibGUtaGVhZGVyLWNlbGwtd3JhcHBlciB7XG4gICAgICAgIGN1cnNvcjogcG9pbnRlcjtcbiAgICAgIH1cbiAgICB9XG4gICAgLmRhdGF0YWJsZS1ib2R5IHtcbiAgICAgICYtcm93IHtcbiAgICAgICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICAgIGJvcmRlci10b3A6IDA7XG4gICAgICAgIHZlcnRpY2FsLWFsaWduOiB0b3A7XG4gICAgICAgIC8qIHN0eWxlbGludC1kaXNhYmxlICovXG4gICAgICAgICYuZGF0YXRhYmxlLXJvdy1ldmVuIHtcbiAgICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiB0cmFuc3BhcmVudCAhaW1wb3J0YW50O1xuICAgICAgICAgICY6aG92ZXIsICY6YWN0aXZlIHtcbiAgICAgICAgICAgIGJhY2tncm91bmQtY29sb3I6IEBjb2xvci1wZi1ibHVlLTUwICFpbXBvcnRhbnQ7XG4gICAgICAgICAgICBib3JkZXItYm90dG9tLWNvbG9yOiBAY29sb3ItcGYtYmx1ZS0yMDA7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgICYuZGF0YXRhYmxlLXJvdy1vZGQge1xuICAgICAgICAgIGJhY2tncm91bmQtY29sb3I6IEBjb2xvci1wZi1ibGFjay0xNTAgIWltcG9ydGFudDtcbiAgICAgICAgICAmOmhvdmVyLCAmOmFjdGl2ZSB7XG4gICAgICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiBAY29sb3ItcGYtYmx1ZS01MCAhaW1wb3J0YW50O1xuICAgICAgICAgICAgYm9yZGVyLWJvdHRvbS1jb2xvcjogQGNvbG9yLXBmLWJsdWUtMjAwO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICAmLmFjdGl2ZSB7XG4gICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogQGNvbG9yLXBmLWJsdWUtNDAwICFpbXBvcnRhbnQ7XG4gICAgICAgICAgYm9yZGVyLWJvdHRvbS1jb2xvcjogQGNvbG9yLXBmLWJsdWUtNTAwICFpbXBvcnRhbnQ7XG4gICAgICAgICAgY29sb3I6ICNmZmY7XG4gICAgICAgICAgJjpob3ZlciB7XG4gICAgICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiBAY29sb3ItcGYtYmx1ZS00MDAgIWltcG9ydGFudDtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgLyogc3R5bGVsaW50LWVuYWJsZSAqL1xuICAgICAgICAuZGF0YXRhYmxlLWJvZHktY2VsbCB7XG4gICAgICAgICAgcGFkZGluZzogMnB4IDEwcHggM3B4O1xuICAgICAgICAgIHRleHQtYWxpZ246IGxlZnQ7XG4gICAgICAgICAgdmVydGljYWwtYWxpZ246IHRvcDtcbiAgICAgICAgICAmLnBmbmctdGFibGUtZG5kLW9ubHkge1xuICAgICAgICAgICAgcGFkZGluZy1sZWZ0OiAwO1xuICAgICAgICAgICAgcGFkZGluZy1yaWdodDogMDtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgICYtY2VsbCB7XG4gICAgICAgIGJvcmRlci1yaWdodDogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICAgICY6Zmlyc3QtY2hpbGQge1xuICAgICAgICAgIGJvcmRlci1sZWZ0OiAxcHggc29saWQgQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgICAgfVxuICAgICAgICAmLWxhYmVsIHtcbiAgICAgICAgICAuZmEge1xuICAgICAgICAgICAgJjpob3ZlciB7XG4gICAgICAgICAgICAgIGN1cnNvcjogcG9pbnRlcjtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIC5kYXRhdGFibGUtc2Nyb2xsLFxuICAgICAgLmRhdGF0YWJsZS1yb3ctd3JhcHBlciB7XG4gICAgICAgIC8qIHN0eWxlbGludC1kaXNhYmxlICovXG4gICAgICAgIHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7XG4gICAgICAgIC8qIHN0eWxlbGludC1lbmFibGUgKi9cbiAgICAgIH1cbiAgICAgIC5kYXRhdGFibGUtZ3JvdXAtaGVhZGVyIHtcbiAgICAgICAgYmFja2dyb3VuZDogQGNvbG9yLXBmLWJsYWNrLTE1MDtcbiAgICAgICAgYm9yZGVyLWJvdHRvbTogc29saWQgMXB4IEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICAgIGJvcmRlci1sZWZ0OiBzb2xpZCAxcHggQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgICAgYm9yZGVyLXJpZ2h0OiBzb2xpZCAxcHggQGNvbG9yLXBmLWJsYWNrLTMwMDtcbiAgICAgICAgLyogc3R5bGVsaW50LWRpc2FibGUgKi9cbiAgICAgICAgd2lkdGg6IDEwMCUgIWltcG9ydGFudDtcbiAgICAgICAgLyogc3R5bGVsaW50LWVuYWJsZSAqL1xuICAgICAgfVxuICAgICAgLmVtcHR5LXJvdyB7XG4gICAgICAgIGJvcmRlci1ib3R0b206IDFweCBzb2xpZCBAY29sb3ItcGYtYmxhY2stMzAwO1xuICAgICAgICBib3JkZXItbGVmdDogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICAgIGJvcmRlci1yaWdodDogMXB4IHNvbGlkIEBjb2xvci1wZi1ibGFjay0zMDA7XG4gICAgICAgIG1hcmdpbi1ib3R0b206IDVweDtcbiAgICAgICAgcGFkZGluZzogMnB4IDEwcHggMXB4O1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIiwiLmRyb3Bkb3duLWtlYmFiLXBmLmludmlzaWJsZSB7XG4gIG9wYWNpdHk6IDA7XG4gIHBvaW50ZXItZXZlbnRzOiBub25lO1xufVxuXG4udG9vbGJhci1wZi1hY3Rpb25zIHtcbiAgLmJ0biB7IG1pbi13aWR0aDogdW5zZXQ7IH1cbiAgLnRvb2xiYXItcGYtdmlldy1zZWxlY3RvciB7XG4gICAgYSB7IGN1cnNvcjogcG9pbnRlcjsgfVxuICB9XG4gIC5kcm9wZG93bi1tZW51IHtcbiAgICBhIHsgY3Vyc29yOiBwb2ludGVyOyB9XG4gIH1cbiAgLmRyb3Bkb3duLWtlYmFiLXBmIHsgZmxvYXQ6IHJpZ2h0OyB9XG4gIC50b29sYmFyLWFwZi1maWx0ZXIge1xuICAgIC8qIHN0eWxlbGludC1kaXNhYmxlICovXG4gICAgcGFkZGluZy1sZWZ0OiAwICFpbXBvcnRhbnQ7XG4gICAgLyogc3R5bGVsaW50LWVuYWJsZSAqL1xuICAgIEBtZWRpYSAobWluLXdpZHRoOiA3NjhweCkge1xuICAgICAgcGFkZGluZy1sZWZ0OiAwO1xuICAgIH1cbiAgfVxufVxuXG4udG9vbGJhci1wZi1pbmNsdWRlLWFjdGlvbnMge1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gIG1hcmdpbjogMCA1cHg7XG59XG5cbi50b29sYmFyLXBmLWFjdGlvbnMubm8tZmlsdGVyLXJlc3VsdHMgeyBtYXJnaW4tYm90dG9tOiAxMHB4OyB9XG4iLCIucGZuZy13aXphcmQtY2FuY2VsLWlubGluZSB7XG4gIG1hcmdpbi1sZWZ0OiAyNXB4O1xufVxuLnBmbmctd2l6YXJkLWZvb3Rlci1pbmxpbmUge1xuICB0ZXh0LWFsaWduOiBsZWZ0O1xufVxuLnBmbmctd2l6YXJkLW1haW4ge1xuICBtYXJnaW4tbGVmdDogMDtcbn1cbi5wZm5nLXdpemFyZC1wb3NpdGlvbi1vdmVycmlkZSB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbn1cbi53aXphcmQtcGYtZm9vdGVyIHtcbiAgLnBmbmctd2l6YXJkLXByZXZpb3VzLWJ0biB7XG4gICAgJi5wZm5nLXdpemFyZC1idG4tbm8tYmFjayB7XG4gICAgICBkaXNwbGF5OiBub25lO1xuICAgIH1cbiAgfVxufVxuLndpemFyZC1wZi1zdGVwcy1pbmRpY2F0b3IgbGkgYS5kaXNhYmxlZCB7XG4gIGN1cnNvcjogZGVmYXVsdDtcbiAgJjpob3ZlciB7XG4gICAgLndpemFyZC1wZi1zdGVwLW51bWJlciB7XG4gICAgICBiYWNrZ3JvdW5kLWNvbG9yOiBAY29sb3ItcGYtd2hpdGU7XG4gICAgICBib3JkZXItY29sb3I6IEBjb2xvci1wZi1ibGFjay00MDA7XG4gICAgICBjb2xvcjogQGNvbG9yLXBmLWJsYWNrLTQwMDtcbiAgICB9XG4gIH1cbn1cbiIsIi5wZm5nLXdpemFyZC1zaW5nbGUtc3RlcCB7XG4gIG1hcmdpbi1sZWZ0OiAwO1xufVxuXG4ud2l6YXJkLXBmLXJvdyB7XG4gIGhlaWdodDogaW5oZXJpdDtcbn1cbiJdLCJmaWxlIjoicGF0dGVybmZseS1uZy5jc3MifQ== */ diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/main.ts b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/main.ts new file mode 100644 index 000000000..078364051 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/main.ts @@ -0,0 +1,73 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import Keycloak, { KeycloakInitOptions } from 'keycloak-js'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +// Load Keycloak config from server. Need to do this before invoking +// keycloak-js constructor to first check the enabled flag. +let keycloakConfig: any; +console.log('[Microcks launch] Origin: ' + location.origin); + +function getKeycloakConfig(callback: any) { + const xhr = new XMLHttpRequest(); + xhr.open('GET', location.origin + '/api/keycloak/config', true); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200 || fileLoaded(xhr)) { + keycloakConfig = JSON.parse(xhr.responseText); + callback(null); + } else { + callback(xhr.response); + } + } + }; + xhr.send(); +} + +// Actually call the getKeycloakConfig function and process with startup. +getKeycloakConfig(function(err: any, datums: any) { + // Deal with error if any. + if (err) { + console.error('[Microcks launch] Error while fetching Keycloak config: ' + err); + throw err; + } + + if (keycloakConfig && keycloakConfig.enabled) { + console.log('[Microcks launch] Keycloak is enabled, launching OIDC login flow...'); + + // Build keycloak-js adapter from config. + //const keycloak = (window as any).Keycloak({ + const keycloak = new Keycloak({ + url: keycloakConfig['auth-server-url'], + realm: keycloakConfig.realm, + clientId: keycloakConfig.resource + }); + //const loginOptions = {onLoad: 'login-required', checkLoginIframe: undefined}; + const loginOptions: KeycloakInitOptions = {onLoad: 'login-required', checkLoginIframe: true}; + + if (location.origin.indexOf('/localhost:') != -1) { + console.log('[Microcks launch] Running locally so disabling Keycloak checkLogin Iframe to respect modern browser restrictions'); + loginOptions.checkLoginIframe = false; + } + + keycloak.init(loginOptions).then(function(authenticated) { + if (authenticated) { + (window as any).keycloak = keycloak; + bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); + } + }).catch(function() { + console.error('[Microcks launch] Error while initializing Keycloak'); + alert('Failed to initialize authentication subsystem.'); + }); + } else { + console.log('[Microcks launch] Keycloak is disabled so running in dev mode with anonymous authent'); + bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); + } +}); + +function fileLoaded(xhr: any) { + return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:'); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/styles.css b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/styles.css new file mode 100644 index 000000000..ac330ef64 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/src/styles.css @@ -0,0 +1,365 @@ +.breadcrumb-bar { + height: 37px; + border-bottom: 1px solid #ccc; + margin-left: -20px; + margin-right: -20px; + margin-top: -20px; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: transparent; + border-radius: 1px; +} + +small { + font-weight: 400; + font-size: 0.9em; + line-height: 1; + color: #9c9c9c; +} +dl { + margin-bottom: 2em; +} +dd, dt { + line-height: 2; +} +.dl-horizontal.left dt { + text-align: left; +} + +[role=button] { + cursor: pointer; +} + +.control-group { + padding-top: 0.8em; + padding-bottom: 0.8em; +} +.form-actions { + padding-top: 2em; +} +.form-actions>button { + margin-left: 5px; +} + +span.card-pf-footer-text { + font-size: 1em !important; + vertical-align: text-top; +} + +.pf-icon-sm { + line-height: 26px; + border-radius: 50%; + font-size: 1.4em; + height: 30px; + line-height: 30px; + width: 30px; +} +.rest-icon-sm { + border: 3px solid #89bf04 !important; +} +.soap-icon-sm { + border: 3px solid #39a5dc !important; +} +.event-icon-sm { + border: 3px solid #ec7a08 !important; +} +.grpc-icon-sm { + border: 3px solid #379c9c !important; +} +.graph-icon-sm { + border: 3px solid #e10098 !important; +} +.generic-icon-sm { + border: 3px solid #9c27b0 !important; +} + +.label-grpc { + background-color: #379c9c; +} +.label-graph { + background-color: #e10098; +} +.label-gene { + background-color: #9c27b0; +} + +.section-label { + text-transform: uppercase; + color: rgb(117, 117, 117); + margin-top: 2em; + margin-bottom: 1em; +} + +.no-margin-top { + margin-top: 0.2em !important; +} + +.subsection-label { + color: rgb(117, 117, 117); + border-bottom: 1px solid rgb(220, 220, 220); + padding-bottom: 10px; + margin-top: 1em; +} + +.learn-more-inline { + display: inline; + font-size: 11px; + font-weight: 400; + margin-left: 5px; +} + +.test-box-container { + display: inline-block; +} +.bar { + border: 1px; + border-bottom: 1px solid #CCC; + margin: 5px; +} +.bar-success { + background: #7BB33D; +} +.bar-success:hover { + background: #73A839; +} +.bar-failure { + background: #dd1f26; +} +.bar-failure:hover { + background: #d41e24; +} + +#dayInvocationsBarChart { + padding-left: 20px; +} +#dayInvocationsBarChart >* .domain { + display: none; +} +#dayInvocationsBarChart >* .axis line { + stroke-width: 1px; + stroke: #eee; + shape-rendering: crispedges; +} +#dayInvocationsBarChart >* .axis text { + fill: #888; +} +#hourInvocationsBarChart { + padding-right: 20px; +} +#hourInvocationsBarChart >* .domain { + display: none; +} +#hourInvocationsBarChart >* .axis line { + stroke-width: 1px; + stroke: #eee; + shape-rendering: crispedges; +} +#hourInvocationsBarChart >* .axis text { + fill: #888; +} +.invocations-bar > rect { + fill: #0088ce; + fill-opacity: 0.7; +} +.invocations-bar > rect:hover { + fill-opacity: 1; +} + +.nav-tabs>li.second-tab:first-child>a { + --padding-left: 0; +} + +.nav-tabs>li.second-tab>a { + border: 0; + line-height: 1; + margin-right: 0; + padding-bottom: 10px; + padding-top: 10px; +} +.nav-tabs>li.second-tab.active>a, .nav-tabs>li.second-tab.active>a:active, .nav-tabs>li.second-tab.active>a:focus, .nav-tabs>li.second-tab.active>a:hover { + background-color: transparent; + border: 0!important; + color: #0088ce; +} + +.nav-tabs>li.second-tab.active>a:before, .nav-tabs>li.second-tab.active>a:focus:before, .nav-tabs>li.second-tab.active>a:hover:before { + background: #0088ce; + bottom: -1px; + content: ""; + display: block; + height: 2px; + left: 15px; + position: absolute; + right: 15px; +} +.nav-tabs>li.second-tab>a:active:before, .nav-tabs>li.second-tab>a:focus:before, .nav-tabs>li.second-tab>a:hover:before { + background: #bbb; + bottom: -1px; + content: ""; + display: block; + height: 2px; + left: 15px; + position: absolute; + right: 15px; +} + +ul.unstyled { + list-style-type: none; + padding-inline-start: 0px; +} + +.rect-defocused { + opacity: 0.3 !important; +} + +.tooltip-break { + word-break: break-word; +} + +/* You can add global styles to this file, and also import other style files */ + +pre.hljs { + overflow-x: auto; + padding: 0.5em; +} + +/* Missing from patternfly.css to ensure compatibility with ngx-bootstrap */ +/* It was .modal-backdrop.in before */ +.modal-backdrop.show { + opacity: .5; +} +.modal-backdrop.fade.show { + opacity: .5; +} + +.fade.show { + opacity: 1; +} +.modal.show .modal-dialog { + transform: translate(0, 0); +} + +/** Missing from patterfly-ng.css that is no longer imported */ +.toolbar-pf .form-group:last-child { + border-right: 0; + margin-bottom: 0; + padding-right: 0; +} + +.toolbar-pf-actions .toolbar-apf-filter { + padding-left: 0 !important; +} +.toolbar-pf-actions .btn { + min-width: unset; +} + +@media (min-width: 768px) { + .toolbar-pf-actions .toolbar-apf-filter { + padding-left: 0; + } +} + +.filter-pf.filter-fields .form-group { + padding-left: 0; + width: 275px; +} +.filter-pf-active-label { + margin-right: 5px; +} + +.filter-select .btn-default { + background-color: #fff; + background-image: none; +} +.filter-select .btn { + border-left: 0; +} + +.bootstrap-select>.dropdown-toggle { + width: 100%; + padding-right: 25px; + z-index: 1; +} +.bootstrap-select.btn-group .dropdown-toggle .filter-option { + display: inline-block; + overflow: hidden; + width: 100%; + text-align: left; +} +.filter-select .btn-default .placeholder { + color: #8b8d8f; + font-style: italic; + font-weight: 400; +} + +.margin-left-10 { + margin-left: 10px !important; +} +.padding-right-10 { + padding-right: 10px !important; +} + +.input-group-btn{ + display: table-cell !important; +} + +/* Angular material icons for the stepper */ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(./lib/fonts/MaterialIcons.woff2) format('woff2'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} + +/** Reajust the Material elements style on Patternly */ +.mat-stepper-horizontal { + background-color: #fff !important; + font-family: "Open Sans", Helvetica, Arial, sans-serif !important; +} +.mat-stepper-horizontal-line { + border-top-width: 1px; + border-top-style: solid; + flex: auto; + height: 0; + margin: 0 -16px; + min-width: 32px; + border-top-color: #bbb; +} +.mat-horizontal-content-container { + padding-top: 3em !important; +} +.mat-step-icon { + background-color: #fff !important; + border-color: #bbb !important; + border-style: solid !important; + border-width: 1px; + color: #bbb !important; +} +.mat-step-icon-selected { + background-color: #39a5dc !important; + color: #fff !important; +} +.mat-step-text-label { + font-family: "Open Sans", Helvetica, Arial, sans-serif; + font-size: 16px; +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.app.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.app.json new file mode 100644 index 000000000..3775b37e3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.json new file mode 100644 index 000000000..5525117c6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.json @@ -0,0 +1,27 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.spec.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.spec.json new file mode 100644 index 000000000..5fb748d92 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/main/webapp/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/MicrocksApplicationFuzz.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/MicrocksApplicationFuzz.java new file mode 100644 index 000000000..1cb75eb4c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/MicrocksApplicationFuzz.java @@ -0,0 +1,144 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.junit.FuzzTest; + +import org.junit.jupiter.api.BeforeEach; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrint; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.utility.DockerImageName; + +import java.io.File; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import static com.code_intelligence.jazzer.junit.SpringFuzzTestHelper.apiTest; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +/** + * @author laurent + */ +@AutoConfigureMockMvc(print = MockMvcPrint.NONE) +@SpringBootTest(classes = MicrocksApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("fuzz") +@TestPropertySource(locations = { "classpath:/config/fuzz.properties" }) +public class MicrocksApplicationFuzz { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(MicrocksApplicationFuzz.class); + + @LocalServerPort + private int port; + + private static final MongoDBContainer mongoDBContainer; + + static { + System.setProperty("JAZZER_FUZZ", "1"); + mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4")).withReuse(false); + mongoDBContainer.start(); + } + + @DynamicPropertySource + public static void setDatasourceProperties(final DynamicPropertyRegistry registry) { + String url = "mongodb://" + mongoDBContainer.getHost() + ":" + mongoDBContainer.getMappedPort(27017) + + "/microcksIT"; + registry.add("spring.data.mongodb.uri", () -> url); + } + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected TestRestTemplate restTemplate; + + private boolean beforeCalled = false; + + @BeforeEach + public void beforeEach() { + beforeCalled = true; + + // Upload PetStore reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/petstore-openapi.json", true); + } + + @FuzzTest(maxDuration = "10s") + public void fuzzVersionInfo(FuzzedDataProvider data) throws Exception { + if (!beforeCalled) { + throw new RuntimeException("BeforeEach was not called"); + } + + String name = data.consumeRemainingAsString(); + apiTest(mockMvc, "/api/version/info", get("/api/version/info").param("name", name)); + } + + @FuzzTest(maxDuration = "10s") + public void fuzzServices(FuzzedDataProvider data) throws Exception { + if (!beforeCalled) { + throw new RuntimeException("BeforeEach was not called"); + } + + int page = data.consumeInt(0, 10); + apiTest(mockMvc, "/api/services", get("/api/services").param("page", String.valueOf(page))); + + String serviceId = data.consumeAsciiString(32); + String encodedServiceId = URLEncoder.encode(serviceId, StandardCharsets.UTF_8); + apiTest(mockMvc, "/api/services/" + encodedServiceId, get("/api/services/" + encodedServiceId)); + } + + /** */ + protected void uploadArtifactFile(String artifactFilePath, boolean isMainArtifact) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", new FileSystemResource(new File(artifactFilePath))); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity response; + if (isMainArtifact) { + response = restTemplate.postForEntity("/api/artifact/upload", requestEntity, String.class); + } else { + response = restTemplate.postForEntity("/api/artifact/upload?mainArtifact=false", requestEntity, String.class); + } + + assertEquals(201, response.getStatusCode().value()); + log.info("Just uploaded: {}", response.getBody()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/listener/DailyStatisticsFeederTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/listener/DailyStatisticsFeederTest.java new file mode 100644 index 000000000..fedc20c9e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/listener/DailyStatisticsFeederTest.java @@ -0,0 +1,84 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import io.github.microcks.domain.DailyStatistic; +import io.github.microcks.event.MockInvocationEvent; +import io.github.microcks.repository.DailyStatisticRepository; +import io.github.microcks.repository.RepositoryTestsConfiguration; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.text.SimpleDateFormat; +import java.util.Calendar; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Test case for DailyStatisticsFeeder class. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = { RepositoryTestsConfiguration.class, ListenerTestsConfiguration.class }) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class DailyStatisticsFeederTest { + + @Autowired + DailyStatisticsFeeder feeder; + + @Autowired + DailyStatisticRepository statisticsRepository; + + @Test + void testOnApplicationEvent() { + Calendar today = Calendar.getInstance(); + MockInvocationEvent event = new MockInvocationEvent(this, "TestService1", "1.0", "123456789", today.getTime(), + 100); + + // Fire event a first time. + feeder.onApplicationEvent(event); + + SimpleDateFormat formater = new SimpleDateFormat("yyyyMMdd"); + String day = formater.format(today.getTime()); + feeder.flushToDatabase(); + DailyStatistic stat = statisticsRepository.findByDayAndServiceNameAndServiceVersion(day, "TestService1", "1.0") + .get(0); + assertNotNull(stat); + assertNotNull(stat.getId()); + assertEquals(day, stat.getDay()); + assertEquals("TestService1", stat.getServiceName()); + assertEquals("1.0", stat.getServiceVersion()); + assertEquals(1, stat.getDailyCount()); + assertEquals(1, stat.getHourlyCount().get(String.valueOf(today.get(Calendar.HOUR_OF_DAY)))); + + // Fire event a second time. + feeder.onApplicationEvent(event); + feeder.flushToDatabase(); + stat = statisticsRepository.findByDayAndServiceNameAndServiceVersion(day, "TestService1", "1.0").get(0); + assertNotNull(stat); + assertNotNull(stat.getId()); + assertEquals(day, stat.getDay()); + assertEquals("TestService1", stat.getServiceName()); + assertEquals("1.0", stat.getServiceVersion()); + assertEquals(2, stat.getDailyCount()); + assertEquals(2, stat.getHourlyCount().get(String.valueOf(today.get(Calendar.HOUR_OF_DAY)))); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/listener/ListenerTestsConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/listener/ListenerTestsConfiguration.java new file mode 100644 index 000000000..875e2d3ad --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/listener/ListenerTestsConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.listener; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * @author laurent + */ +@Configuration +@ComponentScan(basePackages = { "io.github.microcks.listener" }, lazyInit = true) +public class ListenerTestsConfiguration { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/CustomDailyStatisticRepositoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/CustomDailyStatisticRepositoryTest.java new file mode 100644 index 000000000..cea089b05 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/CustomDailyStatisticRepositoryTest.java @@ -0,0 +1,89 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.DailyStatistic; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.data.mongodb.UncategorizedMongoDbException; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.HashMap; +import java.util.Map; + +/** + * Test case for CustomDailyStatisticRepository class. + * @author laurent + */ +@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class CustomDailyStatisticRepositoryTest { + + @Autowired + DailyStatisticRepository repository; + + @BeforeEach + public void setUp() { + // Create a bunch of statistics... + DailyStatistic stat = new DailyStatistic(); + stat.setDay("20140930"); + stat.setServiceName("TestService1"); + stat.setServiceVersion("1.0"); + stat.setDailyCount(2); + stat.setHourlyCount(initializeHourlyMap()); + repository.save(stat); + // with same name and different version ... + stat = new DailyStatistic(); + stat.setDay("20140930"); + stat.setServiceName("TestService1"); + stat.setServiceVersion("1.2"); + stat.setDailyCount(2); + stat.setHourlyCount(initializeHourlyMap()); + repository.save(stat); + } + + @Test + void testAggregateDailyStatistics() { + try { + DailyStatistic stat = repository.aggregateDailyStatistics("20140930"); + } catch (ConverterNotFoundException cvnfe) { + // For now, mapReduce in Fongo is experimental. MapReduce execution is working + // but SpringData cannot convert Fongo Rhino result into Java object + // ("No converter found capable of converting from type org.mozilla.javascript.UniqueTag to type java.lang.Integer") + } catch (UncategorizedMongoDbException ume) { + // For now, mapReduce in Fongo is experimental. MapReduce execution is not working + // ("org.mozilla.javascript.EcmaError: TypeError: Cannot read property "0" from undefined") + } catch (RuntimeException re) { + // For now, mapReduce in Fongo is experimental. MapReduce execution is working + // but SpringData cannot convert Fongo Rhino result into Java object + // ("json can't serialize type : class org.mozilla.javascript.UniqueTag") + } + } + + private Map initializeHourlyMap() { + Map result = new HashMap(24); + for (int i = 0; i < 24; i++) { + result.put(String.valueOf(i), 0); + } + return result; + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/CustomTestConformanceMetricRepositoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/CustomTestConformanceMetricRepositoryTest.java new file mode 100644 index 000000000..1fa38fbed --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/CustomTestConformanceMetricRepositoryTest.java @@ -0,0 +1,73 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.TestConformanceMetric; +import io.github.microcks.domain.WeightedMetricValue; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test case for CustomTestConformanceMetricRepository class. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class CustomTestConformanceMetricRepositoryTest { + + @Autowired + TestConformanceMetricRepository repository; + + @Test + void testAggregateTestMetricCoverage() { + // Save a bunch of coverage metrics. + TestConformanceMetric m1 = new TestConformanceMetric(); + m1.setAggregationLabelValue("domain1"); + m1.setCurrentScore(90); + + TestConformanceMetric m2 = new TestConformanceMetric(); + m2.setAggregationLabelValue("domain1"); + m2.setCurrentScore(70); + + TestConformanceMetric m3 = new TestConformanceMetric(); + m3.setAggregationLabelValue("domain2"); + m3.setCurrentScore(85); + + repository.save(m1); + repository.save(m2); + repository.save(m3); + + // Query domain aggregates and assert. + List domainValues = repository.aggregateTestConformanceMetric(); + + assertEquals(2, domainValues.size()); + assertEquals("domain2", domainValues.get(0).getName()); + assertEquals(1, domainValues.get(0).getWeight()); + assertEquals(85, domainValues.get(0).getValue(), 0); + assertEquals("domain1", domainValues.get(1).getName()); + assertEquals(2, domainValues.get(1).getWeight()); + assertEquals(80, domainValues.get(1).getValue(), 0); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/DailyStatisticRepositoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/DailyStatisticRepositoryTest.java new file mode 100644 index 000000000..5e1cc4457 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/DailyStatisticRepositoryTest.java @@ -0,0 +1,74 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import static org.junit.jupiter.api.Assertions.*; + +import io.github.microcks.domain.DailyStatistic; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Test case for DailyStatisticRepository class. + * @author laurent + */ +@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class DailyStatisticRepositoryTest { + + @Autowired + DailyStatisticRepository repository; + + @BeforeEach + public void setUp() { + // Create a bunch of statistics... + DailyStatistic stat = new DailyStatistic(); + stat.setDay("20140319"); + stat.setServiceName("TestService1"); + stat.setServiceVersion("1.0"); + repository.save(stat); + // with same name and different version ... + stat = new DailyStatistic(); + stat.setDay("20140319"); + stat.setServiceName("TestService1"); + stat.setServiceVersion("1.2"); + repository.save(stat); + } + + @Test + void testFindByDayAndServiceNameAndServiceVersion() { + // Retrieve a stat using theses 3 criteria. + DailyStatistic stat = repository.findByDayAndServiceNameAndServiceVersion("20140319", "TestService1", "1.0") + .get(0); + assertNotNull(stat); + assertNotNull(stat.getId()); + assertEquals("20140319", stat.getDay()); + assertEquals("TestService1", stat.getServiceName()); + assertEquals("1.0", stat.getServiceVersion()); + // Retrieve another stat object. + DailyStatistic otherStat = repository.findByDayAndServiceNameAndServiceVersion("20140319", "TestService1", "1.2") + .get(0); + assertNotNull(otherStat); + assertNotNull(otherStat.getId()); + assertNotEquals(stat.getId(), otherStat.getId()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/GenericResourceRepositoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/GenericResourceRepositoryTest.java new file mode 100644 index 000000000..27023c598 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/GenericResourceRepositoryTest.java @@ -0,0 +1,109 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.GenericResource; +import io.github.microcks.domain.Service; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for DynamicResourceRepository class. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class GenericResourceRepositoryTest { + + @Autowired + GenericResourceRepository repository; + + @Autowired + ServiceRepository serviceRepository; + + @Test + void testCreateGenericResource() { + + // Create a minimal service. + Service service = new Service(); + service.setName("DynamicAPI"); + service.setVersion("1.0"); + serviceRepository.save(service); + + // Define a JSON payload to put into a GenericResource associated to service. + String jsonString = "{ \"foo\": 1234, \"bar\": \"string value\" }"; + GenericResource resource = new GenericResource(); + resource.setServiceId(service.getId()); + resource.setPayload(Document.parse(jsonString)); + + // Save to repository. + GenericResource dynaResource = repository.save(resource); + + // Test some manipulations of the payload Document. + Document payload = dynaResource.getPayload(); + payload.append("id", dynaResource.getId()); + + assertTrue(1234 == payload.getInteger("foo")); + assertEquals("string value", payload.getString("bar")); + assertEquals(dynaResource.getId(), payload.getString("id")); + + // Adding more resources before querying. + jsonString = "{ \"foo\": 1234, \"bar\": \"other value\" }"; + GenericResource resource1 = new GenericResource(); + resource1.setServiceId(service.getId()); + resource1.setPayload(Document.parse(jsonString)); + repository.save(resource1); + + jsonString = "{ \"foo\": 1235, \"bar\": \"other value\" }"; + GenericResource resource2 = new GenericResource(); + resource2.setServiceId(service.getId()); + resource2.setPayload(Document.parse(jsonString)); + repository.save(resource2); + + // Query by example using 1 field. + List dynaResources = repository.findByServiceIdAndJSONQuery(service.getId(), + "{ \"foo\": 1234 }"); + assertEquals(2, dynaResources.size()); + for (GenericResource r : dynaResources) { + assertTrue(resource.getId().equals(r.getId()) || resource1.getId().equals(r.getId())); + } + + // Query by example using 1 other field value. + dynaResources = repository.findByServiceIdAndJSONQuery(service.getId(), "{ \"foo\": 1235 }"); + assertEquals(1, dynaResources.size()); + assertEquals(resource2.getId(), dynaResources.get(0).getId()); + + // Query by example using 2 fields. + dynaResources = repository.findByServiceIdAndJSONQuery(service.getId(), + "{ \"foo\": 1234, \"bar\": \"other value\"}"); + assertEquals(1, dynaResources.size()); + assertEquals(resource1.getId(), dynaResources.get(0).getId()); + + // Query by example using 1 field with complex expression. + dynaResources = repository.findByServiceIdAndJSONQuery(service.getId(), "{ \"foo\": {$gt: 1234, $lt: 1236} }"); + assertEquals(1, dynaResources.size()); + assertEquals(resource2.getId(), dynaResources.get(0).getId()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/RepositoryTestsConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/RepositoryTestsConfiguration.java new file mode 100644 index 000000000..da57ca71f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/RepositoryTestsConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import de.bwaldvogel.mongo.MongoServer; +import de.bwaldvogel.mongo.backend.memory.MemoryBackend; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; + +import javax.annotation.PreDestroy; +import java.net.InetSocketAddress; + +/** + * Spring configuration for repositories tests. + * @author laurent + */ +@TestConfiguration +@ComponentScan(basePackages = { "io.github.microcks.security", "io.github.microcks.service" }) +@EnableMongoRepositories(basePackages = { "io.github.microcks.repository" }) +public class RepositoryTestsConfiguration extends AbstractMongoClientConfiguration { + + private MongoClient client; + private MongoServer server; + + @Override + protected String getDatabaseName() { + return "demo-test"; + } + + @Override + public MongoClient mongoClient() { + // Replacing fongo by mongo-java-server, according to + // https://github.com/fakemongo/fongo/issues/337 + //return new Fongo("mongo-test").getMongo(); + + // Create a server and bind on a random local port + server = new MongoServer(new MemoryBackend()); + InetSocketAddress serverAddress = server.bind(); + //client = new MongoClient(new ServerAddress(serverAddress)); + client = MongoClients.create("mongodb://localhost:" + serverAddress.getPort()); + return client; + } + + @PreDestroy + public void shutdown() { + client.close(); + server.shutdown(); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ResourceRepositoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ResourceRepositoryTest.java new file mode 100644 index 000000000..66b8bd9ca --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ResourceRepositoryTest.java @@ -0,0 +1,75 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test case for ResourceRepository implementation. + * @author laurent + */ +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class ResourceRepositoryTest { + + @Autowired + ResourceRepository repository; + + + @BeforeEach + public void setUp() { + Resource resource = new Resource(); + resource.setName("GitHub OIDC-1.1.4.yaml"); + resource.setType(ResourceType.OPEN_API_SPEC); + resource.setServiceId("6626365f24874269b65c69db"); + resource.setPath("github-oidc-1.1.4-openapi.yaml"); + resource.setMainArtifact(true); + repository.save(resource); + + resource = new Resource(); + resource.setName("GitHub OIDC-1.1.4.yaml"); + resource.setType(ResourceType.OPEN_API_SPEC); + resource.setServiceId("6626365f24874269b65c69db"); + resource.setPath("github-oidc-1.1.4-openapi-metadata.yaml"); + resource.setMainArtifact(false); + repository.save(resource); + } + + @Test + void testFindMainByServiceIdAndType() { + List resources = repository.findMainByServiceId("6626365f24874269b65c69db"); + assertEquals(1, resources.size()); + + Resource resource = resources.get(0); + assertEquals("6626365f24874269b65c69db", resource.getServiceId()); + assertEquals(ResourceType.OPEN_API_SPEC, resource.getType()); + assertEquals("GitHub OIDC-1.1.4.yaml", resource.getName()); + assertEquals("github-oidc-1.1.4-openapi.yaml", resource.getPath()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ServiceRepositoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ServiceRepositoryTest.java new file mode 100644 index 000000000..6711dcd26 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ServiceRepositoryTest.java @@ -0,0 +1,94 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for ServiceRepository implementation. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class ServiceRepositoryTest { + + @Autowired + ServiceRepository repository; + + String serviceId; + + @BeforeEach + public void setUp() { + // Create a bunch of services... + Service service = new Service(); + service.setName("HelloWorld"); + service.setVersion("1.2"); + repository.save(service); + // with same name and different version ... + service = new Service(); + service.setName("HelloWorld"); + service.setVersion("1.1"); + repository.save(service); + // with different name ... + service = new Service(); + service.setName("MyService-hello"); + service.setVersion("1.1"); + repository.save(service); + serviceId = service.getId(); + } + + @Test + void testFindOne() { + Service service = repository.findById(serviceId).orElse(null); + assertNotNull(service); + assertEquals("MyService-hello", service.getName()); + assertEquals("1.1", service.getVersion()); + } + + @Test + void testFindByNameAndVersion() { + Service service = repository.findByNameAndVersion("HelloWorld", "1.2"); + assertNotNull(service); + assertEquals("HelloWorld", service.getName()); + assertEquals("1.2", service.getVersion()); + } + + @Test + void testFindByNameLike() { + List services = repository.findByNameLike("world"); + assertTrue(!services.isEmpty()); + assertEquals(2, services.size()); + for (Service service : services) { + assertEquals("HelloWorld", service.getName()); + } + + services = repository.findByNameLike("Hello"); + assertTrue(!services.isEmpty()); + assertEquals(3, services.size()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ServiceStateRepositoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ServiceStateRepositoryTest.java new file mode 100644 index 000000000..695d41ef1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/ServiceStateRepositoryTest.java @@ -0,0 +1,54 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.repository; + +import io.github.microcks.domain.ServiceState; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for ServiceStateRepository implementation. + * @author laurent + */ +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class ServiceStateRepositoryTest { + + @Autowired + ServiceStateRepository repository; + + @BeforeEach + public void setUp() { + ServiceState status = new ServiceState("azertyuiop", "foo"); + status.setValue("bar"); + repository.save(status); + } + + @Test + void testFindByServiceIdAndKey() { + ServiceState status = repository.findByServiceIdAndKey("azertyuiop", "foo"); + assertEquals("bar", status.getValue()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/TestResultRepositoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/TestResultRepositoryTest.java new file mode 100644 index 000000000..67d8f0ed4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/repository/TestResultRepositoryTest.java @@ -0,0 +1,93 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.repository; + +import io.github.microcks.domain.Service; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestRunnerType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for TestResultRepository implementation. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class TestResultRepositoryTest { + + @Autowired + ServiceRepository serviceRepository; + + @Autowired + TestResultRepository repository; + + String serviceId; + + @BeforeEach + public void setUp() { + // Create a service... + Service service = new Service(); + service.setName("HelloWorld"); + service.setVersion("1.0"); + serviceRepository.save(service); + // Create a bunch of test results for this service. + TestResult testResult = new TestResult(); + testResult.setInProgress(false); + testResult.setRunnerType(TestRunnerType.HTTP); + testResult.setTestNumber(1L); + testResult.setServiceId(service.getId()); + testResult.setTestedEndpoint("http://localhost:8088/HelloWorld"); + repository.save(testResult); + // ... another one. + testResult = new TestResult(); + testResult.setInProgress(false); + testResult.setRunnerType(TestRunnerType.HTTP); + testResult.setTestNumber(2L); + testResult.setServiceId(service.getId()); + testResult.setTestedEndpoint("http://localhost:8088/HelloWorld"); + repository.save(testResult); + serviceId = service.getId(); + } + + @Test + void testFindByService() { + long count = repository.countByServiceId(serviceId); + assertEquals(2, count); + List results = repository.findByServiceId(serviceId); + assertEquals(2, results.size()); + } + + @Test + void testFindLastOnesForService() { + List older = repository.findByServiceId(serviceId, + PageRequest.of(0, 2, Sort.Direction.DESC, "testNumber")); + assertEquals(2, older.size()); + Long newTestNumber = older.get(0).getTestNumber() + 1L; + assertEquals(3L, newTestNumber.longValue()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/KeycloakTokenToUserInfoMapperTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/KeycloakTokenToUserInfoMapperTest.java new file mode 100644 index 000000000..f192c5316 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/KeycloakTokenToUserInfoMapperTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for KeycloakTokenToUserInfoMapper class. + * @author laurent + */ +class KeycloakTokenToUserInfoMapperTest { + + /** + * { "alg": "RS256", "typ": "JWT", "kid": "bMWP9GYcT9PpSPKhkBWD9mOZeHq1YQKHq4JXTaxzd-o" } { "exp": 1695715998, "iat": + * 1695715698, "auth_time": 1695715698, "jti": "dda2e10f-f908-45c9-81a7-088fc80d9b02", "iss": + * "http://localhost:8180/realms/microcks", "aud": "microcks-app", "sub": "e9a5e235-31ac-4bf8-943d-76df95d548a3", + * "typ": "Bearer", "azp": "microcks-app-js", "nonce": "30f09cc0-3cdb-4afc-85b4-36dd9fd498da", "session_state": + * "e892de3b-7054-4967-bbef-677b62c32aa0", "acr": "1", "allowed-origins": [ "http://localhost:8080", + * "http://localhost:4200" ], "resource_access": { "microcks-app": { "roles": [ "manager", "user" ] } }, "scope": + * "openid profile email", "sid": "e892de3b-7054-4967-bbef-677b62c32aa0", "email_verified": false, "name": "Pastry + * Manager", "microcks-groups": [ "/microcks/manager/pastry" ], "preferred_username": "pastry-manager", "given_name": + * "Pastry", "family_name": "Manager" } + */ + private static final String JWT_BEARER = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiTVdQOUdZY1Q5UHBTUEtoa0JXRDltT1plSHExWVFLSHE0SlhUYXh6ZC1vIn0.eyJleHAiOjE2OTU3MTU5OTgsImlhdCI6MTY5NTcxNTY5OCwiYXV0aF90aW1lIjoxNjk1NzE1Njk4LCJqdGkiOiJkZGEyZTEwZi1mOTA4LTQ1YzktODFhNy0wODhmYzgwZDliMDIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgxODAvcmVhbG1zL21pY3JvY2tzIiwiYXVkIjoibWljcm9ja3MtYXBwIiwic3ViIjoiZTlhNWUyMzUtMzFhYy00YmY4LTk0M2QtNzZkZjk1ZDU0OGEzIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibWljcm9ja3MtYXBwLWpzIiwibm9uY2UiOiIzMGYwOWNjMC0zY2RiLTRhZmMtODViNC0zNmRkOWZkNDk4ZGEiLCJzZXNzaW9uX3N0YXRlIjoiZTg5MmRlM2ItNzA1NC00OTY3LWJiZWYtNjc3YjYyYzMyYWEwIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJodHRwOi8vbG9jYWxob3N0OjQyMDAiXSwicmVzb3VyY2VfYWNjZXNzIjp7Im1pY3JvY2tzLWFwcCI6eyJyb2xlcyI6WyJtYW5hZ2VyIiwidXNlciJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiJlODkyZGUzYi03MDU0LTQ5NjctYmJlZi02NzdiNjJjMzJhYTAiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJQYXN0cnkgTWFuYWdlciIsIm1pY3JvY2tzLWdyb3VwcyI6WyIvbWljcm9ja3MvbWFuYWdlci9wYXN0cnkiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoicGFzdHJ5LW1hbmFnZXIiLCJnaXZlbl9uYW1lIjoiUGFzdHJ5IiwiZmFtaWx5X25hbWUiOiJNYW5hZ2VyIn0.Z1F3OjBJl3ko4O4BzpKZ2rcy086_vtNqrHFGw10MZpXmumAk1Yww_gf2yz1KhgjZkxNLxfr_1kEefK223Pi3yYCFboXXWbAtFCb2TztOw9RgZU9Fs1Z4mNCCAkwYVLYG2iQr-TlOje9JMYliptHtm5FRRqF-bfsd0tKWhJRezk_DCxdCTVQ_Hx9fFHY1if9-OiRcKYU7F5XU_yFSDP-P0j6KKqX2lpMuWKOKsfWfdZkoBm02JbSAiCKqLKG8R14d3D-cYkxGnil-QSXIsQqSK8DL7RLKxLKCKykkDunbCx2JBw9MvV1TDmSrEszMF1jj46DpYO036gJV7F0PhKePdg"; + + @Test + void testMap() { + // Prepare a Security Context. + MicrocksJwtConverter converter = new MicrocksJwtConverter(); + Jwt jwt = null; + + try { + JWT parsedJwt = JWTParser.parse(JWT_BEARER); + jwt = MicrocksJwtConverterTest.createJwt(JWT_BEARER, parsedJwt); + } catch (Exception e) { + fail("Parsing Jwt bearer should not fail"); + } + + // Convert and assert granted authorities. + JwtAuthenticationToken authenticationToken = converter.convert(jwt); + SecurityContext context = new SecurityContextImpl(authenticationToken); + + // Execute and assert user data. + UserInfo userInfo = KeycloakTokenToUserInfoMapper.map(context); + + assertEquals("Pastry Manager", userInfo.getName()); + assertEquals("pastry-manager", userInfo.getUsername()); + assertEquals("Pastry", userInfo.getGivenName()); + assertEquals("Manager", userInfo.getFamilyName()); + assertNull(userInfo.getEmail()); + + assertEquals(2, userInfo.getRoles().length); + assertTrue(Arrays.stream(userInfo.getRoles()).toList().contains("user")); + assertTrue(Arrays.stream(userInfo.getRoles()).toList().contains("manager")); + + assertEquals(1, userInfo.getGroups().length); + assertTrue(Arrays.stream(userInfo.getGroups()).toList().contains("/microcks/manager/pastry")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/MicrocksJwtConverterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/MicrocksJwtConverterTest.java new file mode 100644 index 000000000..02e2549b9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/MicrocksJwtConverterTest.java @@ -0,0 +1,92 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a test case for MicrocksJwtConverter class. + * @author laurent + */ +class MicrocksJwtConverterTest { + + /** + * { "alg": "RS256", "typ": "JWT", "kid": "bMWP9GYcT9PpSPKhkBWD9mOZeHq1YQKHq4JXTaxzd-o" } { "exp": 1695715998, "iat": + * 1695715698, "auth_time": 1695715698, "jti": "dda2e10f-f908-45c9-81a7-088fc80d9b02", "iss": + * "http://localhost:8180/realms/microcks", "aud": "microcks-app", "sub": "e9a5e235-31ac-4bf8-943d-76df95d548a3", + * "typ": "Bearer", "azp": "microcks-app-js", "nonce": "30f09cc0-3cdb-4afc-85b4-36dd9fd498da", "session_state": + * "e892de3b-7054-4967-bbef-677b62c32aa0", "acr": "1", "allowed-origins": [ "http://localhost:8080", + * "http://localhost:4200" ], "resource_access": { "microcks-app": { "roles": [ "manager", "user" ] } }, "scope": + * "openid profile email", "sid": "e892de3b-7054-4967-bbef-677b62c32aa0", "email_verified": false, "name": "Pastry + * Manager", "microcks-groups": [ "/microcks/manager/pastry" ], "preferred_username": "pastry-manager", "given_name": + * "Pastry", "family_name": "Manager" } + */ + private static final String jwtBearer = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiTVdQOUdZY1Q5UHBTUEtoa0JXRDltT1plSHExWVFLSHE0SlhUYXh6ZC1vIn0.eyJleHAiOjE2OTU3MTU5OTgsImlhdCI6MTY5NTcxNTY5OCwiYXV0aF90aW1lIjoxNjk1NzE1Njk4LCJqdGkiOiJkZGEyZTEwZi1mOTA4LTQ1YzktODFhNy0wODhmYzgwZDliMDIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgxODAvcmVhbG1zL21pY3JvY2tzIiwiYXVkIjoibWljcm9ja3MtYXBwIiwic3ViIjoiZTlhNWUyMzUtMzFhYy00YmY4LTk0M2QtNzZkZjk1ZDU0OGEzIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibWljcm9ja3MtYXBwLWpzIiwibm9uY2UiOiIzMGYwOWNjMC0zY2RiLTRhZmMtODViNC0zNmRkOWZkNDk4ZGEiLCJzZXNzaW9uX3N0YXRlIjoiZTg5MmRlM2ItNzA1NC00OTY3LWJiZWYtNjc3YjYyYzMyYWEwIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJodHRwOi8vbG9jYWxob3N0OjQyMDAiXSwicmVzb3VyY2VfYWNjZXNzIjp7Im1pY3JvY2tzLWFwcCI6eyJyb2xlcyI6WyJtYW5hZ2VyIiwidXNlciJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiJlODkyZGUzYi03MDU0LTQ5NjctYmJlZi02NzdiNjJjMzJhYTAiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJQYXN0cnkgTWFuYWdlciIsIm1pY3JvY2tzLWdyb3VwcyI6WyIvbWljcm9ja3MvbWFuYWdlci9wYXN0cnkiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoicGFzdHJ5LW1hbmFnZXIiLCJnaXZlbl9uYW1lIjoiUGFzdHJ5IiwiZmFtaWx5X25hbWUiOiJNYW5hZ2VyIn0.Z1F3OjBJl3ko4O4BzpKZ2rcy086_vtNqrHFGw10MZpXmumAk1Yww_gf2yz1KhgjZkxNLxfr_1kEefK223Pi3yYCFboXXWbAtFCb2TztOw9RgZU9Fs1Z4mNCCAkwYVLYG2iQr-TlOje9JMYliptHtm5FRRqF-bfsd0tKWhJRezk_DCxdCTVQ_Hx9fFHY1if9-OiRcKYU7F5XU_yFSDP-P0j6KKqX2lpMuWKOKsfWfdZkoBm02JbSAiCKqLKG8R14d3D-cYkxGnil-QSXIsQqSK8DL7RLKxLKCKykkDunbCx2JBw9MvV1TDmSrEszMF1jj46DpYO036gJV7F0PhKePdg"; + + private static final String DECODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to decode the Jwt: %s"; + + @Test + void testConvert() { + MicrocksJwtConverter converter = new MicrocksJwtConverter(); + Jwt jwt = null; + + try { + JWT parsedJwt = JWTParser.parse(jwtBearer); + jwt = createJwt(jwtBearer, parsedJwt); + } catch (Exception e) { + fail("Parsing Jwt bearer should not fail"); + } + + // Convert and assert granted authorities. + JwtAuthenticationToken authenticationToken = converter.convert(jwt); + + assertTrue(authenticationToken.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_user"))); + assertTrue(authenticationToken.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_manager"))); + } + + public static Jwt createJwt(String token, JWT parsedJwt) { + try { + Map headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); + Map claims = parsedJwt.getJWTClaimsSet().getClaims(); + // See https://github.com/jhipster/generator-jhipster/issues/19309 + Map tempClaims = new LinkedHashMap<>(); + for (String key : claims.keySet()) { + Object value = claims.get(key); + if (key.equals("exp") || key.equals("iat")) { + value = ((Date) value).toInstant(); + } + tempClaims.put(key, value); + } + return Jwt.withTokenValue(token).headers(h -> h.putAll(headers)).claims(c -> c.putAll(tempClaims)).build(); + } catch (Exception ex) { + throw new JwtException( + String.format("An error occurred while attempting to decode the Jwt: %s", "Malformed payload")); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/OAuth2AuthorizeClientProviderTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/OAuth2AuthorizeClientProviderTest.java new file mode 100644 index 000000000..facfe1259 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/security/OAuth2AuthorizeClientProviderTest.java @@ -0,0 +1,146 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.security; + +import io.github.microcks.domain.OAuth2AuthorizedClient; +import io.github.microcks.domain.OAuth2ClientContext; +import io.github.microcks.domain.OAuth2GrantType; + +import dasniko.testcontainers.keycloak.KeycloakContainer; +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for OAuth2AuthorizeClientProvider class. + * @author laurent + */ +@Testcontainers +public class OAuth2AuthorizeClientProviderTest { + + @Container + public static KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:24.0.4") + .withRealmImportFile("io/github/microcks/security/myrealm-realm.json"); + + @Test + void testResourceOwnerPassword() { + OAuth2AuthorizedClientProvider provider = new OAuth2AuthorizedClientProvider(); + + OAuth2ClientContext clientContext = new OAuth2ClientContext(); + clientContext.setClientId("myrealm-test"); + clientContext.setClientSecret("QAzrPEJJeDkjKtePKoXyzkqY6exkBauh"); + clientContext.setTokenUri(keycloak.getAuthServerUrl() + "/realms/myrealm/protocol/openid-connect/token"); + clientContext.setGrantType(OAuth2GrantType.PASSWORD); + clientContext.setUsername("admin"); + clientContext.setPassword("myrealm123"); + + OAuth2AuthorizedClient authorizedClient = null; + try { + authorizedClient = provider.authorize(clientContext); + } catch (AuthorizationException authException) { + fail("No AuthorizationException should be thrown"); + } + + assertEquals("admin", authorizedClient.getPrincipalName()); + assertEquals(keycloak.getAuthServerUrl() + "/realms/myrealm/protocol/openid-connect/token", + authorizedClient.getTokenUri()); + assertEquals(OAuth2GrantType.PASSWORD, authorizedClient.getGrantType()); + assertNotNull(authorizedClient.getEncodedAccessToken()); + } + + @Test + void testClientCredentials() { + OAuth2AuthorizedClientProvider provider = new OAuth2AuthorizedClientProvider(); + + OAuth2ClientContext clientContext = new OAuth2ClientContext(); + clientContext.setClientId("myrealm-serviceaccount"); + clientContext.setClientSecret("ab54d329-e435-41ae-a900-ec6b3fe15c54"); + clientContext.setTokenUri(keycloak.getAuthServerUrl() + "/realms/myrealm/protocol/openid-connect/token"); + clientContext.setGrantType(OAuth2GrantType.CLIENT_CREDENTIALS); + + OAuth2AuthorizedClient authorizedClient = null; + try { + authorizedClient = provider.authorize(clientContext); + } catch (AuthorizationException authException) { + fail("No AuthorizationException should be thrown"); + } + + assertEquals("myrealm-serviceaccount", authorizedClient.getPrincipalName()); + assertEquals(keycloak.getAuthServerUrl() + "/realms/myrealm/protocol/openid-connect/token", + authorizedClient.getTokenUri()); + assertEquals(OAuth2GrantType.CLIENT_CREDENTIALS, authorizedClient.getGrantType()); + assertNotNull(authorizedClient.getEncodedAccessToken()); + } + + @Test + void testClientCredentialsFailure() throws AuthorizationException { + assertThrows(AuthorizationException.class, () -> { + OAuth2AuthorizedClientProvider provider = new OAuth2AuthorizedClientProvider(); + + OAuth2ClientContext clientContext = new OAuth2ClientContext(); + clientContext.setClientId("myrealm-serviceaccount"); + clientContext.setClientSecret("ab54d329-e435-41ae-a900-ec6b3fe15c55"); + clientContext.setTokenUri(keycloak.getAuthServerUrl() + "/realms/myrealm/protocol/openid-connect/token"); + clientContext.setGrantType(OAuth2GrantType.CLIENT_CREDENTIALS); + + OAuth2AuthorizedClient authorizedClient = provider.authorize(clientContext); + }); + } + + @Test + void testRefreshToken() { + // First use standard Spring Security classes to authorize and retrieve a refresh token. + DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient(); + + ClientRegistration registration = ClientRegistration.withRegistrationId("test-idp").clientId("myrealm-test") + .clientSecret("QAzrPEJJeDkjKtePKoXyzkqY6exkBauh") + .tokenUri(keycloak.getAuthServerUrl() + "/realms/myrealm/protocol/openid-connect/token") + .authorizationGrantType(AuthorizationGrantType.PASSWORD).scope("openid").build(); + + OAuth2PasswordGrantRequest request = new OAuth2PasswordGrantRequest(registration, "admin", "myrealm123"); + OAuth2AccessTokenResponse response = client.getTokenResponse(request); + + // Then test our Autho provider with this refresh token. + OAuth2AuthorizedClientProvider provider = new OAuth2AuthorizedClientProvider(); + + OAuth2ClientContext clientContext = new OAuth2ClientContext(); + clientContext.setClientId("myrealm-test"); + clientContext.setClientSecret("QAzrPEJJeDkjKtePKoXyzkqY6exkBauh"); + clientContext.setTokenUri(keycloak.getAuthServerUrl() + "/realms/myrealm/protocol/openid-connect/token"); + clientContext.setGrantType(OAuth2GrantType.REFRESH_TOKEN); + clientContext.setRefreshToken(response.getRefreshToken().getTokenValue()); + + OAuth2AuthorizedClient authorizedClient = null; + try { + authorizedClient = provider.authorize(clientContext); + } catch (AuthorizationException authException) { + fail("No AuthorizationException should be thrown"); + } + + assertEquals("myrealm-test", authorizedClient.getPrincipalName()); + assertEquals(keycloak.getAuthServerUrl() + "/realms/myrealm/protocol/openid-connect/token", + authorizedClient.getTokenUri()); + assertEquals(OAuth2GrantType.REFRESH_TOKEN, authorizedClient.getGrantType()); + assertNotNull(authorizedClient.getEncodedAccessToken()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ImportExportServiceTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ImportExportServiceTest.java new file mode 100644 index 000000000..f5d427fa4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ImportExportServiceTest.java @@ -0,0 +1,154 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.repository.RepositoryTestsConfiguration; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ServiceRepository; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for ImportExportService class. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class ImportExportServiceTest { + + @Autowired + private ImportExportService service; + + @Autowired + private ServiceRepository repository; + + @Autowired + private ResourceRepository resourceRepository; + + private List ids = new ArrayList<>(); + + @BeforeEach + public void setUp() { + // Create a bunch of services... + Service service = new Service(); + service.setName("HelloWorld"); + service.setVersion("1.2"); + repository.save(service); + ids.add(service.getId()); + // with same name and different version ... + service = new Service(); + service.setName("HelloWorld"); + service.setVersion("1.1"); + repository.save(service); + ids.add(service.getId()); + // with different name ... + service = new Service(); + service.setName("MyService-hello"); + service.setVersion("1.1"); + repository.save(service); + ids.add(service.getId()); + + // Create associated resources. + Resource resource = new Resource(); + resource.setServiceId(ids.get(0)); + resource.setName("Resource 1"); + resource.setType(ResourceType.WSDL); + resource.setContent(""); + resourceRepository.save(resource); + } + + @Test + void testExportRepository() { + String result = service.exportRepository(ids, "json"); + + ObjectMapper mapper = new ObjectMapper(); + + // Check that result is a valid JSON object. + JsonNode jsonObj = null; + try { + jsonObj = mapper.readTree(result); + } catch (IOException e) { + fail("No exception should be thrown when parsing Json"); + } + + try { + // Retrieve and assert on services part. + ArrayNode services = (ArrayNode) jsonObj.get("services"); + assertEquals(3, services.size()); + for (int i = 0; i < services.size(); i++) { + JsonNode service = services.get(i); + String name = service.get("name").asText(); + assertTrue("HelloWorld".equals(name) || "MyService-hello".equals(name)); + } + } catch (Exception e) { + fail("Exception while getting services array"); + } + + try { + // Retrieve and assert on resources part. + ArrayNode resources = (ArrayNode) jsonObj.get("resources"); + assertEquals(1, resources.size()); + JsonNode resource = resources.get(0); + assertEquals("Resource 1", resource.get("name").asText()); + assertEquals("", resource.get("content").asText()); + } catch (Exception e) { + fail("Exception while getting resources array"); + } + } + + @Test + void testImportRepository() { + // Setup and export result. + String json = "{\"services\":[{\"name\":\"Imp1\",\"version\":\"1.2\",\"xmlNS\":null,\"type\":null,\"operations\":[],\"id\":25638445759706201468670970602}," + + "{\"name\":\"Imp2\",\"version\":\"1.1\",\"xmlNS\":null,\"type\":null,\"operations\":[],\"id\":25638445759706201468670970603}], " + + "\"resources\":[{\"name\":\"Resource 1\",\"content\":\"\",\"type\":\"WSDL\",\"serviceId\":25638445759706201468670970602,\"id\":25638445759706201468670970605}], " + + "\"requests\":[], \"responses\":[]}"; + + boolean result = service.importRepository(json); + + // Get imported service and assert on content. + Service service = repository.findByNameAndVersion("Imp1", "1.2"); + assertNotNull(service); + assertEquals("Imp1", service.getName()); + assertEquals("1.2", service.getVersion()); + assertEquals("25638445759706201468670970602", service.getId().toString()); + + // Get imported resources and assert on content. + List resources = resourceRepository.findByServiceId(service.getId()); + assertEquals(1, resources.size()); + assertEquals("Resource 1", resources.get(0).getName()); + assertEquals("", resources.get(0).getContent()); + assertEquals(ResourceType.WSDL, resources.get(0).getType()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ServiceServiceTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ServiceServiceTest.java new file mode 100644 index 000000000..e88aa553d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ServiceServiceTest.java @@ -0,0 +1,1009 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.service; + +import io.github.microcks.domain.*; +import io.github.microcks.repository.GenericResourceRepository; +import io.github.microcks.repository.RepositoryTestsConfiguration; +import io.github.microcks.repository.RequestRepository; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.repository.ResponseRepository; +import io.github.microcks.repository.ServiceRepository; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.EntityAlreadyExistsException; +import io.github.microcks.util.IdBuilder; +import io.github.microcks.util.MockRepositoryImportException; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.io.File; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for ServiceService class. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class ServiceServiceTest { + + @Autowired + private ServiceService service; + + @Autowired + private ServiceRepository repository; + + @Autowired + private ResourceRepository resourceRepository; + + @Autowired + private GenericResourceRepository genericResourceRepository; + + @Autowired + private RequestRepository requestRepository; + + @Autowired + private ResponseRepository responseRepository; + + + @Test + void testImportServiceDefinition() { + List services = null; + try { + File artifactFile = new File("target/test-classes/io/github/microcks/service/weather-forecast-openapi.yaml"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("weather-forecast-openapi.yaml", true)); + } catch (MockRepositoryImportException mrie) { + mrie.printStackTrace(); + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("WeatherForecast API", importedSvc.getName()); + assertEquals("1.0.0", importedSvc.getVersion()); + assertEquals("weather-forecast-openapi.yaml", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(1, importedSvc.getOperations().size()); + assertEquals("GET /forecast/{region}", importedSvc.getOperations().get(0).getName()); + assertEquals(5, importedSvc.getOperations().get(0).getResourcePaths().size()); + + // Inspect and check resources. + List resources = resourceRepository.findByServiceId(importedSvc.getId()); + assertEquals(1, resources.size()); + + Resource resource = resources.get(0); + assertEquals("WeatherForecast API-1.0.0.yaml", resource.getName()); + assertEquals("weather-forecast-openapi.yaml", resource.getSourceArtifact()); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(5, requests.size()); + for (Request request : requests) { + assertEquals("weather-forecast-openapi.yaml", request.getSourceArtifact()); + } + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(5, responses.size()); + for (Response response : responses) { + assertEquals("weather-forecast-openapi.yaml", response.getSourceArtifact()); + } + } + + @Test + void testImportServiceDefinitionFromGitLabURL() { + List services = null; + try { + services = service.importServiceDefinition( + "https://gitlab.com/api/v4/projects/53583367/repository/files/complex-example%2Fopenapi.yaml/raw?head=main", + null, true, true); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("OpenAPI Car API", importedSvc.getName()); + assertEquals("1.0.0", importedSvc.getVersion()); + assertEquals("openapi.yaml", importedSvc.getSourceArtifact()); + + // Inspect and check resources. + List resources = resourceRepository.findByServiceId(importedSvc.getId()); + assertEquals(10, resources.size()); + + // Now inspect operations. + assertEquals(1, importedSvc.getOperations().size()); + assertEquals("GET /owner/{owner}/car", importedSvc.getOperations().get(0).getName()); + assertEquals(DispatchStyles.URI_PARTS, importedSvc.getOperations().get(0).getDispatcher()); + assertEquals(3, importedSvc.getOperations().get(0).getResourcePaths().size()); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(3, requests.size()); + for (Request request : requests) { + assertEquals("openapi.yaml", request.getSourceArtifact()); + } + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(3, responses.size()); + for (Response response : responses) { + assertEquals("openapi.yaml", response.getSourceArtifact()); + switch (response.getName()) { + case "laurent": + assertEquals("/owner=0", response.getDispatchCriteria()); + assertEquals("[{\"model\":\"BMW X5\",\"year\":2018},{\"model\":\"Tesla Model 3\",\"year\":2020}]", + response.getContent()); + break; + case "maxime": + assertEquals("/owner=1", response.getDispatchCriteria()); + assertEquals("[{\"model\":\"Volkswagen Golf\",\"year\":2017}]", response.getContent()); + break; + case "NOT_FOUND": + assertEquals("/owner=999999999", response.getDispatchCriteria()); + assertEquals("{\"error\":\"Could not find owner\"}", response.getContent()); + break; + default: + fail("Unknown response message"); + } + } + } + + @Test + void testImportServiceDefinitionFromGitLabURL2() { + List resourceNames = List.of("OpenAPI Car API-1.0.0.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--path.yaml", + "OpenAPI Car API-1.0.0-paths-components-schemas-Error.yaml", + "OpenAPI Car API-1.0.0-paths-components-schemas-Owner.yaml", + "OpenAPI Car API-1.0.0-paths-components-schemas-Car.yaml", + "OpenAPI Car API-1.0.0-paths-components-parameters-path-owner.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--get-404-response.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--get-404-examples-NOT_FOUND.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--get-200-response.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--get-200-examples-maxime.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--get-200-examples-laurent.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--car-path.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--car-get-404-response.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--car-get-404-examples-NOT_FOUND.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--car-get-200-response.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--car-get-200-examples-maxime.yaml", + "OpenAPI Car API-1.0.0-paths-owner--owner--car-get-200-examples-laurent.yaml"); + List services = null; + try { + services = service.importServiceDefinition( + "https://gitlab.com/api/v4/projects/53583367/repository/files/complex-example-2%2Fopenapi.yaml/raw?head=main", + null, true, true); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("OpenAPI Car API", importedSvc.getName()); + assertEquals("1.0.0", importedSvc.getVersion()); + assertEquals("openapi.yaml", importedSvc.getSourceArtifact()); + + // Inspect and check resources. + List resources = resourceRepository.findByServiceId(importedSvc.getId()); + assertEquals(resourceNames.size(), resources.size()); + for (Resource resource : resources) { + assertEquals("openapi.yaml", resource.getSourceArtifact()); + assertTrue(resourceNames.contains(resource.getName())); + } + + // Now inspect operations. + assertEquals(2, importedSvc.getOperations().size()); + for (Operation operation : importedSvc.getOperations()) { + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals(3, operation.getResourcePaths().size()); + + // Inspect and check requests and responses. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, operation)); + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, operation)); + + switch (operation.getName()) { + case "GET /owner/{owner}": + for (Response response : responses) { + assertEquals("openapi.yaml", response.getSourceArtifact()); + switch (response.getName()) { + case "laurent": + assertEquals("/owner=0", response.getDispatchCriteria()); + assertEquals("{\"name\":\"Laurent\"}", response.getContent()); + break; + case "maxime": + assertEquals("/owner=1", response.getDispatchCriteria()); + assertEquals("{\"name\":\"Maxime\"}", response.getContent()); + break; + case "NOT_FOUND": + assertEquals("/owner=999999999", response.getDispatchCriteria()); + assertEquals("{\"error\":\"Could not find owner\"}", response.getContent()); + break; + default: + fail("Unknown response message"); + } + } + break; + case "GET /owner/{owner}/car": + for (Response response : responses) { + assertEquals("openapi.yaml", response.getSourceArtifact()); + switch (response.getName()) { + case "laurent": + assertEquals("/owner=0", response.getDispatchCriteria()); + assertEquals( + "[{\"model\":\"BMW X5\",\"year\":2018},{\"model\":\"Tesla Model 3\",\"year\":2020}]", + response.getContent()); + break; + case "maxime": + assertEquals("/owner=1", response.getDispatchCriteria()); + assertEquals("[{\"model\":\"Volkswagen Golf\",\"year\":2017}]", response.getContent()); + break; + case "NOT_FOUND": + assertEquals("/owner=999999999", response.getDispatchCriteria()); + assertEquals("{\"error\":\"Could not find owner\"}", response.getContent()); + break; + default: + fail("Unknown response message"); + } + } + break; + default: + fail("Unknown operation"); + } + } + + } + + @Test + void testImportServiceDefinitionMainAndSecondary() { + List services = null; + try { + File artifactFile = new File( + "target/test-classes/io/github/microcks/service/weather-forecast-raw-openapi.yaml"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("weather-forecast-raw-openapi.yaml", true)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("WeatherForecast API", importedSvc.getName()); + assertEquals("1.1.0", importedSvc.getVersion()); + assertEquals("weather-forecast-raw-openapi.yaml", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(1, importedSvc.getOperations().size()); + assertNull(importedSvc.getOperations().get(0).getResourcePaths()); + + // Inspect and check resources. + List resources = resourceRepository.findByServiceId(importedSvc.getId()); + assertEquals(1, resources.size()); + + Resource resource = resources.get(0); + assertEquals("WeatherForecast API-1.1.0.yaml", resource.getName()); + assertEquals("weather-forecast-raw-openapi.yaml", resource.getSourceArtifact()); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, requests.size()); + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, responses.size()); + + try { + File artifactFile = new File("target/test-classes/io/github/microcks/service/weather-forecast-postman.json"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("weather-forecast-postman.json", false)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes. + importedSvc = services.get(0); + assertEquals("WeatherForecast API", importedSvc.getName()); + assertEquals("1.1.0", importedSvc.getVersion()); + assertEquals("weather-forecast-raw-openapi.yaml", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(1, importedSvc.getOperations().size()); + assertEquals(DispatchStyles.URI_ELEMENTS, importedSvc.getOperations().get(0).getDispatcher()); + assertEquals(5, importedSvc.getOperations().get(0).getResourcePaths().size()); + + // Inspect and check resources. + resources = resourceRepository.findByServiceId(importedSvc.getId()); + assertEquals(2, resources.size()); + + for (Resource resourceItem : resources) { + switch (resourceItem.getType()) { + case OPEN_API_SPEC: + assertEquals("WeatherForecast API-1.1.0.yaml", resourceItem.getName()); + assertEquals("weather-forecast-raw-openapi.yaml", resourceItem.getSourceArtifact()); + break; + case POSTMAN_COLLECTION: + assertEquals("WeatherForecast API-1.1.0.json", resourceItem.getName()); + assertEquals("weather-forecast-postman.json", resourceItem.getSourceArtifact()); + break; + default: + fail("Unexpected resource type: " + resourceItem.getType()); + } + } + + // Inspect and check requests. + requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(5, requests.size()); + for (Request request : requests) { + assertEquals("weather-forecast-postman.json", request.getSourceArtifact()); + } + + // Inspect and check responses. + responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(5, requests.size()); + for (Response response : responses) { + assertEquals("weather-forecast-postman.json", response.getSourceArtifact()); + } + } + + @Test + void testImportServiceDefinitionMainAndSecondarySoapUI() { + List services = null; + try { + File artifactFile = new File( + "target/test-classes/io/github/microcks/service/custom-service-primary-openapi.json"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("custom-service-primary-openapi.json", true)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("custom-service", importedSvc.getName()); + assertEquals("1.0.0", importedSvc.getVersion()); + assertEquals("custom-service-primary-openapi.json", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(2, importedSvc.getOperations().size()); + assertNotNull(importedSvc.getOperations().get(0).getResourcePaths()); + + // Inspect and check resources. + List resources = resourceRepository.findByServiceId(importedSvc.getId()); + assertEquals(1, resources.size()); + + Resource resource = resources.get(0); + assertEquals("custom-service-1.0.0.json", resource.getName()); + assertEquals("custom-service-primary-openapi.json", resource.getSourceArtifact()); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, requests.size()); + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, responses.size()); + + try { + File artifactFile = new File( + "target/test-classes/io/github/microcks/service/custom-service-secondary-soapui.xml"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("custom-service-secondary-soapui.xml", false)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes to check they didn't change. + importedSvc = services.get(0); + assertEquals("custom-service", importedSvc.getName()); + assertEquals("1.0.0", importedSvc.getVersion()); + assertEquals("custom-service-primary-openapi.json", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(2, importedSvc.getOperations().size()); + assertNull(importedSvc.getOperations().get(0).getDispatcher()); + assertEquals(1, importedSvc.getOperations().get(0).getResourcePaths().size()); + + // Inspect and check resources. + resources = resourceRepository.findByServiceId(importedSvc.getId()); + assertEquals(2, resources.size()); + + for (Resource resourceItem : resources) { + switch (resourceItem.getType()) { + case OPEN_API_SPEC: + assertEquals("custom-service-1.0.0.json", resourceItem.getName()); + assertEquals("custom-service-primary-openapi.json", resourceItem.getSourceArtifact()); + break; + case SOAP_UI_PROJECT: + assertEquals("custom-service-1.0.0.xml", resourceItem.getName()); + assertEquals("custom-service-secondary-soapui.xml", resourceItem.getSourceArtifact()); + break; + default: + fail("Unexpected resource type: " + resourceItem.getType()); + } + } + + Operation operation = importedSvc.getOperations().stream() + .filter(op -> "GET /secondary/mediatypetextplain/example01".equals(op.getName())).findFirst().orElse(null); + assertNotNull(operation); + + // Inspect and check requests. + requests = requestRepository.findByOperationId(IdBuilder.buildOperationId(importedSvc, operation)); + assertEquals(1, requests.size()); + for (Request request : requests) { + assertEquals("custom-service-secondary-soapui.xml", request.getSourceArtifact()); + } + + // Inspect and check responses. + responses = responseRepository.findByOperationId(IdBuilder.buildOperationId(importedSvc, operation)); + assertEquals(1, requests.size()); + for (Response response : responses) { + assertEquals("custom-service-secondary-soapui.xml", response.getSourceArtifact()); + } + } + + @Test + void testImportServiceDefinitionMainGraphQLAndSecondaryPostman() { + List services = null; + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/graphql/films.graphql"); + services = service.importServiceDefinition(artifactFile, null, new ArtifactInfo("films.graphql", true)); + } catch (MockRepositoryImportException mrie) { + mrie.printStackTrace(); + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("Movie Graph API", importedSvc.getName()); + assertEquals("1.0", importedSvc.getVersion()); + assertEquals("films.graphql", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(4, importedSvc.getOperations().size()); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, requests.size()); + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, responses.size()); + + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/graphql/films-postman.json"); + services = service.importServiceDefinition(artifactFile, null, new ArtifactInfo("films-postman.json", false)); + } catch (MockRepositoryImportException mrie) { + mrie.printStackTrace(); + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes. + importedSvc = services.get(0); + assertEquals("Movie Graph API", importedSvc.getName()); + assertEquals("1.0", importedSvc.getVersion()); + assertEquals("films.graphql", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(4, importedSvc.getOperations().size()); + + // Inspect and check requests. + requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(1, requests.size()); + for (Request request : requests) { + assertEquals("films-postman.json", request.getSourceArtifact()); + } + + // Inspect and check responses. + responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(1, requests.size()); + for (Response response : responses) { + assertEquals("films-postman.json", response.getSourceArtifact()); + } + } + + @Test + void testImportServiceDefinitionMainGraphQLAndSecondaryExamples() { + List services = null; + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/graphql/films.graphql"); + services = service.importServiceDefinition(artifactFile, null, new ArtifactInfo("films.graphql", true)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("Movie Graph API", importedSvc.getName()); + assertEquals("1.0", importedSvc.getVersion()); + assertEquals("films.graphql", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(4, importedSvc.getOperations().size()); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, requests.size()); + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, responses.size()); + + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/graphql/films-1.0-examples.yml"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("films-1.0-examples.yml", false)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes. + importedSvc = services.get(0); + assertEquals("Movie Graph API", importedSvc.getName()); + assertEquals("1.0", importedSvc.getVersion()); + assertEquals("films.graphql", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(4, importedSvc.getOperations().size()); + + // Inspect and check requests. + requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(1, requests.size()); + for (Request request : requests) { + assertEquals("films-1.0-examples.yml", request.getSourceArtifact()); + } + + // Inspect and check responses. + responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(1, requests.size()); + for (Response response : responses) { + assertEquals("films-1.0-examples.yml", response.getSourceArtifact()); + } + + // Check that dispatch criteria have been correctly extracted. + for (Operation operation : importedSvc.getOperations()) { + if ("film".equals(operation.getName())) { + requests = requestRepository.findByOperationId(IdBuilder.buildOperationId(importedSvc, operation)); + responses = responseRepository.findByOperationId(IdBuilder.buildOperationId(importedSvc, operation)); + + assertEquals(2, requests.size()); + assertEquals(2, responses.size()); + + for (Response response : responses) { + assertTrue("?id=ZmlsbXM6MQ==".equals(response.getDispatchCriteria()) + || "?id=ZmlsbXM6Mg==".equals(response.getDispatchCriteria())); + } + } + } + } + + @Test + void testImportServiceDefinitionMainGrpcAndSecondaryPostman() { + List services = null; + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto"); + services = service.importServiceDefinition(artifactFile, null, new ArtifactInfo("hello-v1.proto", true)); + } catch (MockRepositoryImportException mrie) { + mrie.printStackTrace(); + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("io.github.microcks.grpc.hello.v1.HelloService", importedSvc.getName()); + assertEquals("v1", importedSvc.getVersion()); + assertEquals("hello-v1.proto", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(1, importedSvc.getOperations().size()); + + // As operation as only scalar type, it should be QUERY_ARGS dispatcher. + assertEquals(DispatchStyles.QUERY_ARGS, importedSvc.getOperations().get(0).getDispatcher()); + assertEquals("firstname && lastname", importedSvc.getOperations().get(0).getDispatcherRules()); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, requests.size()); + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, responses.size()); + + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/grpc/HelloService.postman.json"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("HelloService.postman.json", false)); + } catch (MockRepositoryImportException mrie) { + mrie.printStackTrace(); + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes. + importedSvc = services.get(0); + assertEquals("io.github.microcks.grpc.hello.v1.HelloService", importedSvc.getName()); + assertEquals("v1", importedSvc.getVersion()); + assertEquals("hello-v1.proto", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(1, importedSvc.getOperations().size()); + + // Inspect and check requests. + requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(2, requests.size()); + for (Request request : requests) { + assertEquals("HelloService.postman.json", request.getSourceArtifact()); + } + + // Inspect and check responses. + responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(2, requests.size()); + for (Response response : responses) { + assertEquals("HelloService.postman.json", response.getSourceArtifact()); + if ("Laurent".equals(response.getName())) { + assertEquals("?firstname=Laurent?lastname=Broudoux", response.getDispatchCriteria()); + } else if ("Philippe".equals(response.getName())) { + assertEquals("?firstname=Philippe?lastname=Huet", response.getDispatchCriteria()); + } else { + fail("Unexpected response name: " + response.getName()); + } + } + } + + @Test + void testImportServiceDefinitionMainGrpcAndSecondaryExamples() { + List services = null; + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto"); + services = service.importServiceDefinition(artifactFile, null, new ArtifactInfo("hello-v1.proto", true)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + assertNotNull(services); + assertEquals(1, services.size()); + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("io.github.microcks.grpc.hello.v1.HelloService", importedSvc.getName()); + assertEquals("v1", importedSvc.getVersion()); + assertEquals("hello-v1.proto", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(1, importedSvc.getOperations().size()); + + // As operation as only scalar type, it should be QUERY_ARGS dispatcher. + assertEquals(DispatchStyles.QUERY_ARGS, importedSvc.getOperations().get(0).getDispatcher()); + assertEquals("firstname && lastname", importedSvc.getOperations().get(0).getDispatcherRules()); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, requests.size()); + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(0, responses.size()); + + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/grpc/hello-v1-examples.yml"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("hello-v1-examples.yml", false)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes. + importedSvc = services.get(0); + assertEquals("io.github.microcks.grpc.hello.v1.HelloService", importedSvc.getName()); + assertEquals("v1", importedSvc.getVersion()); + assertEquals("hello-v1.proto", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(1, importedSvc.getOperations().size()); + + // Inspect and check requests. + requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(2, requests.size()); + for (Request request : requests) { + assertEquals("hello-v1-examples.yml", request.getSourceArtifact()); + } + + // Inspect and check responses. + responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, importedSvc.getOperations().get(0))); + assertEquals(2, requests.size()); + for (Response response : responses) { + assertEquals("hello-v1-examples.yml", response.getSourceArtifact()); + if ("Laurent".equals(response.getName())) { + assertEquals("?firstname=Laurent?lastname=Broudoux", response.getDispatchCriteria()); + } else if ("John".equals(response.getName())) { + assertEquals("?firstname=John?lastname=Doe", response.getDispatchCriteria()); + } else { + fail("Unexpected response name: " + response.getName()); + } + } + } + + @Test + void testImportServiceDefinitionMainAndSecondariesWithAPIMetadata() { + List services = null; + try { + File artifactFile = new File( + "target/test-classes/io/github/microcks/service/weather-forecast-raw-openapi.yaml"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("weather-forecast-raw-openapi.yaml", true)); + } catch (MockRepositoryImportException mrie) { + mrie.printStackTrace(); + fail("No MockRepositoryImportException should have be thrown"); + } + + try { + File artifactFile = new File("target/test-classes/io/github/microcks/service/weather-forecast-postman.json"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("weather-forecast-postman.json", false)); + } catch (MockRepositoryImportException mrie) { + mrie.printStackTrace(); + fail("No MockRepositoryImportException should have be thrown"); + } + + try { + File artifactFile = new File("target/test-classes/io/github/microcks/service/weather-forecast-metadata.yaml"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("weather-forecast-metadata.yaml", false)); + } catch (MockRepositoryImportException mrie) { + mrie.printStackTrace(); + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("WeatherForecast API", importedSvc.getName()); + assertEquals("1.1.0", importedSvc.getVersion()); + assertEquals("weather-forecast-raw-openapi.yaml", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + + assertEquals(3, importedSvc.getMetadata().getLabels().size()); + assertEquals("weather", importedSvc.getMetadata().getLabels().get("domain")); + assertEquals("GA", importedSvc.getMetadata().getLabels().get("status")); + assertEquals("Team C", importedSvc.getMetadata().getLabels().get("team")); + + assertEquals(1, importedSvc.getOperations().size()); + assertEquals(100, importedSvc.getOperations().get(0).getDefaultDelay().longValue()); + assertEquals(DispatchStyles.FALLBACK, importedSvc.getOperations().get(0).getDispatcher()); + assertNotNull(importedSvc.getOperations().get(0).getDispatcherRules()); + assertEquals(5, importedSvc.getOperations().get(0).getResourcePaths().size()); + } + + @Test + void testImportServiceDefinitionMainGraphQLAndSecondaryHAR() { + List services = null; + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/graphql/films.graphql"); + services = service.importServiceDefinition(artifactFile, null, new ArtifactInfo("films.graphql", true)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes. + Service importedSvc = services.get(0); + assertEquals("Movie Graph API", importedSvc.getName()); + assertEquals("1.0", importedSvc.getVersion()); + assertEquals("films.graphql", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(4, importedSvc.getOperations().size()); + + Optional opFilmOperation = importedSvc.getOperations().stream() + .filter(op -> op.getName().equals("film")).findFirst(); + if (opFilmOperation.isEmpty()) { + fail("film operation should have been discovered"); + } + Operation filmOperation = opFilmOperation.get(); + + // Inspect and check requests. + List requests = requestRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, filmOperation)); + assertEquals(0, requests.size()); + + // Inspect and check responses. + List responses = responseRepository + .findByOperationId(IdBuilder.buildOperationId(importedSvc, filmOperation)); + assertEquals(0, responses.size()); + + try { + File artifactFile = new File("target/test-classes/io/github/microcks/util/har/movie-graph-api-1.0.har"); + services = service.importServiceDefinition(artifactFile, null, + new ArtifactInfo("movie-graph-api-1.0.har", false)); + } catch (MockRepositoryImportException mrie) { + fail("No MockRepositoryImportException should have be thrown"); + } + + // Inspect Service own attributes. + importedSvc = services.get(0); + assertEquals("Movie Graph API", importedSvc.getName()); + assertEquals("1.0", importedSvc.getVersion()); + assertEquals("films.graphql", importedSvc.getSourceArtifact()); + assertNotNull(importedSvc.getMetadata()); + assertEquals(4, importedSvc.getOperations().size()); + + // Inspect and check requests. + requests = requestRepository.findByOperationId(IdBuilder.buildOperationId(importedSvc, filmOperation)); + assertEquals(1, requests.size()); + for (Request request : requests) { + assertEquals("movie-graph-api-1.0.har", request.getSourceArtifact()); + } + + // Inspect and check responses. + responses = responseRepository.findByOperationId(IdBuilder.buildOperationId(importedSvc, filmOperation)); + assertEquals(1, requests.size()); + for (Response response : responses) { + assertEquals("movie-graph-api-1.0.har", response.getSourceArtifact()); + } + } + + @Test + void testCreateGenericResourceService() { + Service created = null; + try { + created = service.createGenericResourceService("Order Service", "1.0", "order", null); + } catch (Exception e) { + fail("No exception should be thrown"); + } + + // Check created object. + assertNotNull(created.getId()); + + // Retrieve object by id and assert on what has been persisted. + Service retrieved = repository.findById(created.getId()).orElse(null); + assertEquals("Order Service", retrieved.getName()); + assertEquals("1.0", retrieved.getVersion()); + assertEquals(ServiceType.GENERIC_REST, retrieved.getType()); + + // Now check operations. + assertEquals(5, retrieved.getOperations().size()); + for (Operation op : retrieved.getOperations()) { + if ("POST /order".equals(op.getName())) { + assertEquals("POST", op.getMethod()); + } else if ("GET /order/:id".equals(op.getName())) { + assertEquals("GET", op.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, op.getDispatcher()); + assertEquals("id", op.getDispatcherRules()); + } else if ("GET /order".equals(op.getName())) { + assertEquals("GET", op.getMethod()); + } else if ("PUT /order/:id".equals(op.getName())) { + assertEquals("PUT", op.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, op.getDispatcher()); + assertEquals("id", op.getDispatcherRules()); + } else if ("DELETE /order/:id".equals(op.getName())) { + assertEquals("DELETE", op.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, op.getDispatcher()); + assertEquals("id", op.getDispatcherRules()); + } else { + fail("Unknown operation name: " + op.getName()); + } + } + } + + @Test + void testCreateGenericResourceServiceFailure() { + try { + service.createGenericResourceService("Order Service", "1.0", "order", null); + } catch (Exception e) { + fail("No exception should be raised on first save()!"); + } + assertThrows(EntityAlreadyExistsException.class, () -> { + service.createGenericResourceService("Order Service", "1.0", "order", null); + }); + } + + @Test + void testCreateGenericResourceServiceWithReference() { + Service created = null; + try { + created = service.createGenericResourceService("Order Service", "1.0", "order", + "{\"customerId\": \"123456789\", \"amount\": 12.5}"); + } catch (Exception e) { + fail("No exception should be thrown"); + } + + // Check created object. + assertNotNull(created.getId()); + + // Check that service has created a reference generic resource. + List resources = genericResourceRepository.findReferencesByServiceId(created.getId()); + assertNotNull(resources); + assertEquals(1, resources.size()); + + GenericResource resource = resources.get(0); + assertTrue(resource.isReference()); + assertEquals("123456789", resource.getPayload().get("customerId")); + assertEquals(12.5, resource.getPayload().get("amount")); + } + + @Test + void testCreateGenericEventServiceWithReference() { + Service created = null; + try { + created = service.createGenericEventService("Order Service", "2.0", "order", + "{\"customerId\": \"123456789\",\n \"amount\": 12.5}"); + } catch (Exception e) { + fail("No exception should be thrown"); + } + + // Check created object. + assertNotNull(created.getId()); + + // Retrieve object by id and assert on what has been persisted. + Service retrieved = repository.findById(created.getId()).orElse(null); + assertEquals("Order Service", retrieved.getName()); + assertEquals("2.0", retrieved.getVersion()); + assertEquals(ServiceType.GENERIC_EVENT, retrieved.getType()); + assertEquals(1, retrieved.getOperations().size()); + + List resources = resourceRepository.findByServiceId(retrieved.getId()); + assertEquals(1, resources.size()); + + Resource resource = resources.get(0); + assertEquals("order-asyncapi.yaml", resource.getName()); + assertEquals(ResourceType.ASYNC_API_SPEC, resource.getType()); + assertNotNull(resource.getContent()); + assertTrue(resource.getContent().contains("payload: {\"customerId\": \"123456789\", \"amount\": 12.5}")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ServiceStateStoreTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ServiceStateStoreTest.java new file mode 100644 index 000000000..538288403 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/service/ServiceStateStoreTest.java @@ -0,0 +1,98 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.service; + +import io.github.microcks.domain.ServiceState; +import io.github.microcks.repository.RepositoryTestsConfiguration; +import io.github.microcks.repository.ServiceStateRepository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.Calendar; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for ServiceStateStore class. + * @author laurent + */ +@SpringJUnitConfig(classes = RepositoryTestsConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +class ServiceStateStoreTest { + + private static final String SERVICE_ID = "serviceId"; + + @Autowired + ServiceStateRepository repository; + + ServiceStateStore store; + + @BeforeEach + public void setUp() { + store = new ServiceStateStore(repository, SERVICE_ID); + } + + @Test + void testServiceStateStore() { + // Test empty repo. + String value = store.get("foo"); + assertNull(value); + + // Put one value with default TTL and check. + store.put("foo", "bar"); + ServiceState state = repository.findByServiceIdAndKey(SERVICE_ID, "foo"); + assertEquals("foo", state.getKey()); + assertEquals("bar", state.getValue()); + assertEquals(SERVICE_ID, state.getServiceId()); + + Calendar in9sec = Calendar.getInstance(); + in9sec.add(Calendar.SECOND, 9); + Calendar in11sec = Calendar.getInstance(); + in11sec.add(Calendar.SECOND, 11); + assertTrue(state.getExpireAt().after(in9sec.getTime())); + assertTrue(state.getExpireAt().before(in11sec.getTime())); + + // Test retrieve existing key. + value = store.get("foo"); + assertEquals("bar", value); + + // Update value with non default TTL and check. + store.put("foo", "baz", 3600); + state = repository.findByServiceIdAndKey(SERVICE_ID, "foo"); + assertEquals("foo", state.getKey()); + assertEquals("baz", state.getValue()); + assertEquals(SERVICE_ID, state.getServiceId()); + + Calendar inL1h = Calendar.getInstance(); + inL1h.add(Calendar.SECOND, 3500); + Calendar inG1h = Calendar.getInstance(); + inG1h.add(Calendar.SECOND, 3700); + assertTrue(state.getExpireAt().after(inL1h.getTime())); + assertTrue(state.getExpireAt().before(inG1h.getTime())); + + // Remove key. + store.delete("foo"); + state = repository.findByServiceIdAndKey(SERVICE_ID, "foo"); + assertNull(state); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/task/ImportServiceDefinitionTaskTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/task/ImportServiceDefinitionTaskTest.java new file mode 100644 index 000000000..e18811569 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/task/ImportServiceDefinitionTaskTest.java @@ -0,0 +1,22 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.task; + +/** + * @author laurent + */ +public class ImportServiceDefinitionTaskTest { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/AbsoluteUrlMatcherTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/AbsoluteUrlMatcherTest.java new file mode 100644 index 000000000..e9b599441 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/AbsoluteUrlMatcherTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class AbsoluteUrlMatcherTest { + @ParameterizedTest + @ValueSource(strings = { "https://www.google.com/search?client=firefox-b-e&q=absolute+urls", + "https://github.com/apoorva256/microcks", "file:///Users/unicorn.jpg" }) + void matches_ShouldReturnTrueForAbsoluteUrls(String absoluteUrl) { + assertTrue(AbsoluteUrlMatcher.matches(absoluteUrl)); + } + + @ParameterizedTest + @ValueSource(strings = { "//www.google.com/", "index.html", "../../file.js" }) + void matches_ShouldReturnFalseForNonAbsoluteUrls(String nonAbsoluteUrl) { + assertFalse(AbsoluteUrlMatcher.matches(nonAbsoluteUrl)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/DispatchCriteriaHelperTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/DispatchCriteriaHelperTest.java new file mode 100644 index 000000000..d778f8f8a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/DispatchCriteriaHelperTest.java @@ -0,0 +1,344 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.test.context.NestedTestConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for DispatchCriteriaHelper class. + * @author laurent + */ +class DispatchCriteriaHelperTest { + + @Nested + class ExtractParamsFromURI { + @Test + void testExtractParamsFromURI() { + // Check with parameters. + String requestPath = "/v2/pet/findByStatus?user_key=998bac0775b1d5f588e0a6ca7c11b852&status=available"; + + // Dispatch string params are sorted. + String dispatchCriteria = DispatchCriteriaHelper.extractParamsFromURI(requestPath); + assertEquals("user_key && status", dispatchCriteria); + } + } + + @Nested + class ExtractPartsFromURIs { + @Test + void testExtractPartsFromURIs() { + // Prepare a bunch of uri paths. + List resourcePaths = new ArrayList<>(); + resourcePaths.add("/v2/pet/findByDate/2017"); + resourcePaths.add("/v2/pet/findByDate/2016/12"); + resourcePaths.add("/v2/pet/findByDate/2016/12/20"); + + // Dispatch parts in natural order. + // Should be deduced to something like "/v2/pet/findByDate/{part1}/{part2}/{part3}" + String dispatchCriteria = DispatchCriteriaHelper.extractPartsFromURIs(resourcePaths); + assertEquals("part1 && part2 && part3", dispatchCriteria); + + // 2nd case: variable part is not terminating. + resourcePaths = new ArrayList<>(); + resourcePaths.add("/v2/pet/1/tattoos"); + resourcePaths.add("/v2/pet/23/tattoos"); + + // Should be deduced to something like "/v2/pet/{part1}/tattoos" + dispatchCriteria = DispatchCriteriaHelper.extractPartsFromURIs(resourcePaths); + assertEquals("part1", dispatchCriteria); + } + } + + @Nested + class ExtractPartsFromURIPattern { + @Test + void testExtractPartsFromURIPattern() { + // Check with single URI pattern. + String operationName = "/deployment/byComponent/{component}/{version}?{{param}}"; + + String dispatchCriteria = DispatchCriteriaHelper.extractPartsFromURIPattern(operationName); + assertEquals("component && version", dispatchCriteria); + + // Check with swagger/postman syntax. + operationName = "/deployment/byComponent/:component/:version?{{param}}"; + + dispatchCriteria = DispatchCriteriaHelper.extractPartsFromURIPattern(operationName); + assertEquals("component && version", dispatchCriteria); + + // Check with extra path containing special character. + operationName = "/deployment/byComponent/{component}/{version}/$count"; + + dispatchCriteria = DispatchCriteriaHelper.extractPartsFromURIPattern(operationName); + assertEquals("component && version", dispatchCriteria); + } + } + + @Nested + class ExtractFromURIPattern { + @ParameterizedTest + @CsvSource({ + // Check with parts sorted in natural order. + "/deployment/byComponent/myComp/1.2, /deployment/byComponent/{component}/{version}", + // Check with parts expressed using swagger/postman syntax. + "/deployment/byComponent/myComp/1.2, /deployment/byComponent/:component/:version", + // Check with parts expressed using swagger/postman syntax. + "/deployment/byComponent/myComp/1.2/$count, /deployment/byComponent/:component/:version/$count", }) + void testExtractFromURIPattern(String requestPath, String operationName) { + + final String dispatcherRule = "component && version"; + final String expectedCriteria = "/component=myComp/version=1.2"; + + // Dispatch string parts are sorted. + String dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(dispatcherRule, operationName, + requestPath); + assertEquals(expectedCriteria, dispatchCriteria); + } + + @Test + void testExtractFromURIPattern2() { + String resourcePath = "/pet/2"; + String operationName = "/pet/:petId"; + String paramRule = "petId"; + + String dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(paramRule, operationName, resourcePath); + assertEquals("/petId=2", dispatchCriteria); + + resourcePath = "/order/123456"; + operationName = "/order/:id"; + paramRule = "id"; + + dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(paramRule, operationName, resourcePath); + assertEquals("/id=123456", dispatchCriteria); + } + + @Test + void testUnsorted() { + // Check with parts not sorted in natural order. + String requestPath = "/deployment/byComponent/1.2/myComp"; + String operationName = "/deployment/byComponent/{version}/{component}"; + String paramRule = "version && component"; + + // Dispatch string parts are sorted. + String dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(paramRule, operationName, requestPath); + assertEquals("/component=myComp/version=1.2", dispatchCriteria); + } + + @Test + void testWithExtension() { + // Check with parts sorted in natural order. + String requestPath = "/deployment/byComponent/myComp/1.2.json"; + String operationName = "/deployment/byComponent/{component}/{version}.json"; + String paramRule = "component && version"; + + // Dispatch string parts are sorted. + String dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(paramRule, operationName, requestPath); + assertEquals("/component=myComp/version=1.2", dispatchCriteria); + } + + @ParameterizedTest + @CsvSource({ "component, /component=myComp", "version, /version=1.2", + "component && version, /component=myComp/version=1.2", "component ?? apiKey, /component=myComp" }) + void testWithCustomRule(String paramRule, String expectedCriteria) { + // Check with parts sorted in natural order. + String requestPath = "/deployment/byComponent/myComp/1.2"; + String operationName = "/deployment/byComponent/{component}/{version}"; + + // Dispatch string parts are sorted. + String dispatchCriteria = DispatchCriteriaHelper.extractFromURIPattern(paramRule, operationName, requestPath); + assertEquals(expectedCriteria, dispatchCriteria); + } + } + + @Nested + class ExtractMapFromURIPattern { + @Test + void extractedMapIsFilteredByParamsRuleString() { + String paramsRuleString = "param1 && param3"; + String requestPath = "/deployment/byComponent/first/second/third"; + String operationName = "/deployment/byComponent/{param1}/{param2}/{param3}"; + + var extractedMap = DispatchCriteriaHelper.extractMapFromURIPattern(paramsRuleString, operationName, + requestPath); + assertTrue(extractedMap.keySet().containsAll(Set.of("param1", "param3"))); + assertFalse(extractedMap.containsKey("param2")); + assertEquals("first", extractedMap.get("param1")); + assertEquals("third", extractedMap.get("param3")); + } + + @ParameterizedTest + @CsvSource({ + // Check with parts sorted in natural order. + "/deployment/byComponent/myComp/1.2, /deployment/byComponent/{component}/{version}", + // Check with parts expressed using swagger/postman syntax. + "/deployment/byComponent/myComp/1.2, /deployment/byComponent/:component/:version", + // Check with parts expressed using swagger/postman syntax. + "/deployment/byComponent/myComp/1.2/$count, /deployment/byComponent/:component/:version/$count", }) + void extractedMapContainsAllParamsWithCorrectValues(String requestPath, String operationName) { + var extractedMap = DispatchCriteriaHelper.extractMapFromURIPattern(operationName, requestPath); + assertTrue(extractedMap.keySet().containsAll(Set.of("version", "component"))); + assertEquals("myComp", extractedMap.get("component")); + assertEquals("1.2", extractedMap.get("version")); + } + + @ParameterizedTest + @CsvSource({ "/pet/2, /pet/:petId, petId, 2", "/order/123456, /order/:id, id, 123456", }) + void extractedMapContainsCorrectParamValue(String requestPath, String operationName, String expectedParameter, + String expectedValue) { + var extractedMap = DispatchCriteriaHelper.extractMapFromURIPattern(operationName, requestPath); + assertTrue(extractedMap.containsKey(expectedParameter)); + assertEquals(expectedValue, extractedMap.get(expectedParameter)); + } + + @Test + void extractedMapContainsParamWithExtension() { + String requestPath = "/deployment/byComponent/myComp/1.2.json"; + String operationName = "/deployment/byComponent/{component}/{version}.json"; + + var extractedMap = DispatchCriteriaHelper.extractMapFromURIPattern(operationName, requestPath); + assertTrue(extractedMap.keySet().containsAll(Set.of("version", "component"))); + assertEquals("myComp", extractedMap.get("component")); + assertEquals("1.2", extractedMap.get("version")); + } + } + + @Nested + class ExtractFromURIParams { + @Test + void testExtractFromURIParams() { + // Check with parameters in no particular order. + String requestPath = "/v2/pet/findByDate/2017/01/04?user_key=998bac0775b1d5f588e0a6ca7c11b852&status=available"; + + // Only 1 parameter should be taken into account according to rules. + String dispatchCriteria = DispatchCriteriaHelper.extractFromURIParams("user_key", requestPath); + assertEquals("?user_key=998bac0775b1d5f588e0a6ca7c11b852", dispatchCriteria); + + // 2 parameters should be considered and sorted according to rules. + dispatchCriteria = DispatchCriteriaHelper.extractFromURIParams("user_key && status", requestPath); + assertEquals("?status=available?user_key=998bac0775b1d5f588e0a6ca7c11b852", dispatchCriteria); + } + + @Test + void testExtractFromURIParamsWithEmpty() { + // Check with parameters that allows empty. + String requestPath = "/search?param1=test¶m2=¶m3="; + + // Only 1 parameter should be taken into account according to rules. + String dispatchCriteria = DispatchCriteriaHelper.extractFromURIParams("param1 && param2", requestPath); + assertEquals("?param1=test", dispatchCriteria); + } + } + + @Nested + class BuildFromPartsMap { + @Test + void testBuildFromPartsMap() { + Multimap partsMap = ArrayListMultimap.create(); + partsMap.put("year", "2018"); + partsMap.put("month", "05"); + partsMap.put("year-summary", "true"); + partsMap.put("half-year", "true"); + + // Dispatch string parts are sorted. + String dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap("month && year", partsMap); + assertEquals("/month=05/year=2018", dispatchCriteria); + + // Only 1 parameter should be taken into account according to rules. + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap("year", partsMap); + assertEquals("/year=2018", dispatchCriteria); + + // 2 parameters should be taken into account according to rules with no inclusion of year. + dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap("month && year-summary", partsMap); + assertEquals("/month=05/year-summary=true", dispatchCriteria); + } + } + + @Nested + class BuildFromParamsMap { + @Test + void testBuildFromParamsMap() { + Multimap paramsMap = ArrayListMultimap.create(); + paramsMap.put("page", "1"); + paramsMap.put("limit", "20"); + paramsMap.put("limitation", "20"); + paramsMap.put("status", "available"); + + // Only 1 parameter should be taken into account according to rules. + String dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap("page", paramsMap); + assertEquals("?page=1", dispatchCriteria); + + // 2 parameters should be considered and sorted according to rules. + dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap("page && limit", paramsMap); + assertEquals("?limit=20?page=1", dispatchCriteria); + + // 2 parameters should be considered and sorted according to rules with no inclusion of limit. + dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap("page && limitation", paramsMap); + assertEquals("?limitation=20?page=1", dispatchCriteria); + } + + @Test + void testBuildFromParamsArrayMap() { + Multimap paramsMap = ArrayListMultimap.create(); + paramsMap.put("page", "1"); + paramsMap.put("limit", "20"); + paramsMap.put("limitation", "20"); + paramsMap.put("status", "available"); + paramsMap.put("status", "busy"); + + // 2 parameters should be taken into account, one with two values according to rules. + String dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap("page && status", paramsMap); + assertEquals("?page=1?status=available?status=busy", dispatchCriteria); + + // 1 parameter with two values should be taken into account according to rules + dispatchCriteria = DispatchCriteriaHelper.buildFromParamsMap("status", paramsMap); + assertEquals("?status=available?status=busy", dispatchCriteria); + } + } + + @Nested + class ExtractCommonSuffix { + @Test + void extractCommonSuffix() { + final var uris = List.of("/ab/def", "/cde/def"); + final var result = DispatchCriteriaHelper.extractCommonSuffix(uris); + assertEquals("/def", result); + } + } + + @Nested + class ExtractFromParamsMap { + @Test + void extractFromParamsMap() { + Map paramsMap = Map.of("foo", "fooValue", "bar", "barValue"); + + // Only 1 parameter should be taken into account according to rules. + String dispatchCriteria = DispatchCriteriaHelper.extractFromParamMap("?foo", paramsMap); + assertEquals("?foo=fooValue", dispatchCriteria); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/GitLabReferenceURLBuilderTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/GitLabReferenceURLBuilderTest.java new file mode 100644 index 000000000..0dbbb4e27 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/GitLabReferenceURLBuilderTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for GitLabReferenceURLBuilder. + * @author laurent + */ +class GitLabReferenceURLBuilderTest { + + private static final String BASE_URL = "https://gitlab.com/api/v4/projects/35980862/repository/files/pastry%2FAPI_Pastry_1.0.0-openapi.yaml/raw?ref=main"; + + @Test + void testGetFileName() { + GitLabReferenceURLBuilder builder = new GitLabReferenceURLBuilder(); + + assertEquals("API_Pastry_1.0.0-openapi.yaml", builder.getFileName(BASE_URL, null)); + + Map> headers = Map.of("X-Gitlab-File-Name", List.of("FooBar.yaml")); + assertEquals("FooBar.yaml", builder.getFileName(BASE_URL, headers)); + + headers = Map.of("x-gitlab-file-name", List.of("FooBar.yaml")); + assertEquals("FooBar.yaml", builder.getFileName(BASE_URL, headers)); + } + + @Test + void testBuildRemoteURL() { + GitLabReferenceURLBuilder builder = new GitLabReferenceURLBuilder(); + + assertEquals("https://gitlab.com/api/v4/projects/35980862/repository/files/pastry%2Fschema-ref.yml/raw?ref=main", + builder.buildRemoteURL(BASE_URL, "schema-ref.yml")); + assertEquals("https://gitlab.com/api/v4/projects/35980862/repository/files/pastry%2Fschema-ref.yml/raw?ref=main", + builder.buildRemoteURL(BASE_URL, "./schema-ref.yml")); + assertEquals("https://gitlab.com/api/v4/projects/35980862/repository/files/refs%2Fschema-ref.yml/raw?ref=main", + builder.buildRemoteURL(BASE_URL, "../refs/schema-ref.yml")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/MockRepositoryExporterFactoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/MockRepositoryExporterFactoryTest.java new file mode 100644 index 000000000..1ec624c01 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/MockRepositoryExporterFactoryTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.util.metadata.ExamplesExporter; +import io.github.microcks.util.openapi.OpenAPIOverlayExporter; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for MockRepositoryExporterFactory. + * @author laurent + */ +class MockRepositoryExporterFactoryTest { + + @Test + void testGetMockRepositoryExporter() { + // Check nominal cases. + MockRepositoryExporter exporter = MockRepositoryExporterFactory + .getMockRepositoryExporter(MockRepositoryExporterFactory.API_EXAMPLES_FORMAT); + assertTrue(exporter instanceof ExamplesExporter); + + exporter = MockRepositoryExporterFactory + .getMockRepositoryExporter(MockRepositoryExporterFactory.OPENAPI_OVERLAY_FORMAT); + assertTrue(exporter instanceof OpenAPIOverlayExporter); + + // Check with case-insensitive comparison. + exporter = MockRepositoryExporterFactory.getMockRepositoryExporter("ApIExamPles"); + assertTrue(exporter instanceof ExamplesExporter); + + exporter = MockRepositoryExporterFactory.getMockRepositoryExporter("oas Overlay"); + assertTrue(exporter instanceof OpenAPIOverlayExporter); + + // Check with default. + exporter = MockRepositoryExporterFactory.getMockRepositoryExporter("My exotic unsupported format"); + assertTrue(exporter instanceof ExamplesExporter); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/MockRepositoryImporterFactoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/MockRepositoryImporterFactoryTest.java new file mode 100644 index 000000000..1130c4232 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/MockRepositoryImporterFactoryTest.java @@ -0,0 +1,209 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.util.asyncapi.AsyncAPI3Importer; +import io.github.microcks.util.asyncapi.AsyncAPIImporter; +import io.github.microcks.util.graphql.GraphQLImporter; +import io.github.microcks.util.grpc.ProtobufImporter; +import io.github.microcks.util.har.HARImporter; +import io.github.microcks.util.metadata.MetadataImporter; +import io.github.microcks.util.openapi.OpenAPIImporter; +import io.github.microcks.util.openapi.SwaggerImporter; +import io.github.microcks.util.postman.PostmanCollectionImporter; +import io.github.microcks.util.postman.PostmanWorkspaceCollectionImporter; +import io.github.microcks.util.soapui.SoapUIProjectImporter; + +import java.io.File; + +import org.junit.jupiter.api.Test; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a test case for MockRepositoryImporterFactory. + * @author laurent + */ +class MockRepositoryImporterFactoryTest { + + @Test + void testGetMockRepositoryImporter() { + + // Load a SoapUI file. + File soapUIProject = new File("../samples/HelloService-soapui-project.xml"); + MockRepositoryImporter importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(soapUIProject, null); + } catch (Throwable t) { + fail("Getting importer for SoapUI should not fail!"); + } + assertTrue(importer instanceof SoapUIProjectImporter); + + // Load a Postman file. + File postmanCollection = new File("../samples/PetstoreAPI-collection.json"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(postmanCollection, null); + } catch (Throwable t) { + fail("Getting importer for Postman should not fail!"); + } + assertTrue(importer instanceof PostmanCollectionImporter); + + // Load a Postman Workspace file. + File postmanWorkspaceCollection = new File( + "target/test-classes/io/github/microcks/util/postman/Swagger Petstore.postman_workspace_collection-2.1.json"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(postmanWorkspaceCollection, null); + } catch (Throwable t) { + fail("Getting importer for Postman Workspace should not fail!"); + } + assertTrue(importer instanceof PostmanWorkspaceCollectionImporter); + + // Load an OpenAPI YAML file. + importer = null; + File openAPISpec = new File("target/test-classes/io/github/microcks/util/openapi/cars-openapi.yaml"); + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(openAPISpec, null); + } catch (Throwable t) { + fail("Getting importer for OpenAPI YAML should not fail!"); + } + assertTrue(importer instanceof OpenAPIImporter); + + // Load an OpenAPI JSON file. + openAPISpec = new File("target/test-classes/io/github/microcks/util/openapi/cars-openapi.json"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(openAPISpec, null); + } catch (Throwable t) { + fail("Getting importer for OpenAPI JSON should not fail!"); + } + assertTrue(importer instanceof OpenAPIImporter); + + // Load an OpenAPI JSON oneliner file. + openAPISpec = new File("target/test-classes/io/github/microcks/util/openapi/openapi-oneliner.json"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(openAPISpec, null); + } catch (Throwable t) { + fail("Getting importer for OpenAPI JSON oneliner should not fail!"); + } + assertTrue(importer instanceof OpenAPIImporter); + + // Load an AsyncAPI JSON file. + File asyncAPISpec = new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi.json"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(asyncAPISpec, null); + } catch (Throwable t) { + fail("Getting importer for AsyncAPI JSON should not fail!"); + } + assertTrue(importer instanceof AsyncAPIImporter); + + // Load an AsyncAPI JSON oneliner file. + asyncAPISpec = new File( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-oneliner.json"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(asyncAPISpec, null); + } catch (Throwable t) { + fail("Getting importer for AsyncAPI JSON oneliner should not fail!"); + } + assertTrue(importer instanceof AsyncAPIImporter); + + // Load an AsyncAPI YAML file. + asyncAPISpec = new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(asyncAPISpec, null); + } catch (Throwable t) { + fail("Getting importer for AsyncAPI YAML should not fail!"); + } + assertTrue(importer instanceof AsyncAPIImporter); + + // Load an AsyncAPI v3 YAML file. + asyncAPISpec = new File("target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(asyncAPISpec, null); + } catch (Throwable t) { + fail("Getting importer for AsyncAPI v3 YAML should not fail!"); + } + assertTrue(importer instanceof AsyncAPI3Importer); + + // Load a Protobuf schema file. + File protobufSchema = new File("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(protobufSchema, null); + } catch (Throwable t) { + fail("Getting importer for Protobuf should not fail!"); + } + assertTrue(importer instanceof ProtobufImporter); + + // Load an APIMetadata file. + File apiMetadata = new File("target/test-classes/io/github/microcks/util/metadata/hello-grpc-v1-metadata.yml"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(apiMetadata, null); + } catch (Throwable t) { + fail("Getting importer for APIMetadata should not fail!"); + } + assertTrue(importer instanceof MetadataImporter); + + // Load a GraphQL schema file. + File graphQLSchema = new File("target/test-classes/io/github/microcks/util/graphql/films.graphql"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(graphQLSchema, null); + } catch (Throwable t) { + fail("Getting importer for GraphQL should not fail!"); + } + assertTrue(importer instanceof GraphQLImporter); + + // Load a Swagger v2 YAML file. + File swaggerSpec = new File("target/test-classes/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(swaggerSpec, null); + } catch (Throwable t) { + fail("Getting importer for Swagger v2 YAML should not fail!"); + } + assertTrue(importer instanceof SwaggerImporter); + + // Load a Swagger v2 JSON file. + swaggerSpec = new File("target/test-classes/io/github/microcks/util/openapi/beer-catalog-api-swagger.json"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(swaggerSpec, null); + } catch (Throwable t) { + fail("Getting importer for Swagger v2 JSON should not fail!"); + } + assertTrue(importer instanceof SwaggerImporter); + + // Load a HAR JSON file. + File harFile = new File("target/test-classes/io/github/microcks/util/har/api-pastries-0.0.1.har"); + importer = null; + try { + importer = MockRepositoryImporterFactory.getMockRepositoryImporter(harFile, null); + } catch (IOException ioe) { + fail("Getting importer for HAR JSON file should not fail!"); + } + assertTrue(importer instanceof HARImporter); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/RelativeReferenceURLBuilderFactoryTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/RelativeReferenceURLBuilderFactoryTest.java new file mode 100644 index 000000000..65fd0cf19 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/RelativeReferenceURLBuilderFactoryTest.java @@ -0,0 +1,48 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for RelativeReferenceURLBuilderFactory. + * @author laurent + */ +class RelativeReferenceURLBuilderFactoryTest { + + @Test + void testGetRelativeReferenceURLBuilder() { + RelativeReferenceURLBuilder builder = RelativeReferenceURLBuilderFactory.getRelativeReferenceURLBuilder(null); + assertTrue(builder instanceof SimpleReferenceURLBuilder); + + Map> properties = Map.of("key", List.of("value1", "value2")); + builder = RelativeReferenceURLBuilderFactory.getRelativeReferenceURLBuilder(properties); + assertTrue(builder instanceof SimpleReferenceURLBuilder); + + properties = Map.of(GitLabReferenceURLBuilder.GITLAB_FILE_NAME_HEADER, List.of("value1", "value2")); + builder = RelativeReferenceURLBuilderFactory.getRelativeReferenceURLBuilder(properties); + assertTrue(builder instanceof GitLabReferenceURLBuilder); + + properties = Map.of(GitLabReferenceURLBuilder.GITLAB_FILE_NAME_HEADER.toLowerCase(), List.of("value1", "value2")); + builder = RelativeReferenceURLBuilderFactory.getRelativeReferenceURLBuilder(properties); + assertTrue(builder instanceof GitLabReferenceURLBuilder); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/ResourceUtilTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/ResourceUtilTest.java new file mode 100644 index 000000000..6ea15ab1e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/ResourceUtilTest.java @@ -0,0 +1,150 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.microcks.domain.Service; +import org.junit.jupiter.api.Test; + +import java.io.*; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test for ResourceUtil helper class. + */ +class ResourceUtilTest { + + @Test + void replaceTemplatesInSpecStream() throws Exception { + // Arrange + Service service = new Service(); + service.setName("TestService"); + service.setVersion("1.0"); + + String resource = "TestResource"; + + ObjectMapper mapper = new ObjectMapper(); + JsonNode referenceSchema = mapper.readTree("{\"type\":\"string\"}"); + + String referencePayload = "{\"data\":\"TestPayload\"}"; + + String template = "{service} {version} {resource} {resourceSchema} {reference}"; + InputStream stream = new ByteArrayInputStream(template.getBytes()); + + // Act + String result = ResourceUtil.replaceTemplatesInSpecStream(stream, service, resource, referenceSchema, + referencePayload); + + // Assert + String expected = """ + TestService 1.0 TestResource type: string {"data":"TestPayload"} + """; + assertEquals(expected, result); + } + + @Test + void replaceTemplatesInSpecOpenApi() throws IOException { + // Arrange + Service service = new Service(); + service.setName("Book Service"); + service.setVersion("1.0.0"); + + String resource = "Book"; + + ObjectMapper mapper = new ObjectMapper(); + JsonNode referenceSchema = mapper.readTree(""" + { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "author": { + "type": "string" + } + } + } + """); + + String referencePayload = ""; + + InputStream template = ResourceUtil.getClasspathResource("templates/openapi-3.0.yaml"); + + // Act + String result = ResourceUtil.replaceTemplatesInSpecStream(template, service, resource, referenceSchema, + referencePayload); + + // Assert + String expected = new BufferedReader(new InputStreamReader(Objects.requireNonNull( + Thread.currentThread().getContextClassLoader().getResourceAsStream("filled-templates/openapi-3.0.yaml")))) + .lines().collect(Collectors.joining("\n")); + + + assertEquals(expected, result); + } + + @Test + void replaceTemplatesInSpecAsyncApi() throws IOException { + // Arrange + Service service = new Service(); + service.setName("Book Service"); + service.setVersion("1.0.0"); + + String resource = "Book"; + + ObjectMapper mapper = new ObjectMapper(); + JsonNode referenceSchema = mapper.readTree(""" + { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "author": { + "type": "string" + } + } + } + """); + + String referencePayload = new ObjectMapper().writeValueAsString(mapper.readTree(""" + { + "title": "Example Title", + "author": "Example Author", + "isbn": "Example ISBN" + } + """)); + + + InputStream template = ResourceUtil.getClasspathResource("templates/asyncapi-2.4.yaml"); + + // Act + String result = ResourceUtil.replaceTemplatesInSpecStream(template, service, resource, referenceSchema, + referencePayload); + + // Assert + String expected = new BufferedReader(new InputStreamReader(Objects.requireNonNull( + Thread.currentThread().getContextClassLoader().getResourceAsStream("filled-templates/asyncapi-2.4.yaml")))) + .lines().collect(Collectors.joining("\n")); + + assertEquals(expected, result); + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/SimpleReferenceURLBuilderTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/SimpleReferenceURLBuilderTest.java new file mode 100644 index 000000000..8d0a381f6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/SimpleReferenceURLBuilderTest.java @@ -0,0 +1,51 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for SimpleReferenceURLBuilder. + * @author laurent + */ +class SimpleReferenceURLBuilderTest { + + private static final String BASE_URL = "https://raw.githubusercontent.com/microcks/microcks/main/samples/API_Pastry_1.0.0-openapi.yaml"; + + @Test + void testGetFileName() { + SimpleReferenceURLBuilder builder = new SimpleReferenceURLBuilder(); + assertEquals("API_Pastry_1.0.0-openapi.yaml", builder.getFileName(BASE_URL, null)); + } + + @Test + void testBuildRemoteURL() { + + SimpleReferenceURLBuilder builder = new SimpleReferenceURLBuilder(); + assertEquals("https://raw.githubusercontent.com/microcks/microcks/main/samples/schema-ref.yml", + builder.buildRemoteURL(BASE_URL, "schema-ref.yml")); + assertEquals("https://raw.githubusercontent.com/microcks/microcks/main/samples/schema-ref.yml", + builder.buildRemoteURL(BASE_URL, "./schema-ref.yml")); + assertEquals("https://raw.githubusercontent.com/microcks/microcks/main/refs/schema-ref.yml", + builder.buildRemoteURL(BASE_URL, "../refs/schema-ref.yml")); + assertEquals("https://raw.githubusercontent.com/microcks/microcks/main/refs/sub/schema-ref.yml", + builder.buildRemoteURL(BASE_URL, "../refs/sub/schema-ref.yml")); + assertEquals("https://raw.githubusercontent.com/microcks/microcks/refs/sub/schema-ref.yml", + builder.buildRemoteURL(BASE_URL, "../../refs/sub/schema-ref.yml")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/URIBuilderTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/URIBuilderTest.java new file mode 100644 index 000000000..97dbff072 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/URIBuilderTest.java @@ -0,0 +1,156 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util; + +import io.github.microcks.domain.Parameter; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a Test for URIBuilder class. + * @laurent + */ +class URIBuilderTest { + + @Test + void testBuildURIFromPatternWithNoParameters() { + String pattern = "http://localhost:8080/blog/{year}/{month}"; + try { + String uri = URIBuilder.buildURIFromPattern(pattern, new ArrayList()); + } catch (NullPointerException npe) { + fail("buildURIFromPattern should not fail with no parameters"); + } + } + + @Test + void testBuildURIFromPattern() { + // Prepare a bunch of parameters. + Parameter yearParam = new Parameter(); + yearParam.setName("year"); + yearParam.setValue("2017"); + + Parameter monthParam = new Parameter(); + monthParam.setName("month"); + monthParam.setValue("08"); + + Parameter statusParam = new Parameter(); + statusParam.setName("status"); + statusParam.setValue("published"); + + Parameter pageParam = new Parameter(); + pageParam.setName("page"); + pageParam.setValue("0"); + + List parameters = new ArrayList<>(); + parameters.add(yearParam); + parameters.add(monthParam); + parameters.add(statusParam); + parameters.add(pageParam); + + // Test with old wadl like template format. + String pattern = "http://localhost:8080/blog/{year}/{month}"; + String uri = URIBuilder.buildURIFromPattern(pattern, parameters); + assertTrue("http://localhost:8080/blog/2017/08?page=0&status=published".equals(uri) + || "http://localhost:8080/blog/2017/08?status=published&page=0".equals(uri)); + + // Test with new swagger like template format. + pattern = "http://localhost:8080/blog/:year/:month"; + uri = URIBuilder.buildURIFromPattern(pattern, parameters); + assertTrue("http://localhost:8080/blog/2017/08?page=0&status=published".equals(uri) + || "http://localhost:8080/blog/2017/08?status=published&page=0".equals(uri)); + } + + @Test + void testBuildURIFromPatternWithEncoding() { + // Prepare a bunch of parameters. + Parameter nameParam = new Parameter(); + nameParam.setName("name"); + nameParam.setValue("Eclair Cafe"); + + Parameter descriptionParam = new Parameter(); + descriptionParam.setName("description"); + descriptionParam.setValue("My desc"); + + List parameters = new ArrayList<>(); + parameters.add(nameParam); + parameters.add(descriptionParam); + + // Test with old wadl like template format. + String pattern = "http://localhost:8080/pastry/{name}"; + String uri = URIBuilder.buildURIFromPattern(pattern, parameters); + assertTrue("http://localhost:8080/pastry/Eclair%20Cafe?description=My+desc".equals(uri)); + + // Test with new swagger like template format. + pattern = "http://localhost:8080/pastry/:name"; + uri = URIBuilder.buildURIFromPattern(pattern, parameters); + assertTrue("http://localhost:8080/pastry/Eclair%20Cafe?description=My+desc".equals(uri)); + } + + @Test + void testBuildURIFromPatternWithMap() { + // Prepare a bunch of parameters. + Map parameters = new HashMap<>(); + parameters.put("year", "2018"); + parameters.put("month", "05"); + parameters.put("status", "published"); + parameters.put("page", "0"); + + // Test with old wadl like template format. + String pattern = "http://localhost:8080/blog/{year}/{month}"; + String uri = URIBuilder.buildURIFromPattern(pattern, parameters); + assertTrue("http://localhost:8080/blog/2018/05?page=0&status=published".equals(uri) + || "http://localhost:8080/blog/2018/05?status=published&page=0".equals(uri)); + + // Test with new swagger like template format. + pattern = "http://localhost:8080/blog/:year/:month"; + uri = URIBuilder.buildURIFromPattern(pattern, parameters); + assertTrue("http://localhost:8080/blog/2018/05?page=0&status=published".equals(uri) + || "http://localhost:8080/blog/2018/05?status=published&page=0".equals(uri)); + } + + @Test + void testBuildURIFromPatternWithMapWithParamsArray() { + // Prepare a bunch of parameters. + Multimap parameters = ArrayListMultimap.create(); + parameters.put("year", "2018"); + parameters.put("month", "05"); + parameters.put("status", "published"); + parameters.put("status", "proofred"); + parameters.put("page", "0"); + + // Test with old wadl like template format. + String pattern = "http://localhost:8080/blog/{year}/{month}"; + String uri = URIBuilder.buildURIFromPattern(pattern, parameters); + assertTrue("http://localhost:8080/blog/2018/05?page=0&status=published&status=proofred".equals(uri) + || "http://localhost:8080/blog/2018/05?status=published&status=proofred&page=0".equals(uri)); + + // Test with new swagger like template format. + pattern = "http://localhost:8080/blog/:year/:month"; + uri = URIBuilder.buildURIFromPattern(pattern, parameters); + assertTrue("http://localhost:8080/blog/2018/05?page=0&status=published&status=proofred".equals(uri) + || "http://localhost:8080/blog/2018/05?status=published&status=proofred&page=0".equals(uri)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/ai/AICopilotHelperTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/ai/AICopilotHelperTest.java new file mode 100644 index 000000000..c92f831e7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/ai/AICopilotHelperTest.java @@ -0,0 +1,1068 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.ai; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.DispatchStyles; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * This is a test case for class AICopilotHelper. + * @author laurent + */ +class AICopilotHelperTest { + + @Test + void testParseRequestResponseOutputYaml() { + String aiResponse = """ + - example: 1 + request: + url: /pastries/croissant + headers: + accept: application/json + body: + response: + code: 200 + headers: + content-type: application/json + body: + name: "Croissant" + description: "A flaky, buttery pastry" + size: "L" + price: 2.5 + status: "available" + + - example: 2 + request: + url: /pastries/donut + headers: + accept: application/json + body: + response: + code: 200 + headers: + content-type: application/json + body: + name: "Donut" + description: "A delicious fried pastry" + size: "M" + price: 1.5 + status: "available" + """; + + Service service = new Service(); + service.setType(ServiceType.REST); + Operation operation = new Operation(); + operation.setName("GET /pastries/{name}"); + operation.setDispatcher(DispatchStyles.URI_PARTS); + operation.setDispatcherRules("name"); + + List results = null; + try { + results = AICopilotHelper.parseRequestResponseTemplateOutput(service, operation, aiResponse); + } catch (Exception e) { + fail("Exception should not be thrown here"); + } + + assertNotNull(results); + assertEquals(2, results.size()); + + // Check that request has been correctly parsed. + RequestResponsePair example1 = results.get(0); + assertNull(example1.getRequest().getContent()); + assertEquals(1, example1.getRequest().getHeaders().size()); + + // Check that response has been correctly parsed. + assertEquals("200", example1.getResponse().getStatus()); + assertNotNull(example1.getResponse().getContent()); + assertFalse(example1.getResponse().getContent().contains("\\n")); + assertEquals(1, example1.getResponse().getHeaders().size()); + } + + @Test + void testParseEventMessageOutputYaml() { + String aiResponse = """ + - example: 1 + message: + headers: + my-app-header: 42 + payload: + id: "12345" + sendAt: "2022-01-01T10:00:00Z" + fullName: "John Doe" + email: "john.doe@example.com" + age: 25 + + - example: 2 + message: + headers: + my-app-header: 75 + payload: + id: "98765" + sendAt: "2022-01-02T14:30:00Z" + fullName: "Jane Smith" + email: "jane.smith@example.com" + age: 28 + """; + + Service service = new Service(); + service.setType(ServiceType.EVENT); + Operation operation = new Operation(); + operation.setName("SUBSCRIBE user/signedup"); + + List results = null; + try { + results = AICopilotHelper.parseUnidirectionalEventTemplateOutput(aiResponse); + } catch (Exception e) { + fail("Exception should not be thrown here"); + } + + assertNotNull(results); + assertEquals(2, results.size()); + + // Check that message 1 has been correctly parsed. + EventMessage event1 = results.get(0).getEventMessage(); + assertEquals(1, event1.getHeaders().size()); + assertNotNull(event1.getContent()); + } + + @Test + void testParseRequestResponseOutputYamlWithMD() { + String aiResponse = """ + ##### Example 1: + + Request: + ```yaml + example: 1 + request: + url: "/pastries?size=L" + headers: + accept: application/json + body: + response: + code: 200 + headers: + content-type: application/json + body: + - name: "Croissant" + description: "A buttery, flaky, viennoiserie pastry named for its well-known crescent shape" + size: "L" + price: 3.49 + status: "available" + - name: "Pain au Chocolat" + description: "A sweet, buttery pastry filled with dark chocolate" + size: "L" + price: 4.99 + status: "available" + - name: "Danish" + description: "A multilayered, laminated sweet pastry in the viennoiserie tradition" + size: "L" + price: 2.99 + status: "available" + ``` + + ##### Example 2: + + Request: + ```yaml + example: 2 + request: + url: "/pastries?size=M" + headers: + accept: application/json + body: + response: + code: 200 + headers: + content-type: application/json + body: + - name: "Eclair" + description: "An oblong pastry made with choux dough filled with a cream and topped with icing" + size: "M" + price: 2.99 + status: "available" + - name: "Macaron" + description: "A sweet meringue-based confection made with egg white, icing sugar, granulated sugar, almond flour, and food coloring" + size: "M" + price: 1.99 + status: "available" + - name: "Madeleine" + description: "A small sponge cake traditionally baked in scallop-shaped madeleine molds" + size: "M" + price: 2.49 + status: "available" + ``` + """; + + Service service = new Service(); + service.setType(ServiceType.REST); + Operation operation = new Operation(); + operation.setName("GET /pastries"); + operation.setDispatcher(DispatchStyles.URI_PARAMS); + operation.setDispatcherRules("size"); + + List results = null; + try { + results = AICopilotHelper.parseRequestResponseTemplateOutput(service, operation, aiResponse); + } catch (Exception e) { + fail("Exception should not be thrown here"); + } + + assertNotNull(results); + assertEquals(2, results.size()); + + // Check that request 1 has been correctly parsed. + RequestResponsePair example1 = results.get(0); + assertNull(example1.getRequest().getContent()); + assertEquals(1, example1.getRequest().getHeaders().size()); + + // Check that response 1 has been correctly parsed. + assertEquals("200", example1.getResponse().getStatus()); + assertNotNull(example1.getResponse().getContent()); + assertFalse(example1.getResponse().getContent().contains("\\n")); + assertEquals(1, example1.getResponse().getHeaders().size()); + assertEquals("?size=L", example1.getResponse().getDispatchCriteria()); + } + + @Test + void testParseRequestResponseOutputYamlWithMD2() { + String aiResponse = """ + ### Example 1 + + ```yaml + - example: 1 + request: + url: /pastry/ChocolateCroissant + headers: + accept: application/json + body: + response: + code: 200 + headers: + content-type: application/json + body: + name: "ChocolateCroissant" + description: "A croissant filled with chocolate" + size: "M" + price: 2.5 + status: "available" + ``` + + ### Example 2 + + ```yaml + - example: 2 + request: + url: /pastry/BlueberryMuffin + headers: + accept: application/json + body: + response: + code: 200 + headers: + content-type: application/json + body: + name: "BlueberryMuffin" + description: "A muffin filled with fresh blueberries" + size: "S" + price: 1.5 + status: "available" + ``` + """; + + Service service = new Service(); + service.setType(ServiceType.REST); + Operation operation = new Operation(); + operation.setName("GET /pastry/{name}"); + operation.setDispatcher(DispatchStyles.URI_PARTS); + operation.setDispatcherRules("name"); + + List results = null; + try { + results = AICopilotHelper.parseRequestResponseTemplateOutput(service, operation, aiResponse); + } catch (Exception e) { + fail("Exception should not be thrown here"); + } + + assertNotNull(results); + assertEquals(2, results.size()); + + for (RequestResponsePair pair : results) { + assertNull(pair.getRequest().getContent()); + assertEquals(1, pair.getRequest().getHeaders().size()); + + // Check that response has been correctly parsed. + assertEquals("200", pair.getResponse().getStatus()); + assertNotNull(pair.getResponse().getContent()); + assertEquals(1, pair.getResponse().getHeaders().size()); + + if ("1".equals(pair.getResponse().getName())) { + assertEquals("/name=ChocolateCroissant", pair.getResponse().getDispatchCriteria()); + } else if ("2".equals(pair.getResponse().getName())) { + assertEquals("/name=BlueberryMuffin", pair.getResponse().getDispatchCriteria()); + } else { + fail("Unknown example pair name"); + } + } + } + + @Test + void testParseURIElementsDispatchCriteria() { + String aiResponse = """ + ### Example 1 + + ```yaml + - example: 1 + request: + url: /customer/12345/accounts?filter=portfolio + headers: + accept: application/json + parameters: + customerId: 12345 + filter: portfolio + body: + response: + code: 200 + headers: + content-type: application/json + body: + id: 12345-portfolio + description: "Portfolio account" + ``` + + ### Example 2 + + ```yaml + - example: 2 + request: + url: /customer/67890/accounts + headers: + accept: application/json + parameters: + filter: standard + body: + response: + code: 200 + headers: + content-type: application/json + body: + id: 67890-standard + description: "Standard account" + ``` + """; + + Service service = new Service(); + service.setType(ServiceType.REST); + Operation operation = new Operation(); + operation.setName("GET /customer/{customerId}/accounts"); + operation.setDispatcher(DispatchStyles.URI_ELEMENTS); + operation.setDispatcherRules("customerId ?? filter"); + + List results = null; + try { + results = AICopilotHelper.parseRequestResponseTemplateOutput(service, operation, aiResponse); + } catch (Exception e) { + fail("Exception should not be thrown here"); + } + + assertNotNull(results); + assertEquals(2, results.size()); + + for (RequestResponsePair pair : results) { + if ("1".equals(pair.getResponse().getName())) { + assertEquals("/customerId=12345?filter=portfolio", pair.getResponse().getDispatchCriteria()); + } else if ("2".equals(pair.getResponse().getName())) { + assertEquals("/customerId=67890?filter=standard", pair.getResponse().getDispatchCriteria()); + } else { + fail("Unknown example pair name"); + } + } + } + + @Test + void testParseRequestResponseOutputSplitWithJson() { + String aiResponse = """ + Example 1: + + - example: 1 + request: + url: https://example.com/graphql + headers: + accept: application/json + body: | + { + "query": "query { film(id: \\"123\\") { id, title, episodeID, director, starCount, rating } }" + } + response: + code: 200 + headers: + content-type: application/json + body: | + { + "data": { + "film": { + "id": "123", + "title": "Star Wars: Episode IV - A New Hope", + "episodeID": 4, + "director": "George Lucas", + "starCount": 5000, + "rating": 8.7 + } + } + } + + Example 2: + + - example: 2 + request: + url: https://example.com/graphql + headers: + accept: application/json + body: | + { + "query": "query($id: String) { film(id: $id) { id, title, episodeID, director, starCount, rating } }", + "variables": { + "id": "456" + } + } + response: + code: 200 + headers: + content-type: application/json + body: | + { + "data": { + "film": { + "id": "456", + "title": "The Godfather", + "episodeID": 1, + "director": "Francis Ford Coppola", + "starCount": 4500, + "rating": 9.2 + } + } + } + """; + + Service service = new Service(); + service.setType(ServiceType.GRAPHQL); + Operation operation = new Operation(); + operation.setName("film"); + operation.setDispatcher(DispatchStyles.QUERY_ARGS); + operation.setDispatcherRules("id"); + List results = null; + try { + results = AICopilotHelper.parseRequestResponseTemplateOutput(service, operation, aiResponse); + } catch (Exception e) { + fail("Exception should not be thrown here"); + } + + assertNotNull(results); + assertEquals(2, results.size()); + + // Check that request 1 has been correctly parsed. + RequestResponsePair example1 = results.get(0); + assertNotNull(example1.getRequest().getContent()); + assertFalse(example1.getRequest().getContent().contains("\\n")); + assertEquals(1, example1.getRequest().getHeaders().size()); + + // Check that response 1 has been correctly parsed. + assertEquals("200", example1.getResponse().getStatus()); + assertNotNull(example1.getResponse().getContent()); + assertFalse(example1.getResponse().getContent().contains("\\n")); + assertEquals(1, example1.getResponse().getHeaders().size()); + + // Check that request 2 has been correctly parsed. + RequestResponsePair example2 = results.get(1); + assertNotNull(example2.getRequest().getContent()); + assertFalse(example2.getRequest().getContent().contains("\\n")); + assertEquals(1, example2.getRequest().getHeaders().size()); + + // Check that response 2 has been correctly parsed. + assertEquals("200", example1.getResponse().getStatus()); + assertNotNull(example2.getResponse().getContent()); + assertFalse(example2.getResponse().getContent().contains("\\n")); + assertEquals(1, example2.getResponse().getHeaders().size()); + assertEquals("?id=456", example2.getResponse().getDispatchCriteria()); + } + + @Test + void testParseGrpcRequestResponseOutputYamlWithMD() { + String aiResponse = """ + ```yaml + - example: 1 + request: + body: {"firstname": "John", "lastname": "Doe"} + response: + body: {"greeting": "Hello, John Doe!"} + + - example: 2 + request: + body: {"firstname": "Jane", "lastname": "Smith"} + response: + body: {"greeting": "Hello, Jane Smith!"} + ``` + """; + + Service service = new Service(); + service.setType(ServiceType.GRPC); + Operation operation = new Operation(); + operation.setName("greeting"); + operation.setDispatcher(DispatchStyles.JSON_BODY); + operation.setDispatcherRules(""); + List results = null; + try { + results = AICopilotHelper.parseRequestResponseTemplateOutput(service, operation, aiResponse); + } catch (Exception e) { + fail("Exception should not be thrown here"); + } + + assertNotNull(results); + assertEquals(2, results.size()); + + // Check that request 1 has been correctly parsed. + RequestResponsePair example1 = results.get(0); + assertNotNull(example1.getRequest().getContent()); + assertFalse(example1.getRequest().getContent().contains("\\n")); + assertTrue(example1.getRequest().getContent().contains("John")); + + // Check that response 1 has been correctly parsed. + assertNotNull(example1.getResponse().getContent()); + assertFalse(example1.getResponse().getContent().contains("\\n")); + assertTrue(example1.getResponse().getContent().contains("Hello, John Doe!")); + } + + @Test + void testRemoveTokensInNode() { + // Create an ObjectMapper and a test JsonNode + ObjectMapper mapper = new ObjectMapper(); + ObjectNode specNode = mapper.createObjectNode(); + specNode.put("info", "Information"); + specNode.put("openapi", "3.0.0"); + specNode.put("paths", "/path"); + specNode.put("tags", "Tags"); + specNode.put("components", "Components"); + specNode.put("servers", "Servers"); + + // Define fields to keep + List keysToKeep = List.of("info", "openapi", "paths"); + + // Call method removeTagsInNode + AICopilotHelper.removeTokensInNode(specNode, keysToKeep); + + // Ensure only desired fields + assertFalse(specNode.has("tags")); + assertFalse(specNode.has("components")); + assertFalse(specNode.has("servers")); + assertTrue(specNode.has("info")); + assertTrue(specNode.has("openapi")); + assertTrue(specNode.has("paths")); + } + + @Test + void testGetTokenNames() { + // Create an ObjectMapper and a test JsonNode + ObjectMapper mapper = new ObjectMapper(); + ObjectNode specNode = mapper.createObjectNode(); + specNode.put("info", "Information"); + specNode.put("openapi", "3.0.0"); + specNode.put("paths", "/path"); + + // Call the getFieldNames method + List fieldNames = AICopilotHelper.getTokenNames(specNode); + + // Verify that the field names are retrieved correctly + List expectedFieldNames = List.of("info", "openapi", "paths"); + assertEquals(expectedFieldNames, fieldNames); + } + + @Test + void testRemoveSecurityTokenInNode() throws Exception { + String jsonPathSpec = "{\"get\":{\"parameters\":[{\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}},\"text/xml\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}},\"description\":\"Pastry with specified name\"}},\"operationId\":\"GetPastryByName\",\"summary\":\"Get Pastry by name\",\"description\":\"Get Pastry by name\",\"security\":[{\"clientSecret\":[],\"clientId\":[]}]}}"; + // Create a test JsonNode with a security tag in the verb node + ObjectMapper mapper = new ObjectMapper(); + JsonNode pathSpec = mapper.readTree(jsonPathSpec); + + // Call the removeSecurityTagInVerbNode method + AICopilotHelper.removeSecurityTokenInNode(pathSpec, "get"); + + // Verify that the security tag is removed from the verb node + assertFalse(pathSpec.get("get").has("security")); + + // Call the removeSecurityTagInVerbNode method + AICopilotHelper.removeSecurityTokenInNode(pathSpec, "get"); + + // Verify that the method does not throw an exception when the security tag is + // absent + // and the verb node remains unchanged + assertFalse(pathSpec.get("get").has("security")); + } + + @Test + void testFilterOpenAPISpec() throws Exception { + // Create a test JsonNode with a security tag in the verb node + String jsonSpecNode = "{\"openapi\":\"3.0.2\",\"info\":{\"title\":\"API Pastry - 2.0\",\"version\":\"2.0.0\",\"description\":\"API definition of API Pastry sample app\",\"contact\":{\"name\":\"Laurent Broudoux\",\"url\":\"http://github.com/lbroudoux\",\"email\":\"laurent.broudoux@gmail.com\"},\"license\":{\"name\":\"MIT License\",\"url\":\"https://opensource.org/licenses/MIT\"}},\"paths\":{\"/pastry\":{\"summary\":\"Global operations on pastries\",\"get\":{\"tags\":[\"pastry\"],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"array\",\"items\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}}},\"description\":\"Get list of pastries\"}},\"operationId\":\"GetPastries\",\"summary\":\"Get list of pastries\"}},\"/pastry/{name}\":{\"summary\":\"Specific operation on pastry\",\"get\":{\"parameters\":[{\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}},\"text/xml\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}},\"description\":\"Pastry with specified name\"}},\"operationId\":\"GetPastryByName\",\"summary\":\"Get Pastry by name\",\"description\":\"Get Pastry by name\",\"security\":[{\"clientSecret\":[],\"clientId\":[]}]},\"patch\":{\"requestBody\":{\"content\":{\"application/json\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}},\"text/xml\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}},\"required\":true},\"parameters\":[{\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}},\"text/xml\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}},\"description\":\"Changed pastry\"}},\"operationId\":\"PatchPastry\",\"summary\":\"Patch existing pastry\"},\"parameters\":[{\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}]}},\"components\":{\"schemas\":{\"Pastry\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}},\"securitySchemes\":{\"clientId\":{\"type\":\"apiKey\",\"name\":\"Client-Id\",\"in\":\"header\"},\"clientSecret\":{\"type\":\"apiKey\",\"name\":\"Client-Secret\",\"in\":\"header\"}}},\"tags\":[{\"name\":\"pastry\",\"description\":\"Pastry resource\"}]}"; + + // Create a ObjectMapper + ObjectMapper mapper = new ObjectMapper(); + // Create a test JsonNode with references and tags + JsonNode specNode = mapper.readTree(jsonSpecNode); + + // Call the filterOpenAPISpec methods. + AICopilotHelper.filterOpenAPISpecOnOperation(specNode, "GET /pastry/{name}"); + AICopilotHelper.filterOpenAPISpec(specNode); + + // Verify that the keys not to keep are removed correctly + assertFalse(specNode.has("tags")); + + assertTrue(specNode.has("components")); + assertTrue(specNode.has("paths")); + assertTrue(specNode.has("openapi")); + assertTrue(specNode.has("info")); + + assertTrue(specNode.get("paths").has("/pastry/{name}")); + assertFalse(specNode.get("paths").has("/pastry")); + // Ensure security property is removed + assertFalse(specNode.get("paths").get("/pastry/{name}").has("security")); + } + + @Test + void testFilterAsyncAPISpec() throws Exception { + // Create a test JsonNode with a security tag in the verb node + String jsonSpecNode = "{\"asyncapi\":\"2.0.0\",\"id\":\"urn:io.microcks.example.user-signedup\",\"info\":{\"title\":\"User signed-up API\",\"version\":\"0.1.1\",\"description\":\"Sample AsyncAPI for user signedup events\"},\"defaultContentType\":\"application/json\",\"channels\":{\"user/signedup\":{\"description\":\"The topic on which user signed up events may be consumed\",\"subscribe\":{\"summary\":\"Receive informations about user signed up\",\"operationId\":\"receivedUserSignedUp\",\"message\":{\"description\":\"An event describing that a user just signed up.\",\"traits\":[{\"headers\":{\"type\":\"object\",\"properties\":{\"my-app-header\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":100}}}}],\"payload\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"id\":{\"type\":\"string\"},\"sendAt\":{\"type\":\"string\"},\"fullName\":{\"type\":\"string\"},\"email\":{\"type\":\"string\",\"format\":\"email\"},\"age\":{\"type\":\"integer\",\"minimum\":18}}}}}}},\"components\":{\"messageTraits\":{\"commonHeaders\":{\"headers\":{\"type\":\"object\",\"properties\":{\"my-app-header\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":100}}}}}}}"; + + // Create a ObjectMapper + ObjectMapper mapper = new ObjectMapper(); + // Create a test JsonNode with references and tags + JsonNode specNode = mapper.readTree(jsonSpecNode); + + // Call the reduceSpecSize method + AICopilotHelper.filterAsyncAPISpec(specNode); + + // Verify that the keys not to keep are removed correctly + assertFalse(specNode.has("id")); + + assertTrue(specNode.has("components")); + assertTrue(specNode.has("channels")); + assertTrue(specNode.has("asyncapi")); + assertTrue(specNode.has("info")); + + assertTrue(specNode.get("channels").has("user/signedup")); + } + + @Test + void testResolveReferenceAndRemoveTokensInNode() throws Exception { + + String jsonSpecNode = "{\"openapi\":\"3.0.2\",\"info\":{\"title\":\"API Pastry - 2.0\",\"version\":\"2.0.0\",\"description\":\"API definition of API Pastry sample app\",\"contact\":{\"name\":\"Laurent Broudoux\",\"url\":\"http://github.com/lbroudoux\",\"email\":\"laurent.broudoux@gmail.com\"},\"license\":{\"name\":\"MIT License\",\"url\":\"https://opensource.org/licenses/MIT\"}},\"paths\":{\"/pastry\":{\"summary\":\"Global operations on pastries\",\"get\":{\"tags\":[\"pastry\"],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/components/schemas/Pastry\"}},\"examples\":{\"pastries_json\":{\"value\":[{\"name\":\"Baba Rhum\",\"description\":\"Delicieux Baba au Rhum pas calorique du tout\",\"size\":\"L\",\"price\":3.2,\"status\":\"available\"},{\"name\":\"Divorces\",\"description\":\"Delicieux Divorces pas calorique du tout\",\"size\":\"M\",\"price\":2.8,\"status\":\"available\"},{\"name\":\"Tartelette Fraise\",\"description\":\"Delicieuse Tartelette aux Fraises fraiches\",\"size\":\"S\",\"price\":2,\"status\":\"available\"}]}}}},\"description\":\"Get list of pastries\"}},\"operationId\":\"GetPastries\",\"summary\":\"Get list of pastries\"}},\"/pastry/{name}\":{\"summary\":\"Specific operation on pastry\",\"get\":{\"parameters\":[{\"examples\":{\"Eclair Cafe\":{\"value\":\"Eclair Cafe\"},\"Eclair Cafe Xml\":{\"value\":\"Eclair Cafe\"},\"Millefeuille\":{\"value\":\"Millefeuille\"}},\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Pastry\"},\"examples\":{\"Eclair Cafe\":{\"value\":{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"available\"}},\"Millefeuille\":{\"value\":{\"name\":\"Millefeuille\",\"description\":\"Delicieux Millefeuille pas calorique du tout\",\"size\":\"L\",\"price\":4.4,\"status\":\"available\"}}}},\"text/xml\":{\"schema\":{\"$ref\":\"#/components/schemas/Pastry\"},\"examples\":{\"Eclair Cafe Xml\":{\"value\":\"\\n" + + // + " Eclair Cafe\\n" + // + " Delicieux Eclair au Cafe pas calorique du tout\\n" + // + " M\\n" + // + " 2.5\\n" + // + " available\\n" + // + "\"}}}},\"description\":\"Pastry with specified name\"}},\"operationId\":\"GetPastryByName\",\"summary\":\"Get Pastry by name\",\"description\":\"Get Pastry by name\",\"security\":[{\"clientSecret\":[],\"clientId\":[]}]},\"patch\":{\"requestBody\":{\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Pastry\"},\"examples\":{\"Eclair Cafe\":{\"value\":{\"price\":2.6}}}},\"text/xml\":{\"schema\":{\"$ref\":\"#/components/schemas/Pastry\"},\"examples\":{\"Eclair Cafe Xml\":{\"value\":\"\\n" + + // + "\\t2.6\\n" + // + "\"}}}},\"required\":true},\"parameters\":[{\"examples\":{\"Eclair Cafe\":{\"value\":\"Eclair Cafe\"},\"Eclair Cafe Xml\":{\"value\":\"Eclair Cafe\"}},\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Pastry\"},\"examples\":{\"Eclair Cafe\":{\"value\":{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.6,\"status\":\"available\"}}}},\"text/xml\":{\"schema\":{\"$ref\":\"#/components/schemas/Pastry\"},\"examples\":{\"Eclair Cafe Xml\":{\"value\":\"\\n" + + // + " Eclair Cafe\\n" + // + " Delicieux Eclair au Cafe pas calorique du tout\\n" + // + " M\\n" + // + " 2.6\\n" + // + " available\\n" + // + "\"}}}},\"description\":\"Changed pastry\"}},\"operationId\":\"PatchPastry\",\"summary\":\"Patch existing pastry\"},\"parameters\":[{\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}]}},\"components\":{\"schemas\":{\"Pastry\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}},\"example\":{\"name\":\"My Pastry\",\"description\":\"A short description os my pastry\",\"size\":\"M\",\"price\":4.5,\"status\":\"available\"}}},\"securitySchemes\":{\"clientId\":{\"type\":\"apiKey\",\"name\":\"Client-Id\",\"in\":\"header\"},\"clientSecret\":{\"type\":\"apiKey\",\"name\":\"Client-Secret\",\"in\":\"header\"}}},\"tags\":[{\"name\":\"pastry\",\"description\":\"Pastry resource\"}]}"; + + String jsonExpectedSpecNodeResult = "{\"openapi\":\"3.0.2\",\"info\":{\"title\":\"API Pastry - 2.0\",\"version\":\"2.0.0\",\"description\":\"API definition of API Pastry sample app\",\"contact\":{\"name\":\"Laurent Broudoux\",\"url\":\"http://github.com/lbroudoux\",\"email\":\"laurent.broudoux@gmail.com\"},\"license\":{\"name\":\"MIT License\",\"url\":\"https://opensource.org/licenses/MIT\"}},\"paths\":{\"/pastry\":{\"summary\":\"Global operations on pastries\",\"get\":{\"tags\":[\"pastry\"],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"array\",\"items\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}}},\"description\":\"Get list of pastries\"}},\"operationId\":\"GetPastries\",\"summary\":\"Get list of pastries\"}},\"/pastry/{name}\":{\"summary\":\"Specific operation on pastry\",\"get\":{\"parameters\":[{\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}},\"text/xml\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}},\"description\":\"Pastry with specified name\"}},\"operationId\":\"GetPastryByName\",\"summary\":\"Get Pastry by name\",\"description\":\"Get Pastry by name\",\"security\":[{\"clientSecret\":[],\"clientId\":[]}]},\"patch\":{\"requestBody\":{\"content\":{\"application/json\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}},\"text/xml\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}},\"required\":true},\"parameters\":[{\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}},\"text/xml\":{\"schema\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}}}}},\"description\":\"Changed pastry\"}},\"operationId\":\"PatchPastry\",\"summary\":\"Patch existing pastry\"},\"parameters\":[{\"name\":\"name\",\"description\":\"pastry name\",\"schema\":{\"type\":\"string\"},\"in\":\"path\",\"required\":true}]}},\"components\":{\"schemas\":{\"Pastry\":{\"title\":\"Root Type for Pastry\",\"description\":\"The root of the Pastry type's schema.\",\"type\":\"object\",\"properties\":{\"name\":{\"description\":\"Name of this pastry\",\"type\":\"string\"},\"description\":{\"description\":\"A short description of this pastry\",\"type\":\"string\"},\"size\":{\"description\":\"Size of pastry (S, M, L)\",\"type\":\"string\"},\"price\":{\"format\":\"double\",\"description\":\"Price (in USD) of this pastry\",\"type\":\"number\"},\"status\":{\"description\":\"Status in stock (available, out_of_stock)\",\"type\":\"string\"}},\"example\":{\"name\":\"My Pastry\",\"description\":\"A short description os my pastry\",\"size\":\"M\",\"price\":4.5,\"status\":\"available\"}}},\"securitySchemes\":{\"clientId\":{\"type\":\"apiKey\",\"name\":\"Client-Id\",\"in\":\"header\"},\"clientSecret\":{\"type\":\"apiKey\",\"name\":\"Client-Secret\",\"in\":\"header\"}}},\"tags\":[{\"name\":\"pastry\",\"description\":\"Pastry resource\"}]}"; + + // Create a ObjectMapper + ObjectMapper mapper = new ObjectMapper(); + // Create a test JsonNode with references and tags + JsonNode specNode = mapper.readTree(jsonSpecNode); + + // Call the resolveReferenceAndRemoveTagsInNode method + AICopilotHelper.resolveReferenceAndRemoveTokensInNode(specNode, specNode, new HashMap<>(), new HashSet<>()); + + // Verify that the references and tags are removed correctly + assertEquals(jsonExpectedSpecNodeResult, specNode.toString()); + } + + @Test + void testRemoveTokensFromSpec() throws Exception { + // Create a sample specification + String specification = """ + openapi: 3.0.2 + info: + title: API Pastry - 2.0 + version: 2.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + paths: + /pastry: + summary: Global operations on pastries + get: + tags: + - pastry + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pastry' + examples: + pastries_json: + value: + - name: Baba Rhum + description: Delicieux Baba au Rhum pas calorique du tout + size: L + price: 3.2 + status: available + - name: Divorces + description: Delicieux Divorces pas calorique du tout + size: M + price: 2.8 + status: available + - name: Tartelette Fraise + description: Delicieuse Tartelette aux Fraises fraiches + size: S + price: 2 + status: available + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries + /pastry/{name}: + summary: Specific operation on pastry + get: + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + Millefeuille: + value: Millefeuille + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.5 + status: available + Millefeuille: + value: + name: Millefeuille + description: Delicieux Millefeuille pas calorique du tout + size: L + price: 4.4 + status: available + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: |- + + Eclair Cafe + Delicieux Eclair au Cafe pas calorique du tout + M + 2.5 + available + + description: Pastry with specified name + operationId: GetPastryByName + summary: Get Pastry by name + description: Get Pastry by name + security: + - clientSecret: [] + clientId: [] + patch: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + price: 2.6 + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: "\n\t2.6\n" + required: true + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.6 + status: available + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: |- + + Eclair Cafe + Delicieux Eclair au Cafe pas calorique du tout + M + 2.6 + available + + description: Changed pastry + operationId: PatchPastry + summary: Patch existing pastry + parameters: + - name: name + description: pastry name + schema: + type: string + in: path + required: true + components: + schemas: + Pastry: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + description: + description: A short description of this pastry + type: string + size: + description: Size of pastry (S, M, L) + type: string + price: + format: double + description: Price (in USD) of this pastry + type: number + status: + description: Status in stock (available, out_of_stock) + type: string + example: + name: My Pastry + description: A short description os my pastry + size: M + price: 4.5 + status: available + securitySchemes: + clientId: + type: apiKey + name: Client-Id + in: header + clientSecret: + type: apiKey + name: Client-Secret + in: header + tags: + - name: pastry + description: Pastry resource + """; + ; + + String operationName = "GET /pastry/{name}"; + + // Call the removeTagsFromOpenAPISpec method + String result = AICopilotHelper.removeTokensFromSpec(specification, operationName); + + // Expected result after removing tags + String expectedResult = """ + --- + openapi: "3.0.2" + info: + title: "API Pastry - 2.0" + version: "2.0.0" + description: "API definition of API Pastry sample app" + contact: + name: "Laurent Broudoux" + url: "http://github.com/lbroudoux" + email: "laurent.broudoux@gmail.com" + license: + name: "MIT License" + url: "https://opensource.org/licenses/MIT" + paths: + /pastry/{name}: + get: + parameters: + - name: "name" + description: "pastry name" + schema: + type: "string" + in: "path" + required: true + responses: + "200": + content: + application/json: + schema: + title: "Root Type for Pastry" + description: "The root of the Pastry type's schema." + type: "object" + properties: + name: + description: "Name of this pastry" + type: "string" + description: + description: "A short description of this pastry" + type: "string" + size: + description: "Size of pastry (S, M, L)" + type: "string" + price: + format: "double" + description: "Price (in USD) of this pastry" + type: "number" + status: + description: "Status in stock (available, out_of_stock)" + type: "string" + text/xml: + schema: + title: "Root Type for Pastry" + description: "The root of the Pastry type's schema." + type: "object" + properties: + name: + description: "Name of this pastry" + type: "string" + description: + description: "A short description of this pastry" + type: "string" + size: + description: "Size of pastry (S, M, L)" + type: "string" + price: + format: "double" + description: "Price (in USD) of this pastry" + type: "number" + status: + description: "Status in stock (available, out_of_stock)" + type: "string" + description: "Pastry with specified name" + operationId: "GetPastryByName" + summary: "Get Pastry by name" + description: "Get Pastry by name" + """; + + // Verify the result + assertEquals(expectedResult, result); + } + + @Test + void testRemoveTokensFromSpecWithCircularDependencies() throws Exception { + String specification = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/ai/openwealth-custodyServicesAPI.yaml"), + StandardCharsets.UTF_8); + + String operationName = "GET /accounts/{accountId}/positions"; + // Call the removeTokensFromSpec method + String result = AICopilotHelper.removeTokensFromSpec(specification, operationName); + + //System.err.println("Result: " + result); + assertNotNull(result); + } + + @Test + void testRemoveTokensFromLightSpecWithCircularDependencies() throws Exception { + String specification = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/ai/stripe-light.yaml"), StandardCharsets.UTF_8); + + String operationName = "GET /v1/products"; + // Call the removeTokensFromSpec method + String result = AICopilotHelper.removeTokensFromSpec(specification, operationName); + + //System.err.println("Result: " + result); + assertNotNull(result); + } + + @Test + void testRemoveTokensFromHugeSpecWithCircularDependencies() throws Exception { + String specification = FileUtils.readFileToString( + new File("target/test-classes/io/github/microcks/util/ai/stripe-spec3.yaml"), StandardCharsets.UTF_8); + + String operationName = "GET /v1/products"; + // Call the removeTokensFromSpec method + String result = AICopilotHelper.removeTokensFromSpec(specification, operationName); + + // Fallback resolution with global resolution must have been called... and failed because of TextBuffer overflow + // Then, it should default to the complete specification.... + // At least, it doesn't fail! + //System.err.println("Result: " + result); + assertNotNull(result); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/asyncapi/AsyncAPI3ImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/asyncapi/AsyncAPI3ImporterTest.java new file mode 100644 index 000000000..adacbb169 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/asyncapi/AsyncAPI3ImporterTest.java @@ -0,0 +1,552 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.asyncapi; + +import io.github.microcks.domain.BindingType; +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.ReferenceResolver; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class AsyncAPI3Importer. + * @author laurent + */ +class AsyncAPI3ImporterTest { + + @Test + void testSimpleAsyncAPI3ImportYAML() { + AsyncAPI3Importer importer = null; + try { + importer = new AsyncAPI3Importer( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml", null); + } catch (IOException ioe) { + Assertions.fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + Assertions.fail("Exception should not be thrown"); + } + + Assertions.assertEquals(1, services.size()); + Service service = services.get(0); + Assertions.assertEquals("User signed-up API", service.getName()); + Assertions.assertEquals(ServiceType.EVENT, service.getType()); + Assertions.assertEquals("0.3.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + Assertions.assertEquals(1, resources.size()); + Assertions.assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + Assertions.assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + Assertions.assertNotNull(resources.get(0).getContent()); + + importAndAssertOnSimpleAsyncAPI(service, importer, "application/json"); + } + + @Test + void testSimpleNamelessAsyncAPI3ImportYAML() { + AsyncAPI3Importer importer = null; + try { + importer = new AsyncAPI3Importer( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-nameless.yaml", null); + } catch (IOException ioe) { + Assertions.fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + Assertions.fail("Exception should not be thrown"); + } + + Assertions.assertEquals(1, services.size()); + Service service = services.get(0); + + // Check that operations and input/output have been found. + Assertions.assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + if ("SEND publishUserSignedUps".equals(operation.getName())) { + Assertions.assertEquals("SEND", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + Assertions.fail("No exception should be thrown when importing message definitions."); + } + Assertions.assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent event) { + EventMessage eventMessage = event.getEventMessage(); + Assertions.assertNotNull(eventMessage); + + Assertions.assertTrue("userSignedUp-1".equals(eventMessage.getName()) + || "userSignedUp-2".equals(eventMessage.getName())); + } + } + } else { + Assertions.fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testStaticParametersAsyncAPI3ImportYAML() { + AsyncAPI3Importer importer = null; + try { + importer = new AsyncAPI3Importer( + "target/test-classes/io/github/microcks/util/asyncapi/streetlights-asyncapi-3.0-static.yaml", null); + } catch (IOException ioe) { + Assertions.fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + Assertions.fail("Exception should not be thrown"); + } + + Assertions.assertEquals(1, services.size()); + Service service = services.get(0); + Assertions.assertEquals("Streetlights Kafka API", service.getName()); + Assertions.assertEquals(ServiceType.EVENT, service.getType()); + Assertions.assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + Assertions.assertEquals(1, resources.size()); + Assertions.assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + Assertions.assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + Assertions.assertNotNull(resources.get(0).getContent()); + + // Check operations. + Assertions.assertEquals(4, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + Assertions.assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + Assertions.assertEquals("streetlightId", operation.getDispatcherRules()); + Assertions.assertNotNull(operation.getResourcePaths()); + Assertions.assertEquals(1, operation.getResourcePaths().size()); + + if ("RECEIVE receiveLightMeasurement".equals(operation.getName())) { + Assertions.assertEquals("RECEIVE", operation.getMethod()); + Assertions.assertTrue(operation.getResourcePaths() + .contains("smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured")); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + Assertions.fail("No exception should be thrown when importing message definitions."); + } + Assertions.assertEquals(1, exchanges.size()); + + // Check resource paths have been discovered. + Assertions.assertEquals(2, operation.getResourcePaths().size()); + Assertions.assertTrue( + operation.getResourcePaths().contains("smartylighting.streetlights.1.0.event.123.lighting.measured")); + + Exchange exchange = exchanges.get(0); + Assertions.assertTrue(exchange instanceof UnidirectionalEvent); + EventMessage message = ((UnidirectionalEvent) exchange).getEventMessage(); + + assertEquals("Sample", message.getName()); + assertEquals("/streetlightId=123", message.getDispatchCriteria()); + assertNotNull(message.getContent()); + assertTrue(message.getContent().contains("100")); + + } else if ("SEND turnOn".equals(operation.getName())) { + assertEquals("SEND", operation.getMethod()); + } else if ("SEND turnOff".equals(operation.getName())) { + assertEquals("SEND", operation.getMethod()); + } else if ("SEND dimLight".equals(operation.getName())) { + assertEquals("SEND", operation.getMethod()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testDynamicParametersAsyncAPI3ImportYAML() { + AsyncAPI3Importer importer = null; + try { + importer = new AsyncAPI3Importer( + "target/test-classes/io/github/microcks/util/asyncapi/streetlights-asyncapi-3.0-dynamic.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Streetlights Kafka API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check operations. + assertEquals(4, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("streetlightId", operation.getDispatcherRules()); + assertNotNull(operation.getResourcePaths()); + assertEquals(1, operation.getResourcePaths().size()); + + if ("RECEIVE receiveLightMeasurement".equals(operation.getName())) { + assertEquals("RECEIVE", operation.getMethod()); + assertTrue(operation.getResourcePaths() + .contains("smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured")); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + + // Check resource paths have been discovered. + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains( + "smartylighting.streetlights.1.0.event.da059782-3ad0-4e45-88ce-ef3392bc7797.lighting.measured")); + + Exchange exchange = exchanges.get(0); + assertTrue(exchange instanceof UnidirectionalEvent); + EventMessage message = ((UnidirectionalEvent) exchange).getEventMessage(); + + assertEquals("Sample", message.getName()); + assertEquals("/streetlightId=da059782-3ad0-4e45-88ce-ef3392bc7797", message.getDispatchCriteria()); + assertNotNull(message.getContent()); + assertTrue(message.getContent().contains("100")); + + } else if ("SEND turnOn".equals(operation.getName())) { + assertEquals("SEND", operation.getMethod()); + } else if ("SEND turnOff".equals(operation.getName())) { + assertEquals("SEND", operation.getMethod()); + } else if ("SEND dimLight".equals(operation.getName())) { + assertEquals("SEND", operation.getMethod()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testRemoteRelativeReferenceAsyncAPI3ImportYAML() { + AsyncAPI3Importer importer = null; + try { + importer = new AsyncAPI3Importer( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-ref.yaml", + new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.9.x/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-ref.yaml", + null, true)); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.3.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(3, resources.size()); + + Resource asyncAPISpec = resources.get(0); + assertEquals(ResourceType.ASYNC_API_SPEC, asyncAPISpec.getType()); + assertTrue(asyncAPISpec.getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(asyncAPISpec.getContent()); + + // Check references have been denormalized and replaced in content. + assertTrue(asyncAPISpec.getContent() + .contains("User+signed-up+API-0.3.0--user-signedup-commons.yaml#/components/messageTraits/commonHeaders")); + assertTrue(asyncAPISpec.getContent() + .contains("User+signed-up+API-0.3.0--user-signedup-schemas.yaml#/components/schemas/UserInfo")); + + for (int i = 1; i < 3; i++) { + Resource refResource = resources.get(i); + assertNotNull(refResource.getContent()); + if ("User signed-up API-0.3.0--user-signedup-commons.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_SCHEMA, refResource.getType()); + assertEquals("./user-signedup-commons.yaml", refResource.getPath()); + } else if ("User signed-up API-0.3.0--user-signedup-schemas.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_SCHEMA, refResource.getType()); + assertEquals("./user-signedup-schemas.yaml", refResource.getPath()); + } else { + fail("Unknown ref resource found"); + } + } + + importAndAssertOnSimpleAsyncAPI(service, importer, "application/json"); + + // Check binding has been parsed. + Operation operation = service.getOperations().get(0); + assertNotNull(operation.getBindings().get(BindingType.WS.name())); + assertEquals("POST", operation.getBindings().get(BindingType.WS.name()).getMethod()); + + // Check x-microcks-operation extension has been parsed. + assertEquals(30L, operation.getDefaultDelay().longValue()); + } + + @Test + void testRemoteRelativeAvroReferenceAsyncAPI3ImportYAML() { + AsyncAPI3Importer importer = null; + try { + importer = new AsyncAPI3Importer( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-ref.yaml", + new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.9.x/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-ref.yaml", + null, true)); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.3.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(3, resources.size()); + + Resource asyncAPISpec = resources.get(0); + assertEquals(ResourceType.ASYNC_API_SPEC, asyncAPISpec.getType()); + assertTrue(asyncAPISpec.getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(asyncAPISpec.getContent()); + + // Check references have been denormalized and replaced in content. + assertTrue(asyncAPISpec.getContent() + .contains("User+signed-up+API-0.3.0--user-signedup-commons.yaml#/components/messageTraits/commonHeaders")); + assertTrue(asyncAPISpec.getContent().contains("User+signed-up+API-0.3.0--user-signedup.avsc#/User")); + + for (int i = 1; i < 3; i++) { + Resource refResource = resources.get(i); + assertNotNull(refResource.getContent()); + if ("User signed-up API-0.3.0--user-signedup-commons.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_SCHEMA, refResource.getType()); + assertEquals("./user-signedup-commons.yaml", refResource.getPath()); + assertTrue(refResource.getContent().contains("commonHeaders:")); + } else if ("User signed-up API-0.3.0--user-signedup.avsc".equals(refResource.getName())) { + assertEquals(ResourceType.AVRO_SCHEMA, refResource.getType()); + assertEquals("./user-signedup.avsc", refResource.getPath()); + assertTrue(refResource.getContent().contains("\"namespace\": \"microcks.avro\"")); + } else { + fail("Unknown ref resource found"); + } + } + + importAndAssertOnSimpleAsyncAPI(service, importer, "avro/binary"); + } + + @Test + void testRemoteAbsoluteAvroReferenceAsyncAPI3ImportYAML() { + AsyncAPI3Importer importer = null; + try { + importer = new AsyncAPI3Importer( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-absolute-ref.yaml", + new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.9.x/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-absolute-ref.yaml", + null, true)); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.3.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(3, resources.size()); + + Resource asyncAPISpec = resources.get(0); + assertEquals(ResourceType.ASYNC_API_SPEC, asyncAPISpec.getType()); + assertTrue(asyncAPISpec.getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(asyncAPISpec.getContent()); + + // Check references have been denormalized and replaced in content. + assertTrue(asyncAPISpec.getContent() + .contains("User+signed-up+API-0.3.0--user-signedup-commons.yaml#/components/messageTraits/commonHeaders")); + //assertTrue(asyncAPISpec.getContent().contains("User+signed-up+API-0.3.0--user-signedup.avsc#/User")); + + for (int i = 1; i < 3; i++) { + Resource refResource = resources.get(i); + + if ("User signed-up API-0.3.0--user-signedup-commons.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_SCHEMA, refResource.getType()); + assertEquals("./user-signedup-commons.yaml", refResource.getPath()); + assertNotNull(refResource.getContent()); + } else if ("User signed-up API-0.3.0-user-signedup.avsc".equals(refResource.getName())) { + assertEquals(ResourceType.AVRO_SCHEMA, refResource.getType()); + assertEquals( + "https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.avsc", + refResource.getPath()); + assertNotNull(refResource.getContent()); + } else { + System.err.println(refResource.getName()); + fail("Unknown ref resource found"); + } + } + + importAndAssertOnSimpleAsyncAPI(service, importer, "avro/binary"); + } + + private void importAndAssertOnSimpleAsyncAPI(Service service, AsyncAPI3Importer importer, String contentType) { + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + if ("SEND publishUserSignedUps".equals(operation.getName())) { + assertEquals("SEND", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + assertNotNull(eventMessage); + assertEquals(contentType, eventMessage.getMediaType()); + + if ("laurent".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("Laurent Broudoux")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("23", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:28Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else if ("john".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("John Doe")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("24", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:38Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else { + fail("Unknown message name: " + eventMessage.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/asyncapi/AsyncAPIImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/asyncapi/AsyncAPIImporterTest.java new file mode 100644 index 000000000..953862a7c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/asyncapi/AsyncAPIImporterTest.java @@ -0,0 +1,1046 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.asyncapi; + +import io.github.microcks.domain.*; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.ReferenceResolver; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class AsyncAPIImporter. + * @author laurent + */ +class AsyncAPIImporterTest { + + @Test + void testSimpleAsyncAPIImportYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + importAndAssertOnSimpleAsyncAPI(importer); + } + + @Test + void testSimpleAsyncAPIImportJSON() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi.json", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + importAndAssertOnSimpleAsyncAPI(importer); + } + + private void importAndAssertOnSimpleAsyncAPI(AsyncAPIImporter importer) { + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE user/signedup".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + assertNotNull(eventMessage); + assertEquals("application/json", eventMessage.getMediaType()); + + if ("laurent".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("Laurent Broudoux")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("23", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:28Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else if ("john".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("John Doe")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("24", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:38Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else { + fail("Unknown message name: " + eventMessage.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testSimpleAsyncAPIImportYAMLWithExtensions() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Account Service", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + assertNotNull(service.getMetadata()); + assertEquals("authentication", service.getMetadata().getLabels().get("domain")); + assertEquals("GA", service.getMetadata().getLabels().get("status")); + assertEquals("Team B", service.getMetadata().getLabels().get("team")); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE user/signedup".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + assertEquals(30, operation.getDefaultDelay().longValue()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testAsyncAPIImportGHMasterYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-gh-master.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE user/signedup".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + assertNotNull(eventMessage); + assertEquals("application/json", eventMessage.getMediaType()); + + if ("user/signedup-0".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("Laurent Broudoux")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("23", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:28Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else if ("user/signedup-1".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("John Doe")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("24", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:38Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else { + fail("Unknown message name: " + eventMessage.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testAsyncAPI21ImportYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-asyncapi-2.1.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE user/signedup".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + assertNotNull(eventMessage); + assertEquals("application/json", eventMessage.getMediaType()); + + if ("Laurent".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("Laurent Broudoux")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("23", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:28Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else if ("John".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("John Doe")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("24", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:38Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else { + fail("Unknown message name: " + eventMessage.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testAsyncAPI21RefImportYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Account Service", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE user/signedup".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testAsyncAPI21MultiRefsImportYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/user-events-asyncapi-2.1.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User events API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE user/events".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(3, exchanges.size()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testReferenceAsyncAPIImportYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Streetlights API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE smartylighting/streetlights/event/lighting/measured".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + + assertEquals(1, operation.getBindings().size()); + assertNotNull(operation.getBindings().get("MQTT")); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(3, exchanges.size()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testJsonRemoteRelativeReferenceAsyncAPIImportYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml", + new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.7.x/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml", + null, true)); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(2, resources.size()); + + // First resource is AsyncAPI spec itself. + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + assertTrue(resources.get(0).getContent().contains("$ref: \"User+signed-up+API-0.1.0-user-signedup.json\"")); + + // Second resource is JSON SCHEMA spec itself. + assertEquals(ResourceType.JSON_SCHEMA, resources.get(1).getType()); + assertEquals("User signed-up API-0.1.0-user-signedup.json", resources.get(1).getName()); + assertNotNull(resources.get(1).getContent()); + } + + @Test + void testAvroAsyncAPIImportYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up Avro API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.1.1", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE user/signedup".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + assertNotNull(eventMessage); + assertEquals("avro/binary", eventMessage.getMediaType()); + + if ("laurent".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("Laurent Broudoux")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("23", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:28Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else if ("john".equals(eventMessage.getName())) { + assertNotNull(eventMessage.getContent()); + assertTrue(eventMessage.getContent().contains("John Doe")); + assertNotNull(eventMessage.getHeaders()); + assertEquals(2, eventMessage.getHeaders().size()); + + Iterator
headers = eventMessage.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("my-app-header".equals(header.getName())) { + assertEquals("24", header.getValues().iterator().next()); + } else if ("sentAt".equals(header.getName())) { + assertEquals("2020-03-11T08:03:38Z", header.getValues().iterator().next()); + } else { + fail("Unknown header name: " + header.getName()); + } + } + } else { + fail("Unknown message name: " + eventMessage.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testAvroRemoteRefAsyncAPIImportYAML() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml", + new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/master/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml", + null, true)); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up Avro API", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.1.2", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(2, resources.size()); + + // First resource is AsyncAPI spec itself. + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Second resource should be the referenced relative Avro schema. + assertEquals(ResourceType.AVRO_SCHEMA, resources.get(1).getType()); + assertEquals("User signed-up Avro API-0.1.2-user-signedup.avsc", resources.get(1).getName()); + assertNotNull(resources.get(1).getContent()); + assertTrue(resources.get(1).getContent().contains("\"namespace\": \"microcks.avro\"")); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("SUBSCRIBE user/signedup".equals(operation.getName())) { + assertEquals("SUBSCRIBE", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + assertNotNull(eventMessage); + assertEquals("avro/binary", eventMessage.getMediaType()); + + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testAsyncAPIImportWithParametrizedChannel() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("ApiEventService:maintenance", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.0.3", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("PUBLISH apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}" + .equals(operation.getName())) { + assertEquals("PUBLISH", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("resource_region_id && equipmentType && eventType && resourceType && resourceId", + operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths() + .contains("apim/elevator-co/api/V1/json/fr/elevator/maintenance/elev-make-1/abc4711")); + assertTrue(operation.getResourcePaths() + .contains("apim/elevator-co/api/V1/json/de/elevator/maintenance/elev-make-2/xyz0815")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + + if ("misalignment".equals(eventMessage.getName())) { + assertEquals( + "/equipmentType=elevator/eventType=maintenance/resourceId=abc4711/resourceType=elev-make-1/resource_region_id=fr", + eventMessage.getDispatchCriteria()); + assertEquals("application/json", eventMessage.getMediaType()); + assertNotNull(eventMessage.getContent()); + } else if ("doorfailure".equals(eventMessage.getName())) { + assertEquals( + "/equipmentType=elevator/eventType=maintenance/resourceId=xyz0815/resourceType=elev-make-2/resource_region_id=de", + eventMessage.getDispatchCriteria()); + assertEquals("application/json", eventMessage.getMediaType()); + assertNotNull(eventMessage.getContent()); + } else { + fail("Event has the wrong name. Expecting misalignment or doorfailure"); + } + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testAsyncAPIImportWithParametrizedChannelGHMaster() { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter( + "target/test-classes/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-gh-master.yaml", + null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("ApiEventService:maintenance", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.0.3", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("PUBLISH apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}" + .equals(operation.getName())) { + assertEquals("PUBLISH", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("resource_region_id && equipmentType && eventType && resourceType && resourceId", + operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths() + .contains("apim/elevator-co/api/V1/json/fr/elevator/maintenance/elev-make-1/abc4711")); + assertTrue(operation.getResourcePaths() + .contains("apim/elevator-co/api/V1/json/de/elevator/maintenance/elev-make-2/xyz0815")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + + if ("apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-0" + .equals(eventMessage.getName())) { + assertEquals( + "/equipmentType=elevator/eventType=maintenance/resourceId=abc4711/resourceType=elev-make-1/resource_region_id=fr", + eventMessage.getDispatchCriteria()); + assertEquals("application/json", eventMessage.getMediaType()); + assertNotNull(eventMessage.getContent()); + } else if ("apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-1" + .equals(eventMessage.getName())) { + assertEquals( + "/equipmentType=elevator/eventType=maintenance/resourceId=xyz0815/resourceType=elev-make-2/resource_region_id=de", + eventMessage.getDispatchCriteria()); + assertEquals("application/json", eventMessage.getMediaType()); + assertNotNull(eventMessage.getContent()); + } else { + fail("Event has the wrong name. Expecting apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-0" + + " or apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-1"); + } + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = { + "target/test-classes/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-ws.yaml", + "target/test-classes/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-ws-kv.yaml" }) + void testAsyncAPIImportWithCorrectlyParametrizedChannel(String asyncAPISpecPath) { + AsyncAPIImporter importer = null; + try { + importer = new AsyncAPIImporter(asyncAPISpecPath, null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("ApiEventService:maintenance", service.getName()); + assertEquals(ServiceType.EVENT, service.getType()); + assertEquals("0.0.3", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.ASYNC_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("PUBLISH elevator-co/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}" + .equals(operation.getName())) { + assertEquals("PUBLISH", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("resource_region_id && equipmentType && eventType && resourceType && resourceId", + operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue( + operation.getResourcePaths().contains("elevator-co/fr/elevator/maintenance/elev-make-1/abc4711")); + assertTrue( + operation.getResourcePaths().contains("elevator-co/de/elevator/maintenance/elev-make-2/xyz0815")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof UnidirectionalEvent) { + UnidirectionalEvent event = (UnidirectionalEvent) exchange; + EventMessage eventMessage = event.getEventMessage(); + + if ("misalignment".equals(eventMessage.getName())) { + assertEquals( + "/equipmentType=elevator/eventType=maintenance/resourceId=abc4711/resourceType=elev-make-1/resource_region_id=fr", + eventMessage.getDispatchCriteria()); + assertEquals("application/json", eventMessage.getMediaType()); + assertNotNull(eventMessage.getContent()); + } else if ("doorfailure".equals(eventMessage.getName())) { + assertEquals( + "/equipmentType=elevator/eventType=maintenance/resourceId=xyz0815/resourceType=elev-make-2/resource_region_id=de", + eventMessage.getDispatchCriteria()); + assertEquals("application/json", eventMessage.getMediaType()); + assertNotNull(eventMessage.getContent()); + } else { + fail("Event has the wrong name. Expecting misalignment or doorfailure"); + } + } else { + fail("Exchange has the wrong type. Expecting UnidirectionalEvent"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/dispatcher/JsonEvaluationSpecificationTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/dispatcher/JsonEvaluationSpecificationTest.java new file mode 100644 index 000000000..e42dc7687 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/dispatcher/JsonEvaluationSpecificationTest.java @@ -0,0 +1,78 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for JsonEvaluationSpecification class. + * @author laurent + */ +class JsonEvaluationSpecificationTest { + + private final static String JSON_PAYLOAD = "{\"exp\": \"/type\", \"operator\": \"range\", \"cases\": {" + + "\".*[Aa][Ll][Ee].*\": \"OK\", " + "\"default\": \"Bad\"" + "}}"; + + @Test + void testJsonSerialization() { + DispatchCases cases = new DispatchCases(); + Map dispatchCases = new HashMap<>(); + dispatchCases.put(".*[Aa][Ll][Ee].*", "OK"); + dispatchCases.put("default", "Bad"); + cases.putAll(dispatchCases); + + JsonEvaluationSpecification specifications = new JsonEvaluationSpecification(); + specifications.setExp("/type"); + specifications.setOperator(EvaluationOperator.regexp); + specifications.setCases(cases); + + String jsonPayload = null; + try { + ObjectMapper mapper = new ObjectMapper(); + jsonPayload = mapper.writeValueAsString(specifications); + } catch (Exception e) { + fail("No exception should be thrown when writing Json payload..."); + } + + assertNotEquals(-1, jsonPayload.indexOf("exp")); + assertNotEquals(-1, jsonPayload.indexOf("operator")); + assertNotEquals(-1, jsonPayload.indexOf("cases")); + assertNotEquals(-1, jsonPayload.indexOf("default")); + assertNotEquals(-1, jsonPayload.indexOf(".*[Aa][Ll][Ee].*")); + } + + @Test + void testJsonDeserialization() { + JsonEvaluationSpecification specification = null; + try { + specification = JsonEvaluationSpecification.buildFromJsonString(JSON_PAYLOAD); + } catch (Exception e) { + fail("No exception should be thrown when parsing Json payload..."); + } + + assertEquals("/type", specification.getExp()); + assertEquals(EvaluationOperator.range, specification.getOperator()); + assertEquals(2, specification.getCases().size()); + assertEquals("OK", specification.getCases().get(".*[Aa][Ll][Ee].*")); + assertEquals("Bad", specification.getCases().getDefault()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/dispatcher/JsonExpressionEvaluatorTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/dispatcher/JsonExpressionEvaluatorTest.java new file mode 100644 index 000000000..8976e21c8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/dispatcher/JsonExpressionEvaluatorTest.java @@ -0,0 +1,185 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.dispatcher; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for JsonExpressionEvaluator. + * @author laurent + */ +class JsonExpressionEvaluatorTest { + + private static final String BELGIUM_BEER = "{\"name\": \"Maredsous\"," + + "\"country\": \"Belgium\", \"type\": \"Brown ale\"," + "\"rating\": 4.2, \"status\": \"available\"}"; + + private static final String GERMAN_BEER = "{\"name\": \"Weissbier\"," + + "\"country\": \"Germany\", \"type\": \"Wheat\"," + + "\"rating\": 3.5, \"status\": \"out_of_stock\", \"extra\": \"Extra Information\"}"; + + private static final String EXTRA_GERMAN_BEER = "{\"name\": \"Weissbier\"," + + "\"country\": \"Germany\", \"type\": \"Wheat\"," + + "\"rating\": 3.5, \"status\": \"out_of_stock\", \"extra\": {\"value\": \"Extra Information\"}}"; + + private static final String ENGLISH_BEER = "{\"name\": \"Guinness\"," + "\"country\": \"UK\", \"type\": \"Black\"," + + "\"rating\": 4.1, \"status\": \"available\"}"; + + private static final String LAURENT_CARS = "{\"driver\": \"Laurent\", \"cars\": [" + + "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}, " + + "{\"name\": \"jean-pierre\", \"model\": \"Peugeot Traveller\", \"year\": 2017}]}"; + + private static final String LOT_OF_CARS = "{\"driver\": \"Laurent\", \"cars\": [" + + "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}, " + + "{\"name\": \"308\", \"model\": \"Peugeot 308\", \"year\": 2014}, " + + "{\"name\": \"jean-pierre\", \"model\": \"Peugeot Traveller\", \"year\": 2017}]}"; + + private static final String HUGE_LIST_OF_CARS = "{\"driver\": \"Laurent\", \"cars\": [" + + "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}, " + + "{\"name\": \"308\", \"model\": \"Peugeot 308\", \"year\": 2014}, " + + "{\"name\": \"508\", \"model\": \"Peugeot 508\", \"year\": 2015}, " + + "{\"name\": \"3008\", \"model\": \"Peugeot 3008\", \"year\": 2016}, " + + "{\"name\": \"5008\", \"model\": \"Peugeot 5008\", \"year\": 2017}, " + + "{\"name\": \"maurice\", \"model\": \"Peugeot 5008\", \"year\": 2017}, " + + "{\"name\": \"jean-pierre\", \"model\": \"Peugeot Traveller\", \"year\": 2017}]}"; + + @Test + void testEqualsOperatorDispatcher() throws Exception { + + DispatchCases cases = new DispatchCases(); + Map dispatchCases = new HashMap<>(); + dispatchCases.put("Belgium", "OK"); + dispatchCases.put("Germany", "KO"); + dispatchCases.put("default", "Why not?"); + cases.putAll(dispatchCases); + + JsonEvaluationSpecification specifications = new JsonEvaluationSpecification(); + specifications.setExp("/country"); + specifications.setOperator(EvaluationOperator.equals); + specifications.setCases(cases); + + String result = JsonExpressionEvaluator.evaluate(BELGIUM_BEER, specifications); + assertEquals("OK", result); + + result = JsonExpressionEvaluator.evaluate(GERMAN_BEER, specifications); + assertEquals("KO", result); + + result = JsonExpressionEvaluator.evaluate(ENGLISH_BEER, specifications); + assertEquals("Why not?", result); + } + + @Test + void testRangeOperatorDispatcher() throws Exception { + DispatchCases cases = new DispatchCases(); + Map dispatchCases = new HashMap<>(); + dispatchCases.put("[4.2;5.0]", "Acceptable"); + dispatchCases.put("[0;4[", "Too low"); + dispatchCases.put("default", "Why not?"); + cases.putAll(dispatchCases); + + JsonEvaluationSpecification specifications = new JsonEvaluationSpecification(); + specifications.setExp("/rating"); + specifications.setOperator(EvaluationOperator.range); + specifications.setCases(cases); + + String result = JsonExpressionEvaluator.evaluate(BELGIUM_BEER, specifications); + assertEquals("Acceptable", result); + + result = JsonExpressionEvaluator.evaluate(GERMAN_BEER, specifications); + assertEquals("Too low", result); + + result = JsonExpressionEvaluator.evaluate(ENGLISH_BEER, specifications); + assertEquals("Why not?", result); + } + + @Test + void testRegexpOperatorDispatcher() throws Exception { + DispatchCases cases = new DispatchCases(); + Map dispatchCases = new HashMap<>(); + dispatchCases.put(".*[Aa][Ll][Ee].*", "OK"); + dispatchCases.put("default", "Bad"); + cases.putAll(dispatchCases); + + JsonEvaluationSpecification specifications = new JsonEvaluationSpecification(); + specifications.setExp("/type"); + specifications.setOperator(EvaluationOperator.regexp); + specifications.setCases(cases); + + String result = JsonExpressionEvaluator.evaluate(BELGIUM_BEER, specifications); + assertEquals("OK", result); + + result = JsonExpressionEvaluator.evaluate(GERMAN_BEER, specifications); + assertEquals("Bad", result); + + result = JsonExpressionEvaluator.evaluate(ENGLISH_BEER, specifications); + assertEquals("Bad", result); + } + + @Test + void testSizeOperatorDispatcher() throws Exception { + DispatchCases cases = new DispatchCases(); + Map dispatchCases = new HashMap<>(); + dispatchCases.put("[0;2]", "Standard"); + dispatchCases.put("[3;5]", "Lot of"); + dispatchCases.put("default", "Huge"); + cases.putAll(dispatchCases); + + JsonEvaluationSpecification specifications = new JsonEvaluationSpecification(); + specifications.setExp("/cars"); + specifications.setOperator(EvaluationOperator.size); + specifications.setCases(cases); + + String result = JsonExpressionEvaluator.evaluate(LAURENT_CARS, specifications); + assertEquals("Standard", result); + + result = JsonExpressionEvaluator.evaluate(LOT_OF_CARS, specifications); + assertEquals("Lot of", result); + + result = JsonExpressionEvaluator.evaluate(HUGE_LIST_OF_CARS, specifications); + assertEquals("Huge", result); + } + + @Test + void testPresenceOperatorDispatcher() throws Exception { + DispatchCases cases = new DispatchCases(); + Map dispatchCases = new HashMap<>(); + dispatchCases.put("found", "Extra"); + dispatchCases.put("missing", "Normal"); + dispatchCases.put("default", "Basic"); + cases.putAll(dispatchCases); + + JsonEvaluationSpecification specifications = new JsonEvaluationSpecification(); + specifications.setExp("/extra"); + specifications.setOperator(EvaluationOperator.presence); + specifications.setCases(cases); + + String result = JsonExpressionEvaluator.evaluate(BELGIUM_BEER, specifications); + assertEquals("Normal", result); + + result = JsonExpressionEvaluator.evaluate(GERMAN_BEER, specifications); + assertEquals("Extra", result); + + result = JsonExpressionEvaluator.evaluate(ENGLISH_BEER, specifications); + assertEquals("Normal", result); + + result = JsonExpressionEvaluator.evaluate(EXTRA_GERMAN_BEER, specifications); + assertEquals("Extra", result); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/graphql/GraphQLImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/graphql/GraphQLImporterTest.java new file mode 100644 index 000000000..901f2741b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/graphql/GraphQLImporterTest.java @@ -0,0 +1,104 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for GraphQLImporter class. + * @author laurent + */ +class GraphQLImporterTest { + + @Test + void testSimpleGraphQLImport() { + GraphQLImporter importer = null; + try { + importer = new GraphQLImporter("target/test-classes/io/github/microcks/util/graphql/films.graphql"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Service definition import should not fail"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("Movie Graph API", service.getName()); + assertEquals(ServiceType.GRAPHQL, service.getType()); + assertEquals("1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = null; + try { + resources = importer.getResourceDefinitions(service); + } catch (MockRepositoryImportException mrie) { + fail("Resource definition import should not fail"); + } + assertEquals(1, resources.size()); + + Resource resource = resources.get(0); + assertEquals(ResourceType.GRAPHQL_SCHEMA, resource.getType()); + assertEquals("Movie Graph API-1.0.graphql", resource.getName()); + + // Check that operations and input/output have been found. + assertEquals(4, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("allFilms".equals(operation.getName())) { + assertEquals("QUERY", operation.getMethod()); + assertEquals("FilmsConnection", operation.getOutputName()); + assertNull(operation.getDispatcher()); + } else if ("film".equals(operation.getName())) { + assertEquals("QUERY", operation.getMethod()); + assertEquals("Film", operation.getOutputName()); + assertEquals("String", operation.getInputName()); + assertEquals(DispatchStyles.QUERY_ARGS, operation.getDispatcher()); + assertEquals("id", operation.getDispatcherRules()); + } else if ("addStar".equals(operation.getName())) { + assertEquals("MUTATION", operation.getMethod()); + assertEquals("Film", operation.getOutputName()); + assertEquals("String", operation.getInputName()); + assertEquals(DispatchStyles.QUERY_ARGS, operation.getDispatcher()); + assertEquals("filmId", operation.getDispatcherRules()); + } else if ("addReview".equals(operation.getName())) { + assertEquals("MUTATION", operation.getMethod()); + assertEquals("Film", operation.getOutputName()); + assertEquals("String, Review", operation.getInputName()); + assertNull(operation.getDispatcher()); + } else { + fail("Unknown operation"); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/graphql/GraphQLMcpToolConverterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/graphql/GraphQLMcpToolConverterTest.java new file mode 100644 index 000000000..362a54bee --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/graphql/GraphQLMcpToolConverterTest.java @@ -0,0 +1,267 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.graphql; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.listener.ListenerTestsConfiguration; +import io.github.microcks.repository.RepositoryTestsConfiguration; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.service.ArtifactInfo; +import io.github.microcks.service.ServiceService; +import io.github.microcks.util.ai.McpSchema; +import io.github.microcks.web.ControllerTestsConfiguration; +import io.github.microcks.web.GraphQLInvocationProcessor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import graphql.parser.ParserOptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test class for GraphQLMcpToolConverter. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@SpringJUnitConfig(classes = { RepositoryTestsConfiguration.class, ControllerTestsConfiguration.class, + ListenerTestsConfiguration.class }) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class GraphQLMcpToolConverterTest { + + @Autowired + private ServiceService serviceService; + + @Autowired + private ResourceRepository resourceRepository; + + @Autowired + private GraphQLInvocationProcessor graphQLInvocationProcessor; + + private Service servicev1; + private Service servicev2; + private GraphQLMcpToolConverter toolConverterv1; + private GraphQLMcpToolConverter toolConverterv2; + + @BeforeAll + void setUp() throws Exception { + // Import the petstore service definition from the GraphQL tutorial. + File artifactFilev1 = new File("target/test-classes/io/github/microcks/util/graphql/petstore-1.0.graphql"); + File artifactFilev2 = new File("target/test-classes/io/github/microcks/util/graphql/github.graphql"); + // Extract service and resource. + tool converters. + List services = serviceService.importServiceDefinition(artifactFilev1, null, + new ArtifactInfo("petstore-1.0.graphql", true)); + servicev1 = services.getFirst(); + List resources = resourceRepository.findByServiceIdAndType(servicev1.getId(), + ResourceType.GRAPHQL_SCHEMA); + toolConverterv1 = new GraphQLMcpToolConverter(servicev1, resources.getFirst(), graphQLInvocationProcessor, + new ObjectMapper()); + + // For v2, we need to override parser default settings. + ParserOptions.setDefaultParserOptions( + ParserOptions.getDefaultParserOptions().transform(opts -> opts.maxCharacters(10000000))); + ParserOptions + .setDefaultParserOptions(ParserOptions.getDefaultParserOptions().transform(opts -> opts.maxTokens(100000))); + + services = serviceService.importServiceDefinition(artifactFilev2, null, new ArtifactInfo("github.graphql", true)); + servicev2 = services.getFirst(); + resources = resourceRepository.findByServiceIdAndType(servicev2.getId(), ResourceType.GRAPHQL_SCHEMA); + toolConverterv2 = new GraphQLMcpToolConverter(servicev2, resources.getFirst(), graphQLInvocationProcessor, + new ObjectMapper()); + } + + @Test + void testGetToolName() { + for (Operation operation : servicev1.getOperations()) { + String toolName = toolConverterv1.getToolName(operation); + if ("allPets".equals(operation.getName())) { + assertEquals("allPets", toolName); + } else if ("searchPets".equals(operation.getName())) { + assertEquals("searchPets", toolName); + } else if ("advancedSearchPets".equals(operation.getName())) { + assertEquals("advancedSearchPets", toolName); + } else if ("createPet".equals(operation.getName())) { + assertEquals("createPet", toolName); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testGetToolDescription() { + for (Operation operation : servicev1.getOperations()) { + String toolDescription = toolConverterv1.getToolDescription(operation); + if ("allPets".equals(operation.getName())) { + assertEquals("Retrieve all pets from the store. This is not a paginated query.", toolDescription); + } else if ("searchPets".equals(operation.getName())) { + assertNull(toolDescription); + } else if ("advancedSearchPets".equals(operation.getName())) { + assertNull(toolDescription); + } else if ("createPet".equals(operation.getName())) { + assertNull(toolDescription); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + + for (Operation operation : servicev2.getOperations()) { + String toolDescription = toolConverterv2.getToolDescription(operation); + if ("addEnterpriseOrganizationMember".equals(operation.getName())) { + assertEquals("Adds enterprise members to an organization within the enterprise.", toolDescription); + } + } + } + + @Test + void testGetInputSchema() throws Exception { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + for (Operation operation : servicev1.getOperations()) { + McpSchema.JsonSchema inputSchema = toolConverterv1.getInputSchema(operation); + + if ("allPets".equals(operation.getName())) { + assertTrue(inputSchema.properties().isEmpty()); + + assertEquals(""" + --- + type: "object" + properties: {} + required: [] + additionalProperties: false + """, mapper.writeValueAsString(inputSchema)); + + } else if ("searchPets".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + + assertTrue(inputSchema.properties().containsKey("name")); + assertEquals(""" + --- + type: "object" + properties: + name: + type: "string" + required: + - "name" + additionalProperties: false + """, mapper.writeValueAsString(inputSchema)); + + } else if ("advancedSearchPets".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + assertTrue(inputSchema.properties().containsKey("filters")); + + assertEquals(""" + --- + type: "object" + properties: + filters: + type: "array" + items: + type: "object" + properties: + name: + type: "string" + value: + type: "string" + required: + - "name" + - "value" + additionalProperties: false + required: + - "filters" + additionalProperties: false + """, mapper.writeValueAsString(inputSchema)); + + } else if ("createPet".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + assertTrue(inputSchema.properties().containsKey("newPet")); + + assertEquals(""" + --- + type: "object" + properties: + newPet: + type: "object" + properties: + name: + type: "string" + color: + type: "string" + required: + - "name" + - "color" + additionalProperties: false + required: + - "newPet" + additionalProperties: false + """, mapper.writeValueAsString(inputSchema)); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + + for (Operation operation : servicev2.getOperations()) { + McpSchema.JsonSchema inputSchema = toolConverterv2.getInputSchema(operation); + + if ("addEnterpriseOrganizationMember".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + + assertEquals(""" + --- + type: "object" + properties: + input: + type: "object" + properties: + clientMutationId: + type: "string" + enterpriseId: + type: "string" + organizationId: + type: "string" + role: + type: "string" + enum: + - "ADMIN" + - "MEMBER" + userIds: + type: "string" + required: + - "enterpriseId" + - "organizationId" + - "userIds" + additionalProperties: false + required: + - "input" + additionalProperties: false + """, mapper.writeValueAsString(inputSchema)); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcMcpToolConverterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcMcpToolConverterTest.java new file mode 100644 index 000000000..393f4254b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcMcpToolConverterTest.java @@ -0,0 +1,185 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.listener.ListenerTestsConfiguration; +import io.github.microcks.repository.RepositoryTestsConfiguration; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.service.ArtifactInfo; +import io.github.microcks.service.ServiceService; +import io.github.microcks.util.ai.McpSchema; +import io.github.microcks.web.ControllerTestsConfiguration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test class for GRPCMcpToolConverter. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@SpringJUnitConfig(classes = { RepositoryTestsConfiguration.class, ControllerTestsConfiguration.class, + ListenerTestsConfiguration.class }) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class GrpcMcpToolConverterTest { + + @Autowired + private ServiceService serviceService; + + @Autowired + private ResourceRepository resourceRepository; + + private Service servicev1; + private Service servicev2; + private GrpcMcpToolConverter toolConverterv1; + private GrpcMcpToolConverter toolConverterv2; + + @BeforeAll + void setUp() throws Exception { + // Import the petstore service definition from the GRPC tutorial. + File artifactFilev1 = new File("target/test-classes/io/github/microcks/util/grpc/petstore-v1.proto"); + File artifactFilev2 = new File("target/test-classes/io/github/microcks/util/grpc/petstore-v2.proto"); + // Extract service and resource. + tool converters. + List services = serviceService.importServiceDefinition(artifactFilev1, null, + new ArtifactInfo("petstore-v1.proto", true)); + servicev1 = services.getFirst(); + List resources = resourceRepository.findByServiceIdAndType(servicev1.getId(), + ResourceType.PROTOBUF_DESCRIPTOR); + toolConverterv1 = new GrpcMcpToolConverter(servicev1, resources.getFirst(), null, new ObjectMapper()); + // For v2. + services = serviceService.importServiceDefinition(artifactFilev2, null, + new ArtifactInfo("petstore-v2.proto", true)); + servicev2 = services.getFirst(); + resources = resourceRepository.findByServiceIdAndType(servicev2.getId(), ResourceType.PROTOBUF_DESCRIPTOR); + toolConverterv2 = new GrpcMcpToolConverter(servicev2, resources.getFirst(), null, new ObjectMapper()); + } + + @Test + void testGetToolName() { + for (Operation operation : servicev1.getOperations()) { + String toolName = toolConverterv1.getToolName(operation); + if ("getPets".equals(operation.getName())) { + assertEquals("getPets", toolName); + } else if ("searchPets".equals(operation.getName())) { + assertEquals("searchPets", toolName); + } else if ("createPet".equals(operation.getName())) { + assertEquals("createPet", toolName); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testGetToolDescription() { + // Tool description is not implemented in GrpcMcpToolConverter. + for (Operation operation : servicev1.getOperations()) { + String toolDescription = toolConverterv1.getToolDescription(operation); + assertNull(toolDescription); + } + } + + @Test + void testGetInputSchema() throws Exception { + for (Operation operation : servicev1.getOperations()) { + McpSchema.JsonSchema inputSchema = toolConverterv1.getInputSchema(operation); + assertEquals("object", inputSchema.type()); + if ("getPets".equals(operation.getName())) { + assertTrue(inputSchema.properties().isEmpty()); + } else if ("searchPets".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + assertTrue(inputSchema.properties().containsKey("name")); + } else if ("createPet".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + assertTrue(inputSchema.properties().containsKey("name")); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + + String expectedCreatePetSchema = """ + --- + type: "object" + properties: + name: + type: "string" + coat: + type: "object" + properties: + name: + type: "string" + tint: + type: "string" + enum: + - "LIGHT" + - "DARK" + required: [] + additionalProperties: false + bugs: + type: "array" + items: + type: "string" + enum: + - "TICK" + - "FLEA" + tags: + type: "array" + items: + type: "string" + foobars: + type: "array" + items: + type: "object" + properties: + foo: + type: "string" + bar: + type: "string" + required: [] + additionalProperties: false + """; + + for (Operation operation : servicev2.getOperations()) { + if ("createPet".equals(operation.getName())) { + McpSchema.JsonSchema inputSchema = toolConverterv2.getInputSchema(operation); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + try { + String result = mapper.writeValueAsString(inputSchema); + assertEquals(expectedCreatePetSchema, result); + } catch (Exception e) { + fail("Failed to serialize input schema for operation " + operation.getName(), e); + } + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcMetadataUtilTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcMetadataUtilTest.java new file mode 100644 index 000000000..2de254849 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcMetadataUtilTest.java @@ -0,0 +1,112 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +import io.github.microcks.util.script.HttpHeadersStringToStringsMap; +import io.github.microcks.util.script.StringToStringsMap; +import io.grpc.Metadata; + +/** + * This is a test case for GrpcMetadataUtil. + */ +public class GrpcMetadataUtilTest { + @Test + void testMetadataWithMultipleEntries() { + Metadata metadata = new Metadata(); + metadata.put(Metadata.Key.of("foo1", Metadata.ASCII_STRING_MARSHALLER), "bar"); + metadata.put(Metadata.Key.of("foo2", Metadata.ASCII_STRING_MARSHALLER), "bar"); + + StringToStringsMap headers = null; + try { + headers = GrpcMetadataUtil.convertToMap(metadata); + } catch (Exception e) { + fail("No exception should be thrown while parsing grpc metadata"); + } + StringToStringsMap expectedHeaders = new HttpHeadersStringToStringsMap(); + expectedHeaders.put("foo1", "bar"); + expectedHeaders.put("foo2", "bar"); + assertEquals(expectedHeaders, headers); + } + + @Test + void testMetadataWithKeyMultipleTimes() { + Metadata metadata = new Metadata(); + metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar1"); + metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar2"); + + StringToStringsMap headers = null; + try { + headers = GrpcMetadataUtil.convertToMap(metadata); + } catch (Exception e) { + fail("No exception should be thrown while parsing grpc metadata"); + } + StringToStringsMap expectedHeaders = new StringToStringsMap(); + expectedHeaders.put("foo", "bar1"); + expectedHeaders.put("foo", "bar2"); + assertEquals(expectedHeaders, headers); + } + + @Test + void testMetadataWithEmptyMetadata() { + Metadata metadata = new Metadata(); + + StringToStringsMap headers = null; + try { + headers = GrpcMetadataUtil.convertToMap(metadata); + } catch (Exception e) { + fail("No exception should be thrown while parsing empty grpc metadata"); + } + StringToStringsMap expectedHeaders = new StringToStringsMap(); + assertEquals(expectedHeaders, headers); + } + + @Test + void testMetadataWithStringMetadata() { + Metadata metadata = new Metadata(); + metadata.put(Metadata.Key.of("foo1", Metadata.ASCII_STRING_MARSHALLER), "bar"); + + StringToStringsMap headers = null; + try { + headers = GrpcMetadataUtil.convertToMap(metadata); + } catch (Exception e) { + fail("No exception should be thrown while parsing grpc metadata"); + } + StringToStringsMap expectedHeaders = new StringToStringsMap(); + expectedHeaders.put("foo1", "bar"); + assertEquals(expectedHeaders, headers); + } + + @Test + void testMetadataWithBinaryMetadata() { + Metadata metadata = new Metadata(); + metadata.put(Metadata.Key.of("foo1-bin", Metadata.BINARY_BYTE_MARSHALLER), "bar".getBytes()); + + StringToStringsMap headers = null; + try { + headers = GrpcMetadataUtil.convertToMap(metadata); + } catch (Exception e) { + fail("No exception should be thrown while parsing grpc metadata"); + } + StringToStringsMap expectedHeaders = new StringToStringsMap(); + expectedHeaders.put("foo1-bin", "bar"); + assertEquals(expectedHeaders, headers); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcUtilTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcUtilTest.java new file mode 100644 index 000000000..6942fa66d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/GrpcUtilTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.TypeRegistry; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for GrpcUtil class. + * @author laurent + */ +class GrpcUtilTest { + + @Test + void testFindMethodDescriptor() { + // This is the simple HelloService with no dependencies. + String base64ProtobufDescriptor = "CrICCg5oZWxsby12MS5wcm90bxIgaW8uZ2l0aHViLm1pY3JvY2tzLmdycGMuaGVsbG8udjEiSAoMSGVsbG9SZXF1ZXN0EhwKCWZpcnN0bmFtZRgBIAEoCVIJZmlyc3RuYW1lEhoKCGxhc3RuYW1lGAIgASgJUghsYXN0bmFtZSIrCg1IZWxsb1Jlc3BvbnNlEhoKCGdyZWV0aW5nGAEgASgJUghncmVldGluZzJ7CgxIZWxsb1NlcnZpY2USawoIZ3JlZXRpbmcSLi5pby5naXRodWIubWljcm9ja3MuZ3JwYy5oZWxsby52MS5IZWxsb1JlcXVlc3QaLy5pby5naXRodWIubWljcm9ja3MuZ3JwYy5oZWxsby52MS5IZWxsb1Jlc3BvbnNlQgJQAWIGcHJvdG8z"; + + Descriptors.MethodDescriptor desc = null; + try { + desc = GrpcUtil.findMethodDescriptor(base64ProtobufDescriptor, "HelloService", "greeting"); + } catch (Exception e) { + fail("No exception should be thrown while parsing protobuf descriptor and searching service"); + } + + assertNotNull(desc); + assertEquals("io.github.microcks.grpc.hello.v1.HelloService.greeting", desc.getFullName()); + } + + @Test + void testFindMethodDescriptorWithDependency() { + // This is the GoodbyeService with descriptor embedding the shared/uuid.proto dependency. + String base64ProtobufDescriptor = "CjsKEXNoYXJlZC91dWlkLnByb3RvEgZzaGFyZWQiFgoEVVVJRBIOCgJpZBgBIAEoCVICaWRiBnByb3RvMwqDAwoQZ29vZGJ5ZS12MS5wcm90bxIiaW8uZ2l0aHViLm1pY3JvY2tzLmdycGMuZ29vZGJ5ZS52MRoRc2hhcmVkL3V1aWQucHJvdG8iSgoOR29vZGJ5ZVJlcXVlc3QSHAoJZmlyc3RuYW1lGAEgASgJUglmaXJzdG5hbWUSGgoIbGFzdG5hbWUYAiABKAlSCGxhc3RuYW1lIlkKD0dvb2RieWVSZXNwb25zZRIaCghmYXJld2VsbBgBIAEoCVIIZmFyZXdlbGwSKgoJbWVzc2FnZUlkGAIgASgLMgwuc2hhcmVkLlVVSURSCW1lc3NhZ2VJZDKEAQoOR29vZGJ5ZVNlcnZpY2UScgoHZ29vZGJ5ZRIyLmlvLmdpdGh1Yi5taWNyb2Nrcy5ncnBjLmdvb2RieWUudjEuR29vZGJ5ZVJlcXVlc3QaMy5pby5naXRodWIubWljcm9ja3MuZ3JwYy5nb29kYnllLnYxLkdvb2RieWVSZXNwb25zZUICUAFiBnByb3RvMw=="; + + Descriptors.MethodDescriptor desc = null; + try { + desc = GrpcUtil.findMethodDescriptor(base64ProtobufDescriptor, "GoodbyeService", "goodbye"); + } catch (Exception e) { + fail("No exception should be thrown while parsing protobuf descriptor and searching service"); + } + + assertNotNull(desc); + assertEquals("io.github.microcks.grpc.goodbye.v1.GoodbyeService.goodbye", desc.getFullName()); + } + + @Test + void testBuildTypeRegistry() { + // This is the ExampleService from example-any-v1.proto using Any typed parameter and requiring a TypeRegistry. + String base64ProtobufDescriptor = "Ct0BChlnb29nbGUvcHJvdG9idWYvYW55LnByb3RvEg9nb29nbGUucHJvdG9idWYiNgoDQW55EhkKCHR5cGVfdXJsGAEgASgJUgd0eXBlVXJsEhQKBXZhbHVlGAIgASgMUgV2YWx1ZUJvChNjb20uZ29vZ2xlLnByb3RvYnVmQghBbnlQcm90b1ABWiVnaXRodWIuY29tL2dvbGFuZy9wcm90b2J1Zi9wdHlwZXMvYW55ogIDR1BCqgIeR29vZ2xlLlByb3RvYnVmLldlbGxLbm93blR5cGVzYgZwcm90bzMK/AMKFWV4YW1wbGVfc2VydmljZS5wcm90bxIKZXhhbXBsZS52MRoZZ29vZ2xlL3Byb3RvYnVmL2FueS5wcm90byKdAQoORXhhbXBsZU1lc3NhZ2USQwoHcGF5bG9hZBgBIAEoCzIpLmV4YW1wbGUudjEuRXhhbXBsZU1lc3NhZ2UuRXhhbXBsZVBheWxvYWRSB3BheWxvYWQaRgoORXhhbXBsZVBheWxvYWQSNAoKcGFyYW1ldGVycxgBIAEoCzIULmdvb2dsZS5wcm90b2J1Zi5BbnlSCnBhcmFtZXRlcnMihwEKFkV4YW1wbGVSZXNwb25zZU1lc3NhZ2USUwoHcGF5bG9hZBgBIAEoCzI5LmV4YW1wbGUudjEuRXhhbXBsZVJlc3BvbnNlTWVzc2FnZS5FeGFtcGxlUmVzcG9uc2VQYXlsb2FkUgdwYXlsb2FkGhgKFkV4YW1wbGVSZXNwb25zZVBheWxvYWQiKQoTRXhhbXBsZVBhcmFtZXRlcnNWMRISCgR0ZXh0GAEgASgJUgR0ZXh0Ml8KEEV4YW1wbGVTZXJ2aWNlVjESSwoHRXhhbXBsZRIaLmV4YW1wbGUudjEuRXhhbXBsZU1lc3NhZ2UaIi5leGFtcGxlLnYxLkV4YW1wbGVSZXNwb25zZU1lc3NhZ2UiAGIGcHJvdG8z"; + + TypeRegistry registry = null; + try { + registry = GrpcUtil.buildTypeRegistry(base64ProtobufDescriptor); + } catch (Exception e) { + fail("No exception should be thrown while parsing protobuf descriptor and building registry"); + } + + assertNotNull(registry); + assertNotNull(registry.find("example.v1.ExampleParametersV1")); + assertNotNull(registry.find("example.v1.ExampleMessage")); + assertNotNull(registry.find("example.v1.ExampleMessage.ExamplePayload")); + assertNotNull(registry.find("example.v1.ExampleResponseMessage")); + assertNotNull(registry.find("example.v1.ExampleResponseMessage.ExampleResponsePayload")); + assertNotNull(registry.find("google.protobuf.Any")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/ProtobufImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/ProtobufImporterTest.java new file mode 100644 index 000000000..f8e733400 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/grpc/ProtobufImporterTest.java @@ -0,0 +1,301 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.grpc; + +import com.google.protobuf.DescriptorProtos; +import org.junit.jupiter.api.Test; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.ReferenceResolver; + +import java.io.IOException; +import java.util.Base64; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for ProtobufImporter class. + * @author laurent + */ +class ProtobufImporterTest { + + @Test + void testSimpleProtobufImport() { + ProtobufImporter importer = null; + try { + importer = new ProtobufImporter("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Service definition import should not fail"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("io.github.microcks.grpc.hello.v1.HelloService", service.getName()); + assertEquals(ServiceType.GRPC, service.getType()); + assertEquals("v1", service.getVersion()); + assertEquals("io.github.microcks.grpc.hello.v1", service.getXmlNS()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = null; + try { + resources = importer.getResourceDefinitions(service); + } catch (MockRepositoryImportException mrie) { + fail("Resource definition import should not fail"); + } + assertEquals(2, resources.size()); + for (Resource resource : resources) { + assertNotNull(resource.getContent()); + if (ResourceType.PROTOBUF_SCHEMA.equals(resource.getType())) { + assertEquals("io.github.microcks.grpc.hello.v1.HelloService-v1.proto", resource.getName()); + } else if (ResourceType.PROTOBUF_DESCRIPTOR.equals(resource.getType())) { + assertEquals("io.github.microcks.grpc.hello.v1.HelloService-v1.pbb", resource.getName()); + } else { + fail("Resource has not the expected type"); + } + } + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + Operation operation = service.getOperations().get(0); + assertEquals("greeting", operation.getName()); + assertEquals(".io.github.microcks.grpc.hello.v1.HelloRequest", operation.getInputName()); + assertEquals(".io.github.microcks.grpc.hello.v1.HelloResponse", operation.getOutputName()); + } + + @Test + void testProtobufWithOptionsImport() { + ProtobufImporter importer = null; + try { + importer = new ProtobufImporter("target/test-classes/io/github/microcks/util/grpc/hello-v1-option.proto", + null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Service definition import should not fail"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("io.github.microcks.grpc.hello.v1.HelloService", service.getName()); + assertEquals(ServiceType.GRPC, service.getType()); + assertEquals("v1", service.getVersion()); + assertEquals("io.github.microcks.grpc.hello.v1", service.getXmlNS()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = null; + try { + resources = importer.getResourceDefinitions(service); + } catch (MockRepositoryImportException mrie) { + fail("Resource definition import should not fail"); + } + assertEquals(2, resources.size()); + for (Resource resource : resources) { + assertNotNull(resource.getContent()); + if (ResourceType.PROTOBUF_SCHEMA.equals(resource.getType())) { + assertEquals("io.github.microcks.grpc.hello.v1.HelloService-v1.proto", resource.getName()); + } else if (ResourceType.PROTOBUF_DESCRIPTOR.equals(resource.getType())) { + assertEquals("io.github.microcks.grpc.hello.v1.HelloService-v1.pbb", resource.getName()); + + try { + // Check Protobuf Descriptor. + byte[] decodedBinaryPB = Base64.getDecoder().decode(resource.getContent().getBytes("UTF-8")); + + DescriptorProtos.FileDescriptorSet fds = DescriptorProtos.FileDescriptorSet.parseFrom(decodedBinaryPB); + assertEquals(4, fds.getFileCount()); + assertEquals("google/api/http.proto", fds.getFile(0).getName()); + assertEquals("google/api/annotations.proto", fds.getFile(2).getName()); + assertEquals("google/protobuf/descriptor.proto", fds.getFile(1).getName()); + assertEquals("hello-v1-option.proto", fds.getFile(3).getName()); + } catch (Exception e) { + fail("Protobuf file descriptor is not correct"); + } + } else { + fail("Resource has not the expected type"); + } + } + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + Operation operation = service.getOperations().get(0); + assertEquals("greeting", operation.getName()); + assertEquals(".io.github.microcks.grpc.hello.v1.HelloRequest", operation.getInputName()); + assertEquals(".io.github.microcks.grpc.hello.v1.HelloResponse", operation.getOutputName()); + } + + @Test + void testProtobufWithDependenciesImport() { + ProtobufImporter importer = null; + try { + importer = new ProtobufImporter("target/test-classes/io/github/microcks/util/grpc/goodbye-v1.proto", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Service definition import should not fail"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("io.github.microcks.grpc.goodbye.v1.GoodbyeService", service.getName()); + assertEquals(ServiceType.GRPC, service.getType()); + assertEquals("v1", service.getVersion()); + assertEquals("io.github.microcks.grpc.goodbye.v1", service.getXmlNS()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = null; + try { + resources = importer.getResourceDefinitions(service); + } catch (MockRepositoryImportException mrie) { + fail("Resource definition import should not fail"); + } + assertEquals(2, resources.size()); + for (Resource resource : resources) { + assertNotNull(resource.getContent()); + if (ResourceType.PROTOBUF_SCHEMA.equals(resource.getType())) { + assertEquals("io.github.microcks.grpc.goodbye.v1.GoodbyeService-v1.proto", resource.getName()); + } else if (ResourceType.PROTOBUF_DESCRIPTOR.equals(resource.getType())) { + assertEquals("io.github.microcks.grpc.goodbye.v1.GoodbyeService-v1.pbb", resource.getName()); + + try { + // Check Protobuf Descriptor. + byte[] decodedBinaryPB = Base64.getDecoder().decode(resource.getContent().getBytes("UTF-8")); + + DescriptorProtos.FileDescriptorSet fds = DescriptorProtos.FileDescriptorSet.parseFrom(decodedBinaryPB); + assertEquals(2, fds.getFileCount()); + assertEquals("shared/uuid.proto", fds.getFile(0).getName()); + assertEquals("goodbye-v1.proto", fds.getFile(1).getName()); + } catch (Exception e) { + fail("Protobuf file descriptor is not correct"); + } + } else { + fail("Resource has not the expected type"); + } + } + } + + @Test + void testProtobufWithRemoteDependenciesImport() { + ProtobufImporter importer = null; + try { + importer = new ProtobufImporter("target/test-classes/io/github/microcks/util/grpc/remote/goodbye-v1.proto", + new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.5.x/webapp/src/test/resources/io/github/microcks/util/grpc/base.proto", + null, true)); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Service definition import should not fail"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("io.github.microcks.grpc.goodbye.v1.GoodbyeService", service.getName()); + assertEquals(ServiceType.GRPC, service.getType()); + assertEquals("v1", service.getVersion()); + assertEquals("io.github.microcks.grpc.goodbye.v1", service.getXmlNS()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = null; + try { + resources = importer.getResourceDefinitions(service); + } catch (MockRepositoryImportException mrie) { + fail("Resource definition import should not fail"); + } + assertEquals(3, resources.size()); + for (Resource resource : resources) { + assertNotNull(resource.getContent()); + if (ResourceType.PROTOBUF_SCHEMA.equals(resource.getType())) { + assertTrue("io.github.microcks.grpc.goodbye.v1.GoodbyeService-v1.proto".equals(resource.getName()) + || "io.github.microcks.grpc.goodbye.v1.GoodbyeService-v1-shared~1uuid.proto" + .equals(resource.getName())); + } else if (ResourceType.PROTOBUF_DESCRIPTOR.equals(resource.getType())) { + assertEquals("io.github.microcks.grpc.goodbye.v1.GoodbyeService-v1.pbb", resource.getName()); + try { + // Check Protobuf Descriptor. + byte[] decodedBinaryPB = Base64.getDecoder().decode(resource.getContent().getBytes("UTF-8")); + + DescriptorProtos.FileDescriptorSet fds = DescriptorProtos.FileDescriptorSet.parseFrom(decodedBinaryPB); + assertEquals(2, fds.getFileCount()); + assertEquals("shared/uuid.proto", fds.getFile(0).getName()); + assertEquals("goodbye-v1.proto", fds.getFile(1).getName()); + } catch (Exception e) { + fail("Protobuf file descriptor is not correct"); + } + } else { + fail("Resource has not the expected type"); + } + } + } + + @Test + void testProtobufWithEnumDependenciesImport() { + ProtobufImporter importer = null; + try { + importer = new ProtobufImporter( + "target/test-classes/io/github/microcks/util/grpc/TestServiceMissingEnum.proto", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Service definition import should not fail"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("hello.TestService", service.getName()); + assertEquals(ServiceType.GRPC, service.getType()); + assertEquals("hello", service.getVersion()); + assertEquals("hello", service.getXmlNS()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/har/HARImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/har/HARImporterTest.java new file mode 100644 index 000000000..e259a77d4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/har/HARImporterTest.java @@ -0,0 +1,404 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.har; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class HARImporter. + * @author laurent + */ +class HARImporterTest { + + @Test + void testSimpleHARImport() { + HARImporter importer = null; + try { + importer = new HARImporter("target/test-classes/io/github/microcks/util/har/api-pastry-2.0.har"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("API Pastry - 2.0", service.getName()); + assertEquals("2.0.0", service.getVersion()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().get(0); + assertEquals("GET /rest/API+Pastry+-+2.0/2.0.0/pastry/{part1}", operation.getName()); + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("part1", operation.getDispatcherRules()); + + assertEquals(2, operation.getResourcePaths().size()); + + List messages = null; + try { + messages = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(2, messages.size()); + } + + @Test + void testMissingCommentHARImport() { + HARImporter importer = null; + try { + importer = new HARImporter("target/test-classes/io/github/microcks/util/har/microcks.har"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties import fail because of missing comment. + boolean failure = false; + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + failure = true; + assertNotEquals(-1, e.getMessage().indexOf("Expecting a comment in HAR")); + } + assertTrue(failure); + } + + @Test + void testComplexHARImport() { + HARImporter importer = null; + try { + importer = new HARImporter("target/test-classes/io/github/microcks/util/har/api-pastries-0.0.1.har"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("API Pastries", service.getName()); + assertEquals("0.0.1", service.getVersion()); + + assertEquals(3, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /rest/API+Pastries/0.0.1/pastries".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertEquals("size", operation.getDispatcherRules()); + + List messages = null; + try { + messages = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(3, messages.size()); + for (Exchange exchange : messages) { + if (exchange instanceof RequestResponsePair pair) { + Request request = pair.getRequest(); + Response response = pair.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals(request.getName(), response.getName()); + assertEquals("application/json;charset=UTF-8", response.getMediaType()); + assertEquals(1, request.getQueryParameters().size()); + assertEquals("size", request.getQueryParameters().get(0).getName()); + assertEquals(4, request.getHeaders().size()); + if ("2023-08-17T08:43:27.244Z".equals(request.getName())) { + assertEquals("200", response.getStatus()); + assertEquals("?size=M", response.getDispatchCriteria()); + assertEquals("M", request.getQueryParameters().get(0).getValue()); + assertNotNull(response.getContent()); + } else if ("2023-08-17T08:42:25.547Z".equals(request.getName())) { + assertEquals("200", response.getStatus()); + assertEquals("?size=S", response.getDispatchCriteria()); + assertEquals("S", request.getQueryParameters().get(0).getValue()); + assertNotNull(response.getContent()); + } else if ("2023-08-16T13:03:11.873Z".equals(request.getName())) { + assertEquals("200", response.getStatus()); + assertEquals("?size=L", response.getDispatchCriteria()); + assertEquals("L", request.getQueryParameters().get(0).getValue()); + assertNotNull(response.getContent()); + } else { + fail("Unknown request name: " + request.getName()); + } + } + } + + } else if ("GET /rest/API+Pastries/0.0.1/pastries/{part1}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("part1", operation.getDispatcherRules()); + + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/rest/API+Pastries/0.0.1/pastries/Millefeuille")); + assertTrue(operation.getResourcePaths().contains("/rest/API+Pastries/0.0.1/pastries/Eclair+Cafe")); + + List messages = null; + try { + messages = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(2, messages.size()); + for (Exchange exchange : messages) { + if (exchange instanceof RequestResponsePair pair) { + Request request = pair.getRequest(); + Response response = pair.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals(request.getName(), response.getName()); + assertEquals("application/json;charset=UTF-8", response.getMediaType()); + if ("2023-08-17T08:43:38.913Z".equals(request.getName())) { + assertEquals("200", response.getStatus()); + assertEquals("/part1=Millefeuille", response.getDispatchCriteria()); + assertNotNull(response.getContent()); + assertTrue(response.getContent().contains("Delicieux Millefeuille")); + } else if ("2023-08-17T08:43:48.168Z".equals(request.getName())) { + assertEquals("200", response.getStatus()); + assertEquals("/part1=Eclair Cafe", response.getDispatchCriteria()); + assertNotNull(response.getContent()); + assertTrue(response.getContent().contains("Delicieux Eclair au Cafe")); + } else { + fail("Unknown request name: " + request.getName()); + } + } + } + + } else if ("PATCH /rest/API+Pastries/0.0.1/pastries/Eclair+Cafe".equals(operation.getName())) { + assertEquals("PATCH", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/rest/API+Pastries/0.0.1/pastries/Eclair+Cafe")); + + List messages = null; + try { + messages = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(1, messages.size()); + Exchange exchange = messages.get(0); + if (exchange instanceof RequestResponsePair pair) { + Request request = pair.getRequest(); + Response response = pair.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals(request.getName(), response.getName()); + assertEquals("application/json;charset=UTF-8", response.getMediaType()); + if ("2023-08-17T11:05:53.434+02:00".equals(request.getName())) { + assertEquals("200", response.getStatus()); + assertNotNull(response.getContent()); + assertTrue(response.getContent().contains("price")); + } else { + fail("Unknown request name: " + request.getName()); + } + } + } else { + fail("Found an unknown operation! " + operation.getName()); + } + } + } + + @Test + void testComplexHARImportWithAPIPrefix() { + HARImporter importer = null; + try { + importer = new HARImporter( + "target/test-classes/io/github/microcks/util/har/api-pastries-0.0.1-with-prefix.har"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("API Pastries", service.getName()); + assertEquals("0.0.1", service.getVersion()); + + assertEquals(3, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /pastries".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertEquals("size", operation.getDispatcherRules()); + + List messages = null; + try { + messages = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(3, messages.size()); + + } else if ("GET /pastries/{part1}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("part1", operation.getDispatcherRules()); + + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/pastries/Millefeuille")); + assertTrue(operation.getResourcePaths().contains("/pastries/Eclair+Cafe")); + + List messages = null; + try { + messages = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(2, messages.size()); + for (Exchange exchange : messages) { + if (exchange instanceof RequestResponsePair pair) { + Request request = pair.getRequest(); + Response response = pair.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals(request.getName(), response.getName()); + assertEquals("application/json;charset=UTF-8", response.getMediaType()); + if ("2023-08-17T08:43:38.913Z".equals(request.getName())) { + assertEquals("200", response.getStatus()); + assertEquals("/part1=Millefeuille", response.getDispatchCriteria()); + assertNotNull(response.getContent()); + assertTrue(response.getContent().contains("Delicieux Millefeuille")); + } else if ("2023-08-17T08:43:48.168Z".equals(request.getName())) { + assertEquals("200", response.getStatus()); + assertEquals("/part1=Eclair Cafe", response.getDispatchCriteria()); + assertNotNull(response.getContent()); + assertTrue(response.getContent().contains("Delicieux Eclair au Cafe")); + } else { + fail("Unknown request name: " + request.getName()); + } + } + } + + } else if ("PATCH /pastries/Eclair+Cafe".equals(operation.getName())) { + assertEquals("PATCH", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/pastries/Eclair+Cafe")); + + List messages = null; + try { + messages = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(1, messages.size()); + } else { + fail("Found an unknown operation! " + operation.getName()); + } + } + } + + @Test + void testGraphQLMessageImportWithExistingOperation() { + HARImporter importer = null; + try { + importer = new HARImporter("target/test-classes/io/github/microcks/util/har/movie-graph-api-1.0.har"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + + Service service = services.get(0); + assertEquals("Movie Graph API", service.getName()); + assertEquals("1.0", service.getVersion()); + assertEquals(ServiceType.GRAPHQL, service.getType()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().get(0); + assertEquals("film", operation.getName()); + assertEquals("QUERY", operation.getMethod()); + + // Now simulate a secondary artifact import. + operation.setDispatcher(DispatchStyles.QUERY_ARGS); + operation.setDispatcherRules("id"); + + List messages = null; + try { + messages = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, messages.size()); + + Exchange exchange = messages.get(0); + if (exchange instanceof RequestResponsePair pair) { + Request request = pair.getRequest(); + assertEquals("2023-08-23T11:15:01.184+02:00", request.getName()); + assertTrue(request.getContent().contains("query film ($id: String)")); + assertTrue(request.getContent().contains("\"variables\":{")); + assertTrue(request.getContent().contains("\"id\": \"3\"")); + + Response response = pair.getResponse(); + assertEquals("2023-08-23T11:15:01.184+02:00", response.getName()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertEquals("?id=3", response.getDispatchCriteria()); + assertNotNull(response.getContent()); + } else { + fail("Exchange is of unexpected type"); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/ExamplesExporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/ExamplesExporterTest.java new file mode 100644 index 000000000..f8d4a8c9f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/ExamplesExporterTest.java @@ -0,0 +1,166 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.metadata; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.MockRepositoryExportException; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * This is a test case for class ExamplesExporter. + * @author laurent + */ +class ExamplesExporterTest { + + private static final String EXPECTED_REST_SERVICE_EXPORT = """ + apiVersion: mocks.microcks.io/v1alpha1 + kind: APIExamples + metadata: + name: API Pastry - 2.1 + version: 2.1.0 + operations: + GET /pastry/{name}: + Eclair Chocolat: + request: + parameters: + name: Eclair Chocolat + response: + headers: + x-powered-by: microcks + status: 200 + mediaType: application/json + body: |- + { + "name": "Eclair Chocolat", + "price": 2.5 + } + """; + + private static final String EXPECTED_EVENT_SERVICE_EXPORT = """ + apiVersion: mocks.microcks.io/v1alpha1 + kind: APIExamples + metadata: + name: User signed-up API + version: 0.1.0 + operations: + SUBSCRIBE /user/signedup: + jane: + eventMessage: + headers: + my-app-header: 123 + payload: |- + { + "fullName": "Jane Doe", + "email": "jane@microcks.io", + "age": 35 + } + """; + + @Test + void testExportRESTService() { + Service service = new Service(); + service.setId("123-456-789"); + service.setName("API Pastry - 2.1"); + service.setVersion("2.1.0"); + service.setType(ServiceType.REST); + + Operation operation = new Operation(); + operation.setName("GET /pastry/{name}"); + service.setOperations(List.of(operation)); + + Request request = new Request(); + request.setName("Eclair Chocolat"); + Parameter nameParameter = new Parameter(); + nameParameter.setName("name"); + nameParameter.setValue("Eclair Chocolat"); + request.addQueryParameter(nameParameter); + + Response response = new Response(); + response.setName("Eclair Chocolat"); + response.setMediaType("application/json"); + response.setStatus("200"); + response.setContent("{\n \"name\": \"Eclair Chocolat\",\n \"price\": 2.5\n}"); + Header poweredHeader = new Header(); + poweredHeader.setName("x-powered-by"); + poweredHeader.setValues(Set.of("microcks")); + response.addHeader(poweredHeader); + + String exportResult = null; + ExamplesExporter exporter = new ExamplesExporter(); + + try { + exporter.addServiceDefinition(service); + exporter.addMessageDefinitions(service, operation, List.of(new RequestResponsePair(request, response))); + exportResult = exporter.exportAsString(); + } catch (MockRepositoryExportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(EXPECTED_REST_SERVICE_EXPORT, exportResult); + } + + @Test + void testExportEventService() { + Service service = new Service(); + service.setId("123-456-789"); + service.setName("User signed-up API"); + service.setVersion("0.1.0"); + service.setType(ServiceType.EVENT); + + Operation operation = new Operation(); + operation.setName("SUBSCRIBE /user/signedup"); + service.setOperations(List.of(operation)); + + EventMessage eventMessage = new EventMessage(); + eventMessage.setName("jane"); + + Header appHeader = new Header(); + appHeader.setName("my-app-header"); + appHeader.setValues(Set.of("123")); + eventMessage.setHeaders(Set.of(appHeader)); + eventMessage.setContent("{\n \"fullName\": \"Jane Doe\",\n \"email\": \"jane@microcks.io\",\n \"age\": 35\n}"); + + String exportResult = null; + ExamplesExporter exporter = new ExamplesExporter(); + + try { + exporter.addServiceDefinition(service); + exporter.addMessageDefinitions(service, operation, List.of(new UnidirectionalEvent(eventMessage))); + exportResult = exporter.exportAsString(); + } catch (MockRepositoryExportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(EXPECTED_EVENT_SERVICE_EXPORT, exportResult); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/ExamplesImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/ExamplesImporterTest.java new file mode 100644 index 000000000..65910ed1d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/ExamplesImporterTest.java @@ -0,0 +1,352 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.metadata; + +import io.github.microcks.domain.EventMessage; +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.domain.UnidirectionalEvent; +import io.github.microcks.util.MockRepositoryImportException; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class ExamplesImporter. + * @author laurent + */ +class ExamplesImporterTest { + + @Test + void testOpenAPIAPIExamplesImporter() { + ExamplesImporter importer = null; + try { + importer = new ExamplesImporter( + "target/test-classes/io/github/microcks/util/metadata/APIPastry-2.0-examples.yml"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("API Pastry - 2.0", service.getName()); + assertEquals("2.0.0", service.getVersion()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().get(0); + + assertEquals("GET /pastry/{name}", operation.getName()); + + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + // No dispatcher has been seet but we should have at least the resource path pattern. + assertNull(operation.getDispatcher()); + assertNotNull(operation.getResourcePaths()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/pastry/{name}")); + + assertNotNull(exchanges); + assertEquals(3, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair pair) { + assertNotNull(pair.getRequest()); + assertNotNull(pair.getResponse()); + assertEquals(pair.getRequest().getName(), pair.getResponse().getName()); + + assertNotNull(pair.getRequest().getQueryParameters()); + assertEquals(1, pair.getRequest().getQueryParameters().size()); + + Parameter parameter = pair.getRequest().getQueryParameters().get(0); + assertEquals("name", parameter.getName()); + + if ("Eclair Chocolat".equals(pair.getRequest().getName())) { + assertEquals("Eclair Chocolat", parameter.getValue()); + + // Check that content has been transformed in JSON. + assertEquals("application/json", pair.getResponse().getMediaType()); + assertEquals( + "{\"name\":\"Eclair Chocolat\",\"description\":\"Delicieux Eclair Chocolat pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"unknown\"}", + pair.getResponse().getContent()); + } else if ("Eclair Chocolat Xml".equals(pair.getRequest().getName())) { + assertEquals("Eclair Chocolat Xml", parameter.getValue()); + + // Check that content has been read as is (XML). + assertEquals("text/xml", pair.getResponse().getMediaType()); + assertEquals("\n" + " Eclair Cafe\n" + + " Delicieux Eclair au Chocolat pas calorique du tout\n" + + " M\n" + " 2.5\n" + " unknown\n" + "", + pair.getResponse().getContent()); + } else if ("Eclair Chocolat Empty Status".equals(pair.getRequest().getName())) { + assertEquals("Eclair Chocolat", parameter.getValue()); + + // Check that content has been transformed in JSON. + assertEquals("application/json", pair.getResponse().getMediaType()); + assertEquals( + "{\"name\":\"Eclair Chocolat\",\"description\":\"Delicieux Eclair Chocolat pas calorique du tout\",\"size\":\"M\",\"price\":2.5}", + pair.getResponse().getContent()); + // Check that default status value has got set + assertEquals("200", pair.getResponse().getStatus()); + } + } else { + fail("Unknown extracted exchange type"); + } + } + } + + @Test + void testPathAndQueryParametersSplitting() { + ExamplesImporter importer = null; + try { + importer = new ExamplesImporter( + "target/test-classes/io/github/microcks/util/metadata/weather-forecast-examples.yml"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("WeatherForecast API", service.getName()); + assertEquals("1.0.0", service.getVersion()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().get(0); + + assertEquals("GET /forecast/{region}", operation.getName()); + + // Force the operation dispatcher to URI_ELEMENTS to check if importer correctly + // splits the query from the path parameters when computing dispatch criteria. + operation.setDispatcher("URI_ELEMENTS"); + operation.setDispatcherRules("region && apiKey"); + + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertNotNull(exchanges); + assertEquals(1, exchanges.size()); + + Exchange exchange = exchanges.get(0); + assertTrue(exchange instanceof RequestResponsePair); + + RequestResponsePair pair = (RequestResponsePair) exchange; + assertNotNull(pair.getRequest()); + assertNotNull(pair.getRequest().getQueryParameters()); + assertEquals(2, pair.getRequest().getQueryParameters().size()); + + assertNotNull(pair.getResponse()); + assertEquals("/region=north?apiKey=123456", pair.getResponse().getDispatchCriteria()); + } + + @Test + void testHeadersDispatcherExtractiong() { + ExamplesImporter importer = null; + try { + importer = new ExamplesImporter( + "target/test-classes/io/github/microcks/util/metadata/weather-forecast-headers-examples.yml"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("WeatherForecast API", service.getName()); + assertEquals("1.0.0", service.getVersion()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().get(0); + + assertEquals("GET /forecast", operation.getName()); + + // Force the operation dispatcher to URI_ELEMENTS to check if importer correctly + // splits the query from the path parameters when computing dispatch criteria. + operation.setDispatcher("QUERY_HEADER"); + operation.setDispatcherRules("region"); + + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertNotNull(exchanges); + assertEquals(1, exchanges.size()); + + Exchange exchange = exchanges.get(0); + assertTrue(exchange instanceof RequestResponsePair); + + RequestResponsePair pair = (RequestResponsePair) exchange; + assertNotNull(pair.getRequest()); + assertNull(pair.getRequest().getQueryParameters()); + + assertNotNull(pair.getResponse()); + assertEquals("?region=north", pair.getResponse().getDispatchCriteria()); + } + + @Test + void testGRPCAPIExamplesImporter() { + ExamplesImporter importer = null; + try { + importer = new ExamplesImporter( + "target/test-classes/io/github/microcks/util/metadata/hello-grpc-v1-examples.yml"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("io.github.microcks.grpc.hello.v1.HelloService", service.getName()); + assertEquals("v1", service.getVersion()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().get(0); + + assertEquals("greeting", operation.getName()); + + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertNotNull(exchanges); + assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair pair) { + assertNotNull(pair.getRequest()); + assertNotNull(pair.getResponse()); + assertEquals(pair.getRequest().getName(), pair.getResponse().getName()); + + if ("Laurent".equals(pair.getRequest().getName())) { + // Check that content has been transformed in JSON. + assertEquals("{\"firstname\":\"Laurent\",\"lastname\":\"Broudoux\"}", pair.getRequest().getContent()); + assertEquals("{\"greeting\":\"Hello Laurent Broudoux !\"}", pair.getResponse().getContent()); + } else if ("John".equals(pair.getRequest().getName())) { + // Check that content has been read and transformed in JSON. + assertEquals("{\"firstname\": \"John\", \"lastname\": \"Doe\"}", pair.getRequest().getContent()); + assertEquals("{\"greeting\":\"Hello John Doe !\"}", pair.getResponse().getContent()); + } + } else { + fail("Unknown extracted exchange type"); + } + } + } + + @Test + void testAsyncAPIExamplesImporter() { + ExamplesImporter importer = null; + try { + importer = new ExamplesImporter( + "target/test-classes/io/github/microcks/util/metadata/user-signedup-0.1.0-examples.yml"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("User signed-up API", service.getName()); + assertEquals("0.1.0", service.getVersion()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().get(0); + + assertEquals("SUBSCRIBE /user/signedup", operation.getName()); + + List exchanges = null; + try { + service.setType(ServiceType.EVENT); + exchanges = importer.getMessageDefinitions(service, operation); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertNotNull(exchanges); + assertEquals(1, exchanges.size()); + + Exchange exchange = exchanges.get(0); + assertInstanceOf(UnidirectionalEvent.class, exchange); + + EventMessage event = ((UnidirectionalEvent) exchange).getEventMessage(); + + assertNotNull(event); + assertEquals("jane", event.getName()); + assertEquals("{\"fullName\":\"Jane Doe\",\"email\":\"jane@microcks.io\",\"age\":35}", event.getContent()); + + assertNotNull(event.getHeaders()); + assertEquals(2, event.getHeaders().size()); + + for (Header header : event.getHeaders()) { + assertTrue("my-app-header".equals(header.getName()) || "sentAt".equals(header.getName())); + assertTrue(header.getValues().contains("123") || header.getValues().contains("2024-07-14T18:01:28Z")); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/MetadataImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/MetadataImporterTest.java new file mode 100644 index 000000000..6f2a12dc1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/metadata/MetadataImporterTest.java @@ -0,0 +1,117 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.metadata; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.Service; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class MetadataImporter. + * @author laurent + */ +class MetadataImporterTest { + + @Test + void testAPIMetadataImport() { + MetadataImporter importer = null; + try { + importer = new MetadataImporter( + "target/test-classes/io/github/microcks/util/metadata/hello-grpc-v1-metadata.yml"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.getFirst(); + assertEquals("HelloService", service.getName()); + assertEquals("v1", service.getVersion()); + + assertEquals(3, service.getMetadata().getLabels().size()); + assertEquals("greeting", service.getMetadata().getLabels().get("domain")); + assertEquals("stable", service.getMetadata().getLabels().get("status")); + assertEquals("Team A", service.getMetadata().getLabels().get("team")); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().getFirst(); + + assertEquals("POST /greeting", operation.getName()); + assertEquals(Long.valueOf(100), operation.getDefaultDelay()); + assertEquals(DispatchStyles.JSON_BODY, operation.getDispatcher()); + assertNotNull(operation.getDispatcherRules()); + } + + @Test + void testAPIMetadataWithParmaeterConstraintsImport() { + MetadataImporter importer = null; + try { + importer = new MetadataImporter( + "target/test-classes/io/github/microcks/util/metadata/APIPastry-2.0-metadata.yml"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.getFirst(); + assertEquals("API Pastry - 2.0", service.getName()); + assertEquals("2.0.0", service.getVersion()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().getFirst(); + + assertEquals("GET /pastry/{name}", operation.getName()); + assertEquals(Long.valueOf(100), operation.getDefaultDelay()); + assertNotNull(operation.getParameterConstraints()); + assertEquals(2, operation.getParameterConstraints().size()); + + for (ParameterConstraint constraint : operation.getParameterConstraints()) { + if ("Authorization".equals(constraint.getName())) { + assertTrue(constraint.isRequired()); + assertFalse(constraint.isRecopy()); + assertEquals("^Bearer\\s[a-zA-Z0-9\\._-]+$", constraint.getMustMatchRegexp()); + } else if ("x-request-id".equals(constraint.getName())) { + assertTrue(constraint.isRequired()); + assertTrue(constraint.isRecopy()); + assertNull(constraint.getMustMatchRegexp()); + } else { + fail("Unexpected parameter constraint"); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIImporterTest.java new file mode 100644 index 000000000..20c093527 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIImporterTest.java @@ -0,0 +1,1872 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.ParameterConstraint; +import io.github.microcks.domain.ParameterLocation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.ReferenceResolver; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class OpenAPIImporter. + * @author laurent + */ +class OpenAPIImporterTest { + + @ParameterizedTest + @ValueSource(strings = { "target/test-classes/io/github/microcks/util/openapi/cars-openapi.yaml", + "target/test-classes/io/github/microcks/util/openapi/cars-openapi.json", + "target/test-classes/io/github/microcks/util/openapi/cars-openapi-quoted.yaml", + "target/test-classes/io/github/microcks/util/openapi/cars-openapi-spacesops.yaml" }) + void testSimpleOpenAPI(String specificationFile) { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter(specificationFile, null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + importAndAssertOnSimpleOpenAPI(importer); + } + + @ParameterizedTest + @ValueSource(strings = { "target/test-classes/io/github/microcks/util/openapi/cars-openapi-extensions.yaml", + "target/test-classes/io/github/microcks/util/openapi/cars-openapi-extensions.json" }) + void testSimpleOpenAPIWithExtensions(String specificationFile) { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter(specificationFile, null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + importAndAssertOnSimpleOpenAPIWithExtensions(importer); + } + + @Test + void testApicurioPetstoreOpenAPI() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter("target/test-classes/io/github/microcks/util/openapi/petstore-openapi.json", + null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("PetStore API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that operations and input/output have been found. + assertEquals(4, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /pets".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertEquals("tags && limit", operation.getDispatcherRules()); + + } else if ("GET /pets/{id}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("id", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/pets/1")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("zaza", request.getName()); + assertEquals("zaza", response.getName()); + assertEquals("/id=1", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("POST /pets".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertNull(operation.getDispatcher()); + assertNull(operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/pets")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("tigresse", request.getName()); + assertEquals("tigresse", response.getName()); + assertNull(response.getDispatchCriteria()); + assertEquals("201", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("DELETE /pets/{id}".equals(operation.getName())) { + assertEquals("DELETE", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("id", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(0, exchanges.size()); + assertNull(operation.getResourcePaths()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testSimpleOpenAPIImportYAMLNoDashesWithJSON() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/cars-openapi-with-json.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("OpenAPI Car API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + } + + @Test + void testOpenAPIWithOpsPathParameter() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter("target/test-classes/io/github/microcks/util/openapi/locations-openapi.json", + null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("LocationById", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0", service.getVersion()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /location/{id}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("id", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/location/83")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("location", request.getName()); + assertEquals("location", response.getName()); + assertEquals("/id=83", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testOpenAPIImportYAMLWithHeaders() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter("target/test-classes/io/github/microcks/util/openapi/cars-openapi-headers.yaml", + null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("OpenAPI Car API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operation and input/output have been found. + assertEquals(1, service.getOperations().size()); + + Operation operation = service.getOperations().get(0); + assertEquals("GET /owner/{owner}/car", operation.getName()); + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + assertEquals("owner ?? page && limit && x-user-id", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_cars", request.getName()); + assertEquals("laurent_cars", response.getName()); + assertEquals("/owner=laurent?limit=20?page=0", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + + // Check headers now. + assertEquals(2, request.getHeaders().size()); + Iterator
headers = request.getHeaders().iterator(); + while (headers.hasNext()) { + Header header = headers.next(); + if ("x-user-id".equals(header.getName())) { + assertEquals(1, header.getValues().size()); + assertEquals("poiuytrezamlkjhgfdsq", header.getValues().iterator().next()); + } else if ("Accept".equals(header.getName())) { + assertEquals(1, header.getValues().size()); + assertEquals("application/json", header.getValues().iterator().next()); + } else { + fail("Unexpected header name in request"); + } + } + + assertEquals(1, response.getHeaders().size()); + Header header = response.getHeaders().iterator().next(); + assertEquals("x-result-count", header.getName()); + assertEquals(1, header.getValues().size()); + assertEquals("2", header.getValues().iterator().next()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } + + @Test + void testOpenAPIJsonPointer() { + try { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + byte[] bytes = Files + .readAllBytes(Paths.get("target/test-classes/io/github/microcks/util/openapi/cars-openapi.yaml")); + JsonNode openapiSpec = mapper.readTree(bytes); + + String verb = "get"; + String path = "/owner/{owner}/car"; + + String pointer = "/paths/" + path.replace("/", "~1") + "/" + verb + "/responses/200/content/" + + "application/json".replace("/", "~1"); + + JsonNode responseNode = openapiSpec.at(pointer); + assertNotNull(responseNode); + assertFalse(responseNode.isMissingNode()); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + } + + @Test + void testCompleteOpenAPIImportYAML() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/cars-openapi-complete.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("OpenAPI Car API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(4, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /owner/{owner}/car".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + assertEquals("owner ?? page && limit", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_cars", request.getName()); + assertEquals("laurent_cars", response.getName()); + assertEquals("/owner=laurent?limit=20?page=0", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("POST /owner/{owner}/car".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + e.printStackTrace(); + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_307", request.getName()); + assertEquals("laurent_307", response.getName()); + assertEquals("/owner=laurent", response.getDispatchCriteria()); + assertEquals("201", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("GET /owner/{owner}/car/{car}/passenger".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner && car", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car/307/passenger")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_307_passengers", request.getName()); + assertEquals("laurent_307_passengers", response.getName()); + assertEquals("/car=307/owner=laurent", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("POST /owner/{owner}/car/{car}/passenger".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner && car", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(0, exchanges.size()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testCompleteOpenAPI31ImportYAML() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/cars-openapi-3.1-complete.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("OpenAPI Car API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(4, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /owner/{owner}/car".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + assertEquals("owner ?? page && limit", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_cars", request.getName()); + assertEquals("laurent_cars", response.getName()); + assertEquals("/owner=laurent?limit=20?page=0", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("POST /owner/{owner}/car".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + e.printStackTrace(); + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_307", request.getName()); + assertEquals("laurent_307", response.getName()); + assertEquals("/owner=laurent", response.getDispatchCriteria()); + assertEquals("201", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("GET /owner/{owner}/car/{car}/passenger".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner && car", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car/307/passenger")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_307_passengers", request.getName()); + assertEquals("laurent_307_passengers", response.getName()); + assertEquals("/car=307/owner=laurent", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("POST /owner/{owner}/car/{car}/passenger".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner && car", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(0, exchanges.size()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testUncompleteParamsOpenAPIImportYAML() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/cars-openapi-uncomplete-params.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("OpenAPI Car API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(3, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /owner/{owner}/car".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + assertEquals("owner ?? page && limit", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(0, exchanges.size()); + } else if ("POST /owner/{owner}/car".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_307", request.getName()); + assertEquals("laurent_307", response.getName()); + assertEquals("/owner=laurent", response.getDispatchCriteria()); + assertEquals("201", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("GET /owner/{owner}/car/{car}/passenger".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner && car", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + + // TODO: below should be a failure. We currently detect 1 message as we should have 0 + // cause car path parameters is missing. We should add a test into URIBuilder.buildFromParamsMap() + // to check that we have at least the number of params what we have into uri pattern. + //assertEquals(0, messages.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_307_passengers", request.getName()); + assertEquals("laurent_307_passengers", response.getName()); + assertEquals("/owner=laurent", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testExampleValueDeserializationYAML() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter("target/test-classes/io/github/microcks/util/openapi/test-openapi.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + importAndAssertOnTestOpenAPI(importer); + } + + @ParameterizedTest + @ValueSource(strings = { "target/test-classes/io/github/microcks/util/openapi/test-openapi-yaml.yaml", + "target/test-classes/io/github/microcks/util/openapi/test-openapi.json", + "target/test-classes/io/github/microcks/util/openapi/test-openapi-json.json" }) + void testExampleValueDeserialization(String specificationFile) { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter(specificationFile, null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + importAndAssertOnTestOpenAPI(importer); + } + + @Test + void testResponseRefsOpenAPIImport() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/cars-openapi-complex-refs.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("OpenAPI Car API with Refs", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("GET /owner/{owner}/car".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + assertEquals("owner ?? page && limit && x-user-id", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + + if ("laurent_cars".equals(request.getName())) { + assertEquals("/owner=laurent?limit=20?page=0", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else if ("unknown".equals(request.getName())) { + assertEquals("/owner=unknown?limit=20?page=0", response.getDispatchCriteria()); + assertEquals("404", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertEquals("{\"reason\": \"owner not found\"}", response.getContent()); + assertEquals(1, response.getHeaders().size()); + + Header header = response.getHeaders().iterator().next(); + assertEquals("my-custom-header", header.getName()); + assertEquals("unknown", header.getValues().iterator().next()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testParameterRefsOpenAPIImport() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter("target/test-classes/io/github/microcks/util/openapi/param-refs-openapi.yaml", + null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Sample API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("GET /accounts/{accountId}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("accountId", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/accounts/396be545-e2d4-4497-a5b5-700e89ab99c0")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + + if ("Example 1".equals(request.getName())) { + assertEquals("/accountId=396be545-e2d4-4497-a5b5-700e89ab99c0", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + assertEquals("{\"account\":{\"resourceId\":\"f377afb3-5c62-40cc-8f07-1f4749a780eb\"}}", + response.getContent()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testQueryParameterRefsOpenAPIImport() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/query-param-refs-openapi.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("API-Template", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that operations and input/output have been found. + assertEquals(2, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("GET /accounts".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertEquals("level", operation.getDispatcherRules()); + } else if ("GET /resources".equals(operation.getName())) { + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertEquals("resourceType", operation.getDispatcherRules()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testExamplesRefsOpenAPIImport() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter("target/test-classes/io/github/microcks/util/openapi/examples-ref-openapi.yaml", + null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Broken Ref", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("2.0.0", service.getVersion()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + + for (Operation operation : service.getOperations()) { + + if ("GET /v1.0/endpoint".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/v1.0/endpoint")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + + assertEquals("example1", request.getName()); + assertEquals("example1", response.getName()); + assertEquals("someValue", response.getContent()); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testExternalRelativeReferenceOpenAPIImport() { + OpenAPIImporter importer = null; + ReferenceResolver resolver = new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.5.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml", + null, true); + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml", + resolver); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("WeatherForecast API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + List resources = importer.getResourceDefinitions(service); + assertEquals(2, resources.size()); + + Resource openAPISpec = resources.get(0); + assertEquals("WeatherForecast API-1.0.0.yaml", openAPISpec.getName()); + assertEquals(ResourceType.OPEN_API_SPEC, openAPISpec.getType()); + assertTrue(openAPISpec.getContent().contains("WeatherForecast+API-1.0.0--weather-forecast-schema.yaml")); + + Resource refSchema = resources.get(1); + assertEquals("WeatherForecast API-1.0.0--weather-forecast-schema.yaml", refSchema.getName()); + assertEquals(ResourceType.JSON_SCHEMA, refSchema.getType()); + assertEquals("./weather-forecast-schema.yaml", refSchema.getPath()); + assertNotNull(refSchema.getContent()); + assertTrue(refSchema.getContent().contains("A weather forecast for a requested region")); + } + + @Test + void testExternalRelativeReferenceWithJSONPointerOpenAPIImport() { + OpenAPIImporter importer = null; + ReferenceResolver resolver = new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref-example.yaml", + null, true); + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref-example.yaml", + resolver); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("WeatherForecast API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + List resources = importer.getResourceDefinitions(service); + assertEquals(3, resources.size()); + + Resource openAPISpec = resources.get(0); + assertEquals("WeatherForecast API-1.0.0.yaml", openAPISpec.getName()); + assertEquals(ResourceType.OPEN_API_SPEC, openAPISpec.getType()); + assertTrue(openAPISpec.getContent().contains("WeatherForecast+API-1.0.0--weather-forecast-schema.yaml")); + + for (int i = 1; i < 3; i++) { + Resource refResource = resources.get(i); + if ("WeatherForecast API-1.0.0--weather-examples.json".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_FRAGMENT, refResource.getType()); + assertEquals("./weather-examples.json", refResource.getPath()); + assertNotNull(refResource.getContent()); + assertTrue(refResource.getContent().contains("\"region\": \"east\"")); + } else if ("WeatherForecast API-1.0.0--weather-forecast-schema.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_SCHEMA, refResource.getType()); + assertEquals("./weather-forecast-schema.yaml", refResource.getPath()); + assertNotNull(refResource.getContent()); + assertTrue(refResource.getContent().contains("A weather forecast for a requested region")); + } else { + fail("Unknown ref resource found"); + } + } + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /forecast/{region}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(5, exchanges.size()); + assertEquals(5, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/forecast/north")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertNotNull(response.getContent()); + + if ("unknown".equals(request.getName())) { + assertEquals("Region is unknown. Choose in north, west, east or south.", response.getContent()); + } else { + assertEquals("/region=" + request.getName(), response.getDispatchCriteria()); + assertTrue(response.getContent().contains("\"region\":\"" + request.getName() + "\"")); + } + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testExternalAbsoluteReferenceOpenAPIImport() { + OpenAPIImporter importer = null; + ReferenceResolver resolver = new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.5.x/webapp/src/test/resources/io/github/microcks/util/openapi/", + null, true); + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref.yaml", + resolver); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.getFirst(); + assertEquals("WeatherForecast API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().getFirst(); + assertEquals(1, operation.getParameterConstraints().size()); + ParameterConstraint constraint = operation.getParameterConstraints().iterator().next(); + assertEquals("apiKey", constraint.getName()); + assertTrue(constraint.isRequired()); + assertEquals(ParameterLocation.query, constraint.getIn()); + + List resources = importer.getResourceDefinitions(service); + assertEquals(2, resources.size()); + + Resource openAPISpec = resources.getFirst(); + assertEquals("WeatherForecast API-1.0.0.yaml", openAPISpec.getName()); + assertEquals(ResourceType.OPEN_API_SPEC, openAPISpec.getType()); + assertFalse(openAPISpec.getContent().contains("WeatherForecast API-1.0.0-weather-forecast-schema.yaml")); + + Resource refSchema = resources.get(1); + assertEquals("WeatherForecast API-1.0.0-weather-forecast-schema.yaml", refSchema.getName()); + assertEquals(ResourceType.JSON_SCHEMA, refSchema.getType()); + assertEquals( + "https://raw.githubusercontent.com/microcks/microcks/1.5.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml", + refSchema.getPath()); + assertNotNull(refSchema.getContent()); + assertTrue(refSchema.getContent().contains("A weather forecast for a requested region")); + } + + @Test + void testExternalAbsoluteReferenceWithJSONPointerOpenAPIImport() { + OpenAPIImporter importer = null; + ReferenceResolver resolver = new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/openapi/", + null, true); + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref-pointers.yaml", + resolver); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("WeatherForecast API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + List resources = importer.getResourceDefinitions(service); + assertEquals(3, resources.size()); + + Resource openAPISpec = resources.get(0); + assertEquals("WeatherForecast API-1.0.0.yaml", openAPISpec.getName()); + assertEquals(ResourceType.OPEN_API_SPEC, openAPISpec.getType()); + assertFalse(openAPISpec.getContent().contains("WeatherForecast API-1.0.0-weather-forecast-schema.yaml")); + + for (int i = 1; i < 3; i++) { + Resource refResource = resources.get(i); + if ("WeatherForecast API-1.0.0-weather-forecast-common.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_FRAGMENT, refResource.getType()); + assertEquals( + "https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-common.yaml", + refResource.getPath()); + assertNotNull(refResource.getContent()); + assertTrue(refResource.getContent().contains("title: Common objects to reuse")); + } else if ("WeatherForecast API-1.0.0-weather-forecast-schema.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_SCHEMA, refResource.getType()); + assertEquals( + "https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml", + refResource.getPath()); + assertNotNull(refResource.getContent()); + assertTrue(refResource.getContent().contains("A weather forecast for a requested region")); + } else { + fail("Unknown ref resource found"); + } + } + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /forecast/{region}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(5, exchanges.size()); + assertEquals(5, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/forecast/north")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertNotNull(response.getContent()); + + if ("unknown".equals(request.getName())) { + assertEquals("Region is unknown. Choose in north, west, east or south.", response.getContent()); + } else { + assertEquals("/region=" + request.getName(), response.getDispatchCriteria()); + assertTrue(response.getContent().contains("\"region\":\"" + request.getName() + "\"")); + } + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testExternalRelativeRecursiveReferenceWithJSONPointerOpenAPIImport() { + OpenAPIImporter importer = null; + ReferenceResolver resolver = new ReferenceResolver( + "https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-recursive-ref.yaml", + null, true); + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/weather-forecast-openapi-relative-recursive-ref.yaml", + resolver); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("WeatherForecast API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + List resources = importer.getResourceDefinitions(service); + assertEquals(4, resources.size()); + + Resource openAPISpec = resources.get(0); + assertEquals("WeatherForecast API-1.0.0.yaml", openAPISpec.getName()); + assertEquals(ResourceType.OPEN_API_SPEC, openAPISpec.getType()); + assertTrue(openAPISpec.getContent().contains("WeatherForecast+API-1.0.0--weather-forecast-schema.yaml")); + + for (int i = 1; i < 4; i++) { + Resource refResource = resources.get(i); + if ("WeatherForecast API-1.0.0--weather-forecast-schema.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_SCHEMA, refResource.getType()); + assertEquals("./weather-forecast-schema.yaml", refResource.getPath()); + assertNotNull(refResource.getContent()); + assertTrue(refResource.getContent().contains("A weather forecast for a requested region")); + } else if ("WeatherForecast API-1.0.0--weather-forecast-examples.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_FRAGMENT, refResource.getType()); + assertEquals("./weather-forecast-examples.yaml", refResource.getPath()); + assertNotNull(refResource.getContent()); + assertTrue(refResource.getContent() + .contains("$ref: 'WeatherForecast+API-1.0.0--weather-forecast-common-regions.yaml#/regions/north'")); + } else if ("WeatherForecast API-1.0.0--weather-forecast-common-regions.yaml".equals(refResource.getName())) { + assertEquals(ResourceType.JSON_FRAGMENT, refResource.getType()); + assertEquals("./weather-forecast-common-regions.yaml", refResource.getPath()); + assertNotNull(refResource.getContent()); + assertTrue(refResource.getContent().contains("title: Common regions objects to reuse")); + } else { + fail("Unknown ref resource found"); + } + } + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /forecast/{region}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(5, exchanges.size()); + assertEquals(5, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/forecast/north")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertNotNull(response.getContent()); + + if ("unknown".equals(request.getName())) { + assertEquals("Region is unknown. Choose in north, west, east or south.", response.getContent()); + } else { + assertEquals("/region=" + request.getName(), response.getDispatchCriteria()); + assertTrue(response.getContent().contains("\"region\":\"" + request.getName() + "\"")); + } + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testNoContentResponseOpenAPIImport() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter( + "target/test-classes/io/github/microcks/util/openapi/test-openapi-nocontent.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Test API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + + // Check that operations and input/output have been found. + assertEquals(3, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("DELETE /tests/{id}".equals(operation.getName())) { + assertEquals("DELETE", operation.getMethod()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/tests/66") + || operation.getResourcePaths().contains("/tests/77")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertNull(response.getContent()); + + if ("to-delete-1".equals(request.getName())) { + assertEquals("204", response.getStatus()); + assertEquals("/id=66", response.getDispatchCriteria()); + assertFalse(response.isFault()); + assertEquals(1, request.getQueryParameters().size()); + } else if ("to-delete-2".equals(request.getName())) { + assertEquals("418", response.getStatus()); + assertEquals("/id=77", response.getDispatchCriteria()); + assertTrue(response.isFault()); + assertEquals(1, request.getQueryParameters().size()); + } else { + fail("Unknown request"); + } + } + } + } + } + } + + private void importAndAssertOnSimpleOpenAPI(OpenAPIImporter importer) { + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("OpenAPI Car API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(3, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /owner/{owner}/car".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + assertEquals("owner ?? page && limit", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_cars", request.getName()); + assertEquals("laurent_cars", response.getName()); + assertEquals("/owner=laurent?limit=20?page=0", response.getDispatchCriteria()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("POST /owner/{owner}/car".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/owner/laurent/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_307", request.getName()); + assertEquals("laurent_307", response.getName()); + assertEquals("/owner=laurent", response.getDispatchCriteria()); + assertEquals("201", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("POST /owner/{owner}/car/{car}/passenger".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("owner && car", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(0, exchanges.size()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + private void importAndAssertOnSimpleOpenAPIWithExtensions(OpenAPIImporter importer) { + // Basic import and assertions. + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("OpenAPI Car API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.OPEN_API_SPEC, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(3, service.getOperations().size()); + + try { + // Now assert extensions parsing has been done. + assertNotNull(service.getMetadata()); + assertEquals(3, service.getMetadata().getLabels().size()); + assertEquals("cars", service.getMetadata().getLabels().get("domain")); + assertEquals("beta", service.getMetadata().getLabels().get("status")); + assertEquals("Team A", service.getMetadata().getLabels().get("team")); + + Operation postOp = service.getOperations().stream() + .filter(operation -> operation.getName().equals("POST /owner/{owner}/car")).findFirst().get(); + + assertEquals("POST", postOp.getMethod()); + assertEquals(100, postOp.getDefaultDelay().longValue()); + assertEquals("SCRIPT", postOp.getDispatcher()); + assertTrue(postOp.getDispatcherRules().contains("groovy.json.JsonSlurper")); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, postOp); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals(1, postOp.getResourcePaths().size()); + assertTrue(postOp.getResourcePaths().contains("/owner/{owner}/car")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("laurent_307", request.getName()); + assertEquals("laurent_307", response.getName()); + assertNull(response.getDispatchCriteria()); + assertEquals("201", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } catch (Exception e) { + e.printStackTrace(); + fail("Exception should not be thrown"); + } + } + + private void importAndAssertOnTestOpenAPI(OpenAPIImporter importer) { + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + + // Check that operations and input/output have been found. + assertEquals(2, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /tests".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertNotNull(response.getContent()); + assertNotEquals(0, response.getContent().length()); + assertTrue(response.getContent().startsWith("[")); + assertTrue(response.getContent().contains("\"some text\"")); + assertTrue(response.getContent().contains("11")); + assertTrue(response.getContent().contains("35")); + assertTrue(response.getContent().endsWith("]")); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("GET /tests/{id}".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertNotNull(response.getContent()); + assertNotEquals(0, response.getContent().length()); + assertTrue(response.getContent().startsWith("{")); + assertTrue(response.getContent().contains("\"foo\":")); + assertTrue(response.getContent().contains("\"bar\":")); + assertTrue(response.getContent().endsWith("}")); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } + } + } + + @Test + void testSimpleOpenAPIWithObjectQueryParam() { + OpenAPIImporter importer = null; + try { + importer = new OpenAPIImporter("target/test-classes/io/github/microcks/util/openapi/object-query-params.yaml", + null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /messiah".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertEquals("lastName && firstName", operation.getDispatcherRules()); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIMcpToolConverterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIMcpToolConverterTest.java new file mode 100644 index 000000000..547bab943 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIMcpToolConverterTest.java @@ -0,0 +1,195 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Service; +import io.github.microcks.listener.ListenerTestsConfiguration; +import io.github.microcks.repository.RepositoryTestsConfiguration; +import io.github.microcks.repository.ResourceRepository; +import io.github.microcks.service.ArtifactInfo; +import io.github.microcks.service.ServiceService; +import io.github.microcks.util.ai.McpSchema; +import io.github.microcks.web.ControllerTestsConfiguration; +import io.github.microcks.web.RestInvocationProcessor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test class for OpenAPIMcpToolConverter. + * @author laurent + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@SpringJUnitConfig(classes = { RepositoryTestsConfiguration.class, ControllerTestsConfiguration.class, + ListenerTestsConfiguration.class }) +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class OpenAPIMcpToolConverterTest { + + @Autowired + private ServiceService serviceService; + + @Autowired + private ResourceRepository resourceRepository; + + @Autowired + private RestInvocationProcessor restInvocationProcessor; + + private Service servicev1; + private Service servicev2; + private OpenAPIMcpToolConverter toolConverterv1; + private OpenAPIMcpToolConverter toolConverterv2; + + @BeforeAll + void setUp() throws Exception { + // Import the petstore service definition from the REST tutorial. + File artifactFilev1 = new File("target/test-classes/io/github/microcks/util/openapi/petstore-1.0.0-openapi.yaml"); + File artifactFilev2 = new File("target/test-classes/io/github/microcks/util/openapi/petstore-2.0.0-openapi.yaml"); + // Extract service and resource. + tool converters. + List services = serviceService.importServiceDefinition(artifactFilev1, null, + new ArtifactInfo("petstore-1.0.0-openapi.yaml", true)); + servicev1 = services.getFirst(); + List resources = resourceRepository.findByServiceIdAndType(servicev1.getId(), + ResourceType.OPEN_API_SPEC); + toolConverterv1 = new OpenAPIMcpToolConverter(servicev1, resources.getFirst(), restInvocationProcessor, + new ObjectMapper()); + // For v2. + services = serviceService.importServiceDefinition(artifactFilev2, null, + new ArtifactInfo("petstore-2.0.0-openapi.yaml", true)); + servicev2 = services.getFirst(); + resources = resourceRepository.findByServiceIdAndType(servicev2.getId(), ResourceType.OPEN_API_SPEC); + toolConverterv2 = new OpenAPIMcpToolConverter(servicev2, resources.getFirst(), restInvocationProcessor, + new ObjectMapper()); + } + + @Test + void testGetToolName() { + for (Operation operation : servicev1.getOperations()) { + String toolName = toolConverterv1.getToolName(operation); + if ("GET /my/pets".equals(operation.getName())) { + assertEquals("get_my_pets", toolName); + } else if ("GET /pets".equals(operation.getName())) { + assertEquals("get_pets", toolName); + } else if ("POST /pets".equals(operation.getName())) { + assertEquals("post_pets", toolName); + } else if ("GET /pets/{id}".equals(operation.getName())) { + assertEquals("get_pets_id", toolName); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testGetToolDescription() { + for (Operation operation : servicev1.getOperations()) { + String toolDescription = toolConverterv1.getToolDescription(operation); + if ("GET /my/pets".equals(operation.getName())) { + assertEquals("A list of pets owned by the user", toolDescription); + } else if ("GET /pets".equals(operation.getName())) { + assertEquals("A list of all pets filtered by name", toolDescription); + } else if ("POST /pets".equals(operation.getName())) { + assertEquals("Add a new pet", toolDescription); + } else if ("GET /pets/{id}".equals(operation.getName())) { + assertEquals("Get a pet by its ID", toolDescription); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testGetInputSchema() { + for (Operation operation : servicev1.getOperations()) { + McpSchema.JsonSchema inputSchema = toolConverterv1.getInputSchema(operation); + assertEquals("object", inputSchema.type()); + if ("GET /my/pets".equals(operation.getName())) { + assertTrue(inputSchema.properties().isEmpty()); + } else if ("GET /pets".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + assertTrue(inputSchema.properties().containsKey("filter")); + } else if ("POST /pets".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + assertTrue(inputSchema.properties().containsKey("name")); + } else if ("GET /pets/{id}".equals(operation.getName())) { + assertFalse(inputSchema.properties().isEmpty()); + assertTrue(inputSchema.properties().containsKey("id")); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + + String expectedPostPetsSchema = """ + --- + type: "object" + properties: + name: + type: "string" + coat: + type: "object" + properties: + name: + type: "string" + tint: + type: "string" + enum: + - "light" + - "dark" + required: + - "name" + additionalProperties: false + bugs: + type: "array" + items: + type: "string" + enum: + - "tick" + - "flea" + required: + - "name" + additionalProperties: false + """; + + for (Operation operation : servicev2.getOperations()) { + if ("POST /pets".equals(operation.getName())) { + McpSchema.JsonSchema inputSchema = toolConverterv2.getInputSchema(operation); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + try { + String result = mapper.writeValueAsString(inputSchema); + assertEquals(expectedPostPetsSchema, result); + } catch (Exception e) { + fail("Failed to serialize input schema for operation " + operation.getName(), e); + } + } + } + } +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIOverlayExporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIOverlayExporterTest.java new file mode 100644 index 000000000..2053ee0a4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/OpenAPIOverlayExporterTest.java @@ -0,0 +1,169 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import io.github.microcks.domain.Header; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Parameter; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.MockRepositoryExportException; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class OpenAPIOverlayExporter. + * @author laurent + */ +public class OpenAPIOverlayExporterTest { + + private static final String EXPECTED_EXPORT_HEADER = """ + overlay: 1.0.0 + info: + title: API Pastry - 2.1 Overlay for examples + version: 2.1.0 + actions: + """; + + private static final String EXPECTED_EXPORT_ACTION_1 = """ + - target: "$.paths['/pastry/{name}'].patch.parameters[?@.name=='name'].examples" + update: + Eclair Cafe: + value: Eclair Cafe + """; + private static final String EXPECTED_EXPORT_ACTION_2 = """ + - target: "$.paths['/pastry/{name}'].patch.requestBody.content['application/json'].examples" + update: + Eclair Cafe: + value: |- + { + "price": 2.6 + } + """; + private static final String EXPECTED_EXPORT_ACTION_3 = """ + - target: "$.paths['/pastry/{name}'].patch.responses.200.content['application/json'].examples" + update: + Eclair Cafe: + value: |- + { + "name": "Eclair Cafe", + "description": "Delicieux Eclair au Cafe pas calorique du tout", + "size": "M", + "price": 2.6, + "status": "available" + } + """; + private static final String EXPECTED_EXPORT_ACTION_4 = """ + - target: "$.paths['/pastry/{name}'].get.parameters[?@.name=='name'].examples" + update: + Eclair Chocolat: + value: Eclair Chocolat + """; + private static final String EXPECTED_EXPORT_ACTION_5 = """ + - target: "$.paths['/pastry/{name}'].get.responses.200.content['application/json'].examples" + update: + Eclair Chocolat: + value: |- + { + "name": "Eclair Chocolat", + "price": 2.5 + } + """; + + @Test + void testExportRESTService() throws Exception { + Service service = new Service(); + service.setId("123-456-789"); + service.setName("API Pastry - 2.1"); + service.setVersion("2.1.0"); + service.setType(ServiceType.REST); + + Operation getOperation = new Operation(); + getOperation.setName("GET /pastry/{name}"); + + Request getRequest = new Request(); + getRequest.setName("Eclair Chocolat"); + Parameter nameParameter = new Parameter(); + nameParameter.setName("name"); + nameParameter.setValue("Eclair Chocolat"); + getRequest.addQueryParameter(nameParameter); + + Response getResponse = new Response(); + getResponse.setName("Eclair Chocolat"); + getResponse.setMediaType("application/json"); + getResponse.setStatus("200"); + getResponse.setContent("{\n \"name\": \"Eclair Chocolat\",\n \"price\": 2.5\n}"); + Header poweredHeader = new Header(); + poweredHeader.setName("x-powered-by"); + poweredHeader.setValues(Set.of("microcks")); + getResponse.addHeader(poweredHeader); + + Operation patchOperation = new Operation(); + patchOperation.setName("PATCH /pastry/{name}"); + + Request patchRequest = new Request(); + patchRequest.setName("Eclair Cafe"); + patchRequest.setContent("{\n" + " \"price\": 2.6\n" + "}"); + Parameter nameParameter2 = new Parameter(); + nameParameter2.setName("name"); + nameParameter2.setValue("Eclair Cafe"); + patchRequest.addQueryParameter(nameParameter2); + Header contentHeader = new Header(); + contentHeader.setName("Content-Type"); + contentHeader.setValues(Set.of("application/json")); + patchRequest.addHeader(contentHeader); + + Response patchResponse = new Response(); + patchResponse.setName("Eclair Cafe"); + patchResponse.setMediaType("application/json"); + patchResponse.setStatus("200"); + patchResponse.setContent("{\n" + " \"name\": \"Eclair Cafe\",\n" + + " \"description\": \"Delicieux Eclair au Cafe pas calorique du tout\",\n" + " \"size\": \"M\",\n" + + " \"price\": 2.6,\n" + " \"status\": \"available\"\n" + "}"); + patchResponse.addHeader(poweredHeader); + + service.setOperations(List.of(getOperation, patchOperation)); + + String exportResult = null; + OpenAPIOverlayExporter exporter = new OpenAPIOverlayExporter(); + + try { + exporter.addServiceDefinition(service); + exporter.addMessageDefinitions(service, getOperation, + List.of(new RequestResponsePair(getRequest, getResponse))); + exporter.addMessageDefinitions(service, patchOperation, + List.of(new RequestResponsePair(patchRequest, patchResponse))); + exportResult = exporter.exportAsString(); + } catch (MockRepositoryExportException e) { + fail("Exception should not be thrown"); + } + + assertTrue(exportResult.contains(EXPECTED_EXPORT_HEADER)); + assertTrue(exportResult.contains(EXPECTED_EXPORT_ACTION_1)); + assertTrue(exportResult.contains(EXPECTED_EXPORT_ACTION_2)); + assertTrue(exportResult.contains(EXPECTED_EXPORT_ACTION_3)); + assertTrue(exportResult.contains(EXPECTED_EXPORT_ACTION_4)); + assertTrue(exportResult.contains(EXPECTED_EXPORT_ACTION_5)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/SwaggerImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/SwaggerImporterTest.java new file mode 100644 index 000000000..277735cc1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/openapi/SwaggerImporterTest.java @@ -0,0 +1,122 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.openapi; + +import io.github.microcks.domain.*; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; + +import io.github.microcks.util.openapi.SwaggerImporter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class SwaggerImporter. + * @author laurent + */ +class SwaggerImporterTest { + + @Test + void testSimpleSwaggerImportYAML() { + SwaggerImporter importer = null; + try { + importer = new SwaggerImporter( + "target/test-classes/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + importAndAssertOnSimpleSwagger(importer); + } + + @Test + void testSimpleSwaggerImportJSON() { + SwaggerImporter importer = null; + try { + importer = new SwaggerImporter( + "target/test-classes/io/github/microcks/util/openapi/beer-catalog-api-swagger.json", null); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + + importAndAssertOnSimpleSwagger(importer); + } + + private void importAndAssertOnSimpleSwagger(SwaggerImporter importer) { + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Beer Catalog API", service.getName()); + Assertions.assertEquals(ServiceType.REST, service.getType()); + assertEquals("0.9", service.getVersion()); + + // Now assert extensions parsing has been done. + assertNotNull(service.getMetadata()); + assertEquals(3, service.getMetadata().getLabels().size()); + assertEquals("beers", service.getMetadata().getLabels().get("domain")); + assertEquals("beta", service.getMetadata().getLabels().get("status")); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + assertEquals(ResourceType.SWAGGER, resources.get(0).getType()); + assertTrue(resources.get(0).getName().startsWith(service.getName() + "-" + service.getVersion())); + assertNotNull(resources.get(0).getContent()); + + // Check that operations and input/output have been found. + assertEquals(3, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /beer".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + + } else if ("GET /beer/{name}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("name", operation.getDispatcherRules()); + + } else if ("GET /beer/findByStatus/{status}".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("status", operation.getDispatcherRules()); + + } else { + fail("Unknown operation name: " + operation.getName()); + } + + // Check that messages have been ignored. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(0, exchanges.size()); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/DBAPICollectionImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/DBAPICollectionImporterTest.java new file mode 100644 index 000000000..2c752391d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/DBAPICollectionImporterTest.java @@ -0,0 +1,133 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.util.postman; + +import io.github.microcks.domain.*; +import io.github.microcks.util.MockRepositoryImportException; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author laurent + */ +class DBAPICollectionImporterTest { + + @Test + void testImportOriginal() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/DBAPI.postman_collection.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("dbapi", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + + for (Operation operation : service.getOperations()) { + + if ("GET ".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(5, exchanges.size()); + } else if ("POST ".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(0, exchanges.size()); + } + } + } + + @Test + void testImportReorganised() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/DBAPI.reorganised.postman_collection.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("dbapi", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + + for (Operation operation : service.getOperations()) { + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + + if ("GET /v1/addresses".equals(operation.getName())) { + assertEquals(1, exchanges.size()); + Exchange exchange = exchanges.get(0); + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + + assertEquals(2, request.getHeaders().size()); + assertEquals(15, response.getHeaders().size()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanCollectionImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanCollectionImporterTest.java new file mode 100644 index 000000000..2d00b5e57 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanCollectionImporterTest.java @@ -0,0 +1,709 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.postman; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class PostmanCollectionImporter. + * @author laurent + */ +class PostmanCollectionImporterTest { + + @Test + void testSimpleProjectImportV2() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/Swagger Petstore.postman_collection.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Swagger Petstore", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + + // Check that operations and and input/output have been found. + assertEquals(2, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /v2/pet/findByStatus".equals(operation.getName())) { + // assertions for findByStatus. + assertEquals("GET", operation.getMethod()); + assertEquals(1, operation.getResourcePaths().size()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/findByStatus")); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + e.printStackTrace(); + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + Exchange exchange = exchanges.get(0); + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("findbystatus-available", response.getName()); + assertEquals(1, response.getHeaders().size()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertEquals("?status=available?user_key=998bac0775b1d5f588e0a6ca7c11b852", + response.getDispatchCriteria()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } else if ("GET /v2/pet/:petId".equals(operation.getName())) { + // assertions for findById. + assertEquals("GET", operation.getMethod()); + //assertEquals(2, operation.getResourcePaths().size()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/1")); + assertTrue(operation.getResourcePaths().contains("/v2/pet/2")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("findbyid-2".equals(request.getName())) { + assertEquals("findbyid-2", response.getName()); + assertEquals(1, response.getHeaders().size()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertEquals("/petId=2?user_key=998bac0775b1d5f588e0a6ca7c11b852", response.getDispatchCriteria()); + assertNotNull(response.getContent()); + } else if ("findbyid-1".equals(request.getName())) { + assertEquals("404", response.getStatus()); + assertEquals("/petId=1?user_key=998bac0775b1d5f588e0a6ca7c11b852", response.getDispatchCriteria()); + } else { + fail("Unknown request name: " + request.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testSimpleProjectImportV21() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/Swagger Petstore.postman_collection-2.1.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Swagger Petstore", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.1", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + + // Check that operations and and input/output have been found. + assertEquals(2, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /v2/pet/findByStatus".equals(operation.getName())) { + // assertions for findByStatus. + assertEquals("GET", operation.getMethod()); + assertEquals(1, operation.getResourcePaths().size()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/findByStatus")); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + Exchange exchange = exchanges.get(0); + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("findbystatus-available", response.getName()); + assertEquals(1, response.getHeaders().size()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertEquals("?status=available?user_key=998bac0775b1d5f588e0a6ca7c11b852", + response.getDispatchCriteria()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } else if ("GET /v2/pet/:petId".equals(operation.getName())) { + // assertions for findById. + assertEquals("GET", operation.getMethod()); + //assertEquals(2, operation.getResourcePaths().size()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/1")); + assertTrue(operation.getResourcePaths().contains("/v2/pet/2")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("findbyid-2".equals(request.getName())) { + assertEquals("findbyid-2", response.getName()); + assertEquals(1, response.getHeaders().size()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertEquals("/petId=2?user_key=998bac0775b1d5f588e0a6ca7c11b852", response.getDispatchCriteria()); + assertNotNull(response.getContent()); + } else if ("findbyid-1".equals(request.getName())) { + assertEquals("404", response.getStatus()); + assertEquals("/petId=1?user_key=998bac0775b1d5f588e0a6ca7c11b852", response.getDispatchCriteria()); + } else { + fail("Unknown request name: " + request.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } + + @Test + void testTestAPIImport() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/Test API.postman_collection.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Test API", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + assertEquals("0.0.1", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + + // Check that operations and and input/output have been found. + assertEquals(4, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("POST /order".equals(operation.getName())) { + // Assertions for creation. + assertEquals("POST", operation.getMethod()); + // TODO + //assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("create-123456".equals(request.getName())) { + assertNotNull(request.getContent()); + assertNull(response.getHeaders()); + assertEquals("201", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else if ("create-7891011".equals(request.getName())) { + assertNotNull(request.getContent()); + assertNull(response.getHeaders()); + assertEquals("201", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + } else { + fail("Unknown request name: " + request.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("GET /order".equals(operation.getName())) { + // Assertions for listing. + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + e.printStackTrace(); + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(1, operation.getResourcePaths().size()); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("list-pending_approval".equals(request.getName())) { + assertNull(request.getContent()); + assertNull(response.getHeaders()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + assertEquals("?page=0?status=pending_approval", response.getDispatchCriteria()); + } else if ("list-approved".equals(request.getName())) { + assertNull(request.getContent()); + assertNull(response.getHeaders()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + assertEquals("?page=0?status=approved", response.getDispatchCriteria()); + } else { + fail("Unknown request name: " + request.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("GET /order/:id".equals(operation.getName())) { + // Assertions for retrieval. + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + e.printStackTrace(); + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/order/123456")); + assertTrue(operation.getResourcePaths().contains("/order/7891011")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair entry) { + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("get-123456".equals(request.getName())) { + assertNull(request.getContent()); + assertNull(response.getHeaders()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + assertEquals("/id=123456", response.getDispatchCriteria()); + } else if ("get-7891011".equals(request.getName())) { + assertNull(request.getContent()); + assertNull(response.getHeaders()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertNotNull(response.getContent()); + assertEquals("/id=7891011", response.getDispatchCriteria()); + } else { + fail("Unknown request name: " + request.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } + } + } + + @Test + void testTestAPINoVersionImport() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/Test API no version.postman_collection.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties import fail because of missing version. + boolean failure = false; + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + failure = true; + assertNotEquals(-1, e.getMessage().indexOf("Version property")); + } + assertTrue(failure); + } + + @Test + void testTestAPIMalformedVersionImport() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/Test API bad version.postman_collection.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties import fail because of missing version. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + assertEquals("0.0.1-Description", services.get(0).getVersion()); + } + + @ParameterizedTest + @ValueSource(strings = { + "target/test-classes/io/github/microcks/util/postman/structured-version-identifier-collection.json", + "target/test-classes/io/github/microcks/util/postman/structured-version-digits-collection.json", + "target/test-classes/io/github/microcks/util/postman/structured-version-raw-collection.json", + "target/test-classes/io/github/microcks/util/postman/structured-version-desc-collection.json" }) + void testStructuredVersionImport(String collection) { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter(collection); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties import fail because of missing version. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + assertEquals("1.0.0", services.get(0).getVersion()); + } + + @Test + void testPetstoreWithTrailingDollarImport() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/PetstoreAPI-collection-sample-trailing-dollar.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Petstore API", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + assertEquals("12.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + + // Check that operations and and input/output have been found. + assertEquals(2, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /v2/pet/:petId/$access".equals(operation.getName())) { + // assertions for findById. + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("petId", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/1/$access")); + assertTrue(operation.getResourcePaths().contains("/v2/pet/2/$access")); + + } else if ("GET /v2/pet/:petId/$count".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(DispatchStyles.URI_PARTS, operation.getDispatcher()); + assertEquals("petId", operation.getDispatcherRules()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/1/$count")); + assertTrue(operation.getResourcePaths().contains("/v2/pet/2/$count")); + } else { + fail("Unknown operation"); + } + } + } + + @Test + void testPetstoreWithTrailingSlashImport() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/PetstoreAPI-collection-sample.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Petstore API", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + + // Check that operations and and input/output have been found. + assertEquals(2, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("GET /v2/pet/:petId".equals(operation.getName())) { + // assertions for findById. + assertEquals("GET", operation.getMethod()); + //assertEquals(2, operation.getResourcePaths().size()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/1")); + assertTrue(operation.getResourcePaths().contains("/v2/pet/2")); + } + } + } + + @Test + void testGraphQLCollectionImport() { + PostmanCollectionImporter importer = null; + try { + importer = new PostmanCollectionImporter( + "target/test-classes/io/github/microcks/util/graphql/films-postman.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Movie Graph API", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.0", service.getVersion()); + + // Check that operations and input/output have been found. + assertEquals(4, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + if ("POST allFilms".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + + Exchange exchange = exchanges.get(0); + assertTrue(exchange instanceof RequestResponsePair); + RequestResponsePair pair = (RequestResponsePair) exchange; + + assertNotNull(pair.getRequest().getContent()); + assertNotNull(pair.getResponse().getContent()); + assertEquals("200", pair.getResponse().getStatus()); + assertNull(pair.getResponse().getDispatchCriteria()); + } else if ("POST film".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + + for (Exchange exchange : exchanges) { + assertTrue(exchange instanceof RequestResponsePair); + RequestResponsePair pair = (RequestResponsePair) exchange; + + assertNotNull(pair.getRequest().getContent()); + assertTrue(pair.getRequest().getContent().contains("\"id\": \"ZmlsbXM6MQ==\"") + || pair.getRequest().getContent().contains("\"id\": \"ZmlsbXM6Mg==\"")); + assertNotNull(pair.getResponse().getContent()); + assertEquals("200", pair.getResponse().getStatus()); + } + } else if ("POST addStar".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + + Exchange exchange = exchanges.get(0); + assertTrue(exchange instanceof RequestResponsePair); + RequestResponsePair pair = (RequestResponsePair) exchange; + + assertNotNull(pair.getRequest().getContent()); + assertNotNull(pair.getResponse().getContent()); + assertEquals("200", pair.getResponse().getStatus()); + } else if ("POST addReview".equals(operation.getName())) { + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + + Exchange exchange = exchanges.get(0); + assertTrue(exchange instanceof RequestResponsePair); + RequestResponsePair pair = (RequestResponsePair) exchange; + + assertNotNull(pair.getRequest().getContent()); + assertNotNull(pair.getResponse().getContent()); + // Add this check to ensure that "comment" found in Postman variables is correctly + // integrated into variables. + assertTrue(pair.getRequest().getContent().contains("\"variables\":")); + assertTrue(pair.getRequest().getContent().contains("\"comment\":")); + assertEquals("200", pair.getResponse().getStatus()); + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanUtilTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanUtilTest.java new file mode 100644 index 000000000..2920b6254 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanUtilTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.postman; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for PostmanUtilTest class. + * @author laurent + */ +class PostmanUtilTest { + + @Test + void testAreOperationsEquivalent() { + assertTrue(PostmanUtil.areOperationsEquivalent("GET /PaStRiEs", "get /pastries")); + assertTrue(PostmanUtil.areOperationsEquivalent("GET /PaStRiEs/{name}", "get /pastries/:name")); + assertTrue(PostmanUtil.areOperationsEquivalent("recommendation", "POST recommendation")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanWorkspaceCollectionImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanWorkspaceCollectionImporterTest.java new file mode 100644 index 000000000..f602c51c7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/postman/PostmanWorkspaceCollectionImporterTest.java @@ -0,0 +1,151 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.postman; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.domain.ServiceType; +import io.github.microcks.util.DispatchStyles; +import io.github.microcks.util.MockRepositoryImportException; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class PostmanWorkspaceCollectionImporter. + * @author laurent + */ +class PostmanWorkspaceCollectionImporterTest { + + @Test + void testSimpleProjectImportV21() { + PostmanWorkspaceCollectionImporter importer = null; + try { + importer = new PostmanWorkspaceCollectionImporter( + "target/test-classes/io/github/microcks/util/postman/Swagger Petstore.postman_workspace_collection-2.1.json"); + } catch (IOException ioe) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Swagger Petstore", service.getName()); + assertEquals(ServiceType.REST, service.getType()); + assertEquals("1.1", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + + // Check that operations and and input/output have been found. + assertEquals(2, service.getOperations().size()); + for (Operation operation : service.getOperations()) { + + if ("GET /v2/pet/findByStatus".equals(operation.getName())) { + // assertions for findByStatus. + assertEquals("GET", operation.getMethod()); + assertEquals(1, operation.getResourcePaths().size()); + assertEquals(DispatchStyles.URI_PARAMS, operation.getDispatcher()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/findByStatus")); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + e.printStackTrace(); + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(1, exchanges.size()); + Exchange exchange = exchanges.get(0); + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + assertEquals("findbystatus-available", response.getName()); + assertEquals(1, response.getHeaders().size()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertEquals("?status=available?user_key=998bac0775b1d5f588e0a6ca7c11b852", + response.getDispatchCriteria()); + assertNotNull(response.getContent()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } else if ("GET /v2/pet/:petId".equals(operation.getName())) { + // assertions for findById. + assertEquals("GET", operation.getMethod()); + //assertEquals(2, operation.getResourcePaths().size()); + assertEquals(DispatchStyles.URI_ELEMENTS, operation.getDispatcher()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/v2/pet/1")); + assertTrue(operation.getResourcePaths().contains("/v2/pet/2")); + + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("findbyid-2".equals(request.getName())) { + assertEquals("findbyid-2", response.getName()); + assertEquals(1, response.getHeaders().size()); + assertEquals("200", response.getStatus()); + assertEquals("application/json", response.getMediaType()); + assertEquals("/petId=2?user_key=998bac0775b1d5f588e0a6ca7c11b852", response.getDispatchCriteria()); + assertNotNull(response.getContent()); + } else if ("findbyid-1".equals(request.getName())) { + assertEquals("404", response.getStatus()); + assertEquals("/petId=1?user_key=998bac0775b1d5f588e0a6ca7c11b852", response.getDispatchCriteria()); + } else { + fail("Unknown request name: " + request.getName()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else { + fail("Unknown operation name: " + operation.getName()); + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/script/HttpHeadersStringToStringsMapTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/script/HttpHeadersStringToStringsMapTest.java new file mode 100644 index 000000000..ed38c1280 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/script/HttpHeadersStringToStringsMapTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.script; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for class HttpHeadersStringToStringsMap class. + * @author laurent + */ +class HttpHeadersStringToStringsMapTest { + + @Test + void testRFC7230() { + List value = List.of("12345"); + StringToStringsMap headers = new HttpHeadersStringToStringsMap(); + headers.put("JWTPortail", value); + + assertTrue(headers.hasValues("JWTPortail")); + assertTrue(headers.hasValues("jwtportail")); + assertTrue(headers.hasValues("JwTportail")); + + assertEquals(value, headers.get("JWTPortail")); + assertEquals(value, headers.get("jwtportail")); + assertEquals(value, headers.get("JwTportail")); + } + + @Test + void testAddSingleValue() { + StringToStringsMap headers = new HttpHeadersStringToStringsMap(); + headers.add("foo", "bar"); + assertTrue(headers.hasValues("foo")); + assertEquals(List.of("bar"), headers.get("foo")); + } + + @Test + void testAddMultipleValues() { + StringToStringsMap headers = new HttpHeadersStringToStringsMap(); + headers.add("foo", "bar1"); + headers.add("foo", "bar2"); + assertTrue(headers.hasValues("foo")); + assertEquals(List.of("bar1", "bar2"), headers.get("foo")); + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/script/ScriptEngineBinderTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/script/ScriptEngineBinderTest.java new file mode 100644 index 000000000..c7fad7d5c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/script/ScriptEngineBinderTest.java @@ -0,0 +1,273 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.script; + +import io.github.microcks.service.StateStore; + +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class ScriptEngineBinder class. + * @author laurent + */ +class ScriptEngineBinderTest { + + @Test + void testRequestContentIsBound() { + String script = """ + return mockRequest.requestContent; + """; + + ScriptEngineManager sem = new ScriptEngineManager(); + String body = "content"; + + try { + // Evaluating request with script coming from operation dispatcher rules. + ScriptEngine se = sem.getEngineByExtension("groovy"); + ScriptContext sc = ScriptEngineBinder.buildEvaluationContext(se, body, null, null, null); + String result = (String) se.eval(script, sc); + + assertEquals(body, result); + } catch (Exception e) { + fail("Exception should no be thrown"); + } + } + + @Test + void testRequestContentHeadersAreBound() { + String script = """ + def headers = mockRequest.getRequestHeaders() + log.info("headers: " + headers) + return headers.get("foo", "null"); + """; + + ScriptEngineManager sem = new ScriptEngineManager(); + String body = "content"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("foo", "bar"); + + try { + // Evaluating request with script coming from operation dispatcher rules. + ScriptEngine se = sem.getEngineByExtension("groovy"); + ScriptContext sc = ScriptEngineBinder.buildEvaluationContext(se, body, null, null, request, null); + String result = (String) se.eval(script, sc); + + assertEquals("bar", result); + } catch (Exception e) { + fail("Exception should no be thrown"); + } + } + + @Test + void testUriParametersAreBound() { + String script = """ + def uriParameters = mockRequest.getURIParameters() + log.info("uri parameters: " + uriParameters) + return uriParameters.get("foo", "null"); + """; + + ScriptEngineManager sem = new ScriptEngineManager(); + String body = "content"; + MockHttpServletRequest request = new MockHttpServletRequest(); + Map uriParameters = new HashMap<>(); + uriParameters.put("foo", "bar"); + + try { + // Evaluating request with script coming from operation dispatcher rules. + ScriptEngine se = sem.getEngineByExtension("groovy"); + ScriptContext sc = ScriptEngineBinder.buildEvaluationContext(se, body, null, null, request, uriParameters); + String result = (String) se.eval(script, sc); + + assertEquals("bar", result); + } catch (Exception e) { + fail("Exception should no be thrown"); + } + } + + @Test + void testRequestContextIsModified() { + String script = """ + requestContext.foo = "bar"; + return mockRequest.requestContent; + """; + + ScriptEngineManager sem = new ScriptEngineManager(); + Map context = new HashMap<>(); + String body = "content"; + + try { + // Evaluating request with script coming from operation dispatcher rules. + ScriptEngine se = sem.getEngineByExtension("groovy"); + ScriptContext sc = ScriptEngineBinder.buildEvaluationContext(se, body, context, null, null); + String result = (String) se.eval(script, sc); + + assertEquals(body, result); + assertTrue(context.containsKey("foo")); + assertEquals("bar", context.get("foo")); + } catch (Exception e) { + fail("Exception should no be thrown"); + } + } + + @Test + void testStateStoreIsBoundAndAccessed() { + String script = """ + def foo = store.get("foo"); + def bar = store.put("bar", "barValue"); + store.delete("baz"); + return foo; + """; + + StateStore store = new StateStore() { + private final Map map = new HashMap<>(); + + @Override + public void put(String key, String value) { + map.put(key, value); + } + + @Override + public void put(String key, String value, int secondsTTL) { + map.put(key, value); + } + + @Nullable + @Override + public String get(String key) { + return map.get(key); + } + + @Override + public void delete(String key) { + map.remove(key); + } + }; + + ScriptEngineManager sem = new ScriptEngineManager(); + Map context = new HashMap<>(); + store.put("foo", "fooValue"); + store.put("baz", "bazValue"); + + try { + // Evaluating request with script coming from operation dispatcher rules. + ScriptEngine se = sem.getEngineByExtension("groovy"); + ScriptContext sc = ScriptEngineBinder.buildEvaluationContext(se, "body", context, store, null); + String result = (String) se.eval(script, sc); + + assertEquals("fooValue", result); + assertEquals("barValue", store.get("bar")); + assertNull(store.get("baz")); + } catch (Exception e) { + fail("Exception should no be thrown"); + } + } + + @Test + void testMicrocksXmlHolder() { + String body = """ + + + + + Andrew + + + + """; + + String script = """ + import io.github.microcks.util.soapui.XmlHolder + def holder = new XmlHolder( mockRequest.requestContent ) + def name = holder["//name"] + + if (name == "Andrew"){ + return "Andrew Response" + } else if (name == "Karla"){ + return "Karla Response" + } else { + return "World Response" + } + """; + + ScriptEngineManager sem = new ScriptEngineManager(); + Map context = new HashMap<>(); + + try { + // Evaluating request with script coming from operation dispatcher rules. + ScriptEngine se = sem.getEngineByExtension("groovy"); + ScriptContext sc = ScriptEngineBinder.buildEvaluationContext(se, body, context, null, null); + String result = (String) se.eval(script, sc); + + assertEquals("Andrew Response", result); + } catch (Exception e) { + fail("Exception should no be thrown"); + } + } + + @Test + void testEviwareXmlHolder() { + String body = """ + + + + + Andrew + + + + """; + + String script = """ + import com.eviware.soapui.support.XmlHolder + def holder = new XmlHolder( mockRequest.requestContent ) + def name = holder["//name"] + + if (name == "Andrew"){ + return "Andrew Response" + } else if (name == "Karla"){ + return "Karla Response" + } else { + return "World Response" + } + """; + + ScriptEngineManager sem = new ScriptEngineManager(); + Map context = new HashMap<>(); + + try { + // Evaluating request with script coming from operation dispatcher rules. + ScriptEngine se = sem.getEngineByExtension("groovy"); + ScriptContext sc = ScriptEngineBinder.buildEvaluationContext(se, body, context, null, null); + script = ScriptEngineBinder.ensureSoapUICompatibility(script); + String result = (String) se.eval(script, sc); + + assertEquals("Andrew Response", result); + } catch (Exception e) { + fail("Exception should no be thrown"); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/SoapUIProjectImporterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/SoapUIProjectImporterTest.java new file mode 100644 index 000000000..6d055a163 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/SoapUIProjectImporterTest.java @@ -0,0 +1,553 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui; + +import io.github.microcks.domain.Exchange; +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Request; +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.Resource; +import io.github.microcks.domain.ResourceType; +import io.github.microcks.domain.Response; +import io.github.microcks.domain.Service; +import io.github.microcks.util.MockRepositoryImportException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class SoapUIProjectImporter. + * @author laurent + */ +class SoapUIProjectImporterTest { + + @Test + void testSimpleProjectImport() { + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/RefTest-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("HelloServiceSoapBinding", service.getName()); + assertEquals("http://lbroudoux.github.com/test/service", service.getXmlNS()); + assertEquals("1.2", service.getVersion()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().iterator().next(); + assertEquals("sayHello", operation.getName()); + assertEquals("sayHello", operation.getInputName()); + assertEquals("sayHelloResponse", operation.getOutputName()); + + // Check mock dispatching rules. + assertEquals("QUERY_MATCH", operation.getDispatcher()); + assertTrue(operation.getDispatcherRules() + .contains("declare namespace ser='http://lbroudoux.github.com/test/service';")); + assertTrue(operation.getDispatcherRules().contains("//ser:sayHello/name")); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(2, resources.size()); + + Resource resource = resources.get(0); + Assertions.assertEquals(ResourceType.SOAP_UI_PROJECT, resource.getType()); + assertEquals("HelloServiceSoapBinding-1.2.xml", resource.getName()); + assertNotNull(resource.getContent()); + + resource = resources.get(1); + Assertions.assertEquals(ResourceType.WSDL, resource.getType()); + assertEquals("HelloServiceSoapBinding-1.2.wsdl", resource.getName()); + assertNotNull(resource.getContent()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + e.printStackTrace(); + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("Anne Request".equals(request.getName())) { + assertEquals(3, request.getHeaders().size()); + assertEquals("Anne Response", response.getName()); + assertEquals("Anne", response.getDispatchCriteria()); + } else if ("Laurent Request".equals(request.getName())) { + assertEquals("Laurent Response", response.getName()); + assertEquals("Laurent", response.getDispatchCriteria()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } + + @Test + void testSimpleScriptWithSOAPFaultProjectImport() { + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/HelloService-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("HelloService Mock", service.getName()); + assertEquals("http://www.example.com/hello", service.getXmlNS()); + assertEquals("0.9", service.getVersion()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().iterator().next(); + assertEquals("sayHello", operation.getName()); + assertEquals("sayHello", operation.getInputName()); + assertEquals("sayHelloResponse", operation.getOutputName()); + + // Check mock dispatching rules. + assertEquals("SCRIPT", operation.getDispatcher()); + assertTrue(operation.getDispatcherRules().contains("import com.eviware.soapui.support.XmlHolder")); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(2, resources.size()); + + Resource resource = resources.get(0); + assertEquals(ResourceType.SOAP_UI_PROJECT, resource.getType()); + assertEquals("HelloService Mock-0.9.xml", resource.getName()); + assertNotNull(resource.getContent()); + + resource = resources.get(1); + assertEquals(ResourceType.WSDL, resource.getType()); + assertEquals("HelloService Mock-0.9.wsdl", resource.getName()); + assertNotNull(resource.getContent()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(3, exchanges.size()); + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("Andrew Request".equals(request.getName())) { + assertEquals("Andrew Response", response.getName()); + assertEquals("Andrew Response", response.getDispatchCriteria()); + } else if ("Karla Request".equals(request.getName())) { + assertEquals("Karla Response", response.getName()); + assertEquals("Karla Response", response.getDispatchCriteria()); + } else if ("World Request".equals(request.getName())) { + assertEquals("World Response", response.getName()); + assertEquals("World Response", response.getDispatchCriteria()); + assertTrue(response.isFault()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } + + @Test + void testSimpleScriptProjectImport() { + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/RefTest-script-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("HelloServiceScriptBinding", service.getName()); + assertEquals("http://lbroudoux.github.com/test/service", service.getXmlNS()); + assertEquals("1.0", service.getVersion()); + + // Check that operations and input/output have been found. + assertEquals(1, service.getOperations().size()); + Operation operation = service.getOperations().iterator().next(); + assertEquals("sayHello", operation.getName()); + assertEquals("sayHello", operation.getInputName()); + assertEquals("sayHelloResponse", operation.getOutputName()); + + // Check mock dispatching rules. + assertEquals("SCRIPT", operation.getDispatcher()); + assertTrue(operation.getDispatcherRules().contains("import com.eviware.soapui.support.XmlHolder")); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(service); + assertEquals(2, resources.size()); + Resource resource = resources.get(0); + assertEquals(ResourceType.SOAP_UI_PROJECT, resource.getType()); + assertEquals("HelloServiceScriptBinding-1.0.xml", resource.getName()); + assertNotNull(resource.getContent()); + + resource = resources.get(1); + assertEquals(ResourceType.WSDL, resource.getType()); + assertEquals("HelloServiceScriptBinding-1.0.wsdl", resource.getName()); + assertNotNull(resource.getContent()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + if ("Anne Request".equals(request.getName())) { + assertEquals("Anne Response", response.getName()); + assertEquals("Anne Response", response.getDispatchCriteria()); + } else if ("Laurent Request".equals(request.getName())) { + assertEquals("Laurent Response", response.getName()); + assertEquals("Laurent Response", response.getDispatchCriteria()); + } + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } + + @Test + void testComplexProjectImport() { + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/RefTest-Product-GetProductElements-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("execute_pttBinding_GetProductElements MockService", service.getName()); + assertEquals("1.0.0", service.getVersion()); + + List resources = importer.getResourceDefinitions(service); + assertEquals(3, resources.size()); + + Resource resource = resources.get(0); + assertEquals(ResourceType.SOAP_UI_PROJECT, resource.getType()); + assertNotNull(resource.getContent()); + assertEquals("execute_pttBinding_GetProductElements MockService-1.0.0.xml", resource.getName()); + + resource = resources.get(1); + assertEquals(ResourceType.XSD, resource.getType()); + assertNotNull(resource.getContent()); + assertEquals("Product_Anomalie_v1.0.xsd", resource.getName()); + + resource = resources.get(2); + assertEquals(ResourceType.WSDL, resource.getType()); + assertNotNull(resource.getContent()); + assertEquals("execute_pttBinding_GetProductElements MockService-1.0.0.wsdl", resource.getName()); + // Check that XSD path has been changed into WSDL. + assertTrue(resource.getContent().contains( + "")); + } + + @Test + void testSimpleRestProjectImport() { + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/Test-REST-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("Whabed MockService", service.getName()); + assertEquals("0.0.1", service.getVersion()); + + // Check that resources import does not throw exceptions of failures. + List resources = importer.getResourceDefinitions(service); + assertEquals(1, resources.size()); + + // Check that operations and methods/resourcePaths have been found. + assertEquals(3, service.getOperations().size()); + for (Operation operation : service.getOperations().toArray(new Operation[2])) { + + if ("/deployment".equals(operation.getName())) { + assertEquals("POST", operation.getMethod()); + assertTrue(operation.getResourcePaths().contains("/deployment")); + assertEquals("SEQUENCE", operation.getDispatcher()); + + } else if ("/deployment/byComponent/{component}/{version}.json".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(2, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/deployment/byComponent/testREST/1.2.json")); + assertTrue(operation.getResourcePaths().contains("/deployment/byComponent/testREST/1.3.json")); + assertEquals("SEQUENCE", operation.getDispatcher()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + + if ("deploymentsForTestREST1.2 Request".equals(request.getName())) { + assertEquals("deploymentsForTestREST1.2 Response", response.getName()); + assertEquals("/component=testREST/version=1.2", response.getDispatchCriteria()); + } else if ("deploymentsForTestREST1.3 Request".equals(request.getName())) { + assertEquals("deploymentsForTestREST1.3 Response", response.getName()); + assertEquals("/component=testREST/version=1.3", response.getDispatchCriteria()); + } else { + fail("Message has not an expected name"); + } + + assertEquals("application/json", response.getMediaType()); + assertEquals("200", response.getStatus()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else if ("/deployment/byEnvironment/{environment}/{qualifier}.json".equals(operation.getName())) { + assertEquals("GET", operation.getMethod()); + assertEquals(1, operation.getResourcePaths().size()); + assertTrue(operation.getResourcePaths().contains("/deployment/byEnvironment/QUALIF2/cdsm.json")); + assertEquals("SCRIPT", operation.getDispatcher()); + assertNotNull(operation.getDispatcherRules()); + assertFalse(operation.getDispatcherRules().isEmpty()); + + // Check that messages have been correctly found. + List exchanges = null; + try { + exchanges = importer.getMessageDefinitions(service, operation); + } catch (Exception e) { + fail("No exception should be thrown when importing message definitions."); + } + assertEquals(2, exchanges.size()); + for (Exchange exchange : exchanges) { + if (exchange instanceof RequestResponsePair) { + RequestResponsePair entry = (RequestResponsePair) exchange; + Request request = entry.getRequest(); + Response response = entry.getResponse(); + assertNotNull(request); + assertNotNull(response); + + if ("deploymentsForQUALIF2cdsm Request".equals(request.getName())) { + assertNotNull(request.getQueryParameters()); + assertEquals(4, request.getQueryParameters().size()); + assertEquals("deploymentsForQUALIF2cdsm Response", response.getName()); + assertEquals("deploymentsForQUALIF2cdsm Response", response.getDispatchCriteria()); + } else if ("deploymentsForQUALIF2cdsm2 Request".equals(request.getName())) { + assertNotNull(request.getQueryParameters()); + assertEquals(4, request.getQueryParameters().size()); + assertEquals("deploymentsForQUALIF2cdsm2 Response", response.getName()); + assertEquals("deploymentsForQUALIF2cdsm2 Response", response.getDispatchCriteria()); + } else { + fail("Message has not an expected name"); + } + + assertEquals("application/json", response.getMediaType()); + assertEquals("200", response.getStatus()); + } else { + fail("Exchange has the wrong type. Expecting RequestResponsePair"); + } + } + } else { + fail("Operation has not an expected name"); + } + + } + } + + @Test + void testSimpleProjectNoVersionImport() { + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/RefTest-no-version-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + boolean failure = false; + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + failure = true; + assertNotEquals(-1, e.getMessage().indexOf("Version property")); + } + assertTrue(failure); + } + + @Test + void testHelloAPIProjectImport() { + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/HelloAPI-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + } + + @Test + void testMultipleInterfacesProjectImport() { + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/GetDrivers-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("DriverSoap", service.getName()); + assertEquals("http://www.itra.com", service.getXmlNS()); + assertEquals("1.0", service.getVersion()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(services.get(0)); + assertEquals(2, resources.size()); + Resource resource = resources.get(0); + Assertions.assertEquals(ResourceType.SOAP_UI_PROJECT, resource.getType()); + assertEquals("DriverSoap-1.0.xml", resource.getName()); + assertNotNull(resource.getContent()); + + resource = resources.get(1); + Assertions.assertEquals(ResourceType.WSDL, resource.getType()); + assertEquals("DriverSoap-1.0.wsdl", resource.getName()); + assertNotNull(resource.getContent()); + } + + @Test + void testPartNamesCorrectResolution() { + // Initialized from https://github.com/microcks/microcks/issues/680. + SoapUIProjectImporter importer = null; + try { + importer = new SoapUIProjectImporter( + "target/test-classes/io/github/microcks/util/soapui/VIES-soapui-project.xml"); + } catch (Exception e) { + fail("Exception should not be thrown"); + } + // Check that basic service properties are there. + List services = null; + try { + services = importer.getServiceDefinitions(); + } catch (MockRepositoryImportException e) { + fail("Exception should not be thrown"); + } + assertEquals(1, services.size()); + Service service = services.get(0); + assertEquals("VitaleServiceBinding-v2 Mock", service.getName()); + assertEquals("http://www.cnamts.fr/vitale/webservice/ServiceVitale/v2", service.getXmlNS()); + assertEquals("060000", service.getVersion()); + assertEquals(1, service.getOperations().size()); + + Operation operation = service.getOperations().get(0); + assertEquals("recupererSuiviParcoursCarte", operation.getName()); + assertEquals("urn:ServiceVitale:2:recupererSuiviParcoursCarte", operation.getAction()); + assertEquals("recupererSuiviParcoursCarteRequestElement", operation.getInputName()); + assertEquals("recupererSuiviParcoursCarteResponseElement", operation.getOutputName()); + + // Check that resources have been parsed, correctly renamed, etc... + List resources = importer.getResourceDefinitions(services.get(0)); + assertEquals(3, resources.size()); + + Resource resource = resources.get(1); + Assertions.assertEquals(ResourceType.XSD, resource.getType()); + assertEquals("Suivi_Parcours_Carte_2.0.xsd", resource.getName()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/SoapUIXPathBuilderTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/SoapUIXPathBuilderTest.java new file mode 100644 index 000000000..24d0424b1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/SoapUIXPathBuilderTest.java @@ -0,0 +1,85 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui; + +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; + +import javax.xml.xpath.XPathExpression; + +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a test case for class SoapUIXPathBuilder class. + * @author laurent + */ +class SoapUIXPathBuilderTest { + + @Test + void testBuildXPathMatcherFromRulesSimple() { + + String rules = "declare namespace ser='http://www.example.com/hello';\n" + "//ser:sayHello/name"; + String soap = "\n" + + " \n" + " \n" + " \n" + + " Karla\n" + " \n" + " \n" + + ""; + + XPathExpression expression = null; + + try { + expression = SoapUIXPathBuilder.buildXPathMatcherFromRules(rules); + } catch (Throwable t) { + fail("No exception should be thrown while parsing rules"); + } + + String result = null; + try { + result = expression.evaluate(new InputSource(new StringReader(soap))); + } catch (Throwable t) { + fail("No exception should be thrown while evaluating xpath"); + } + assertEquals("Karla", result); + } + + @Test + void testBuildXPathMatcherFromRulesFunction() { + + String rules = "declare namespace ser='http://www.example.com/hello';\n" + + "concat(//ser:sayHello/title/text(),' ',//ser:sayHello/name/text())"; + String soap = "\n" + + " \n" + " \n" + " \n" + + " Ms.\n" + " Karla\n" + " \n" + + " \n" + ""; + + XPathExpression expression = null; + + try { + expression = SoapUIXPathBuilder.buildXPathMatcherFromRules(rules); + } catch (Throwable t) { + fail("No exception should be thrown while parsing rules"); + } + + String result = null; + try { + result = expression.evaluate(new InputSource(new StringReader(soap))); + } catch (Throwable t) { + fail("No exception should be thrown while evaluating xpath"); + } + assertEquals("Ms. Karla", result); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/XmlHolderTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/XmlHolderTest.java new file mode 100644 index 000000000..8781c0c7d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/XmlHolderTest.java @@ -0,0 +1,68 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for class XmlHolder class. + * @author laurent + */ +class XmlHolderTest { + + private final String validSoap = """ + + + + + Hello Andrew ! + + + + """; + + @Test + void testWithoutNamespace() throws Exception { + String xpathStr = """ + //hel:sayHelloResponse/sayHello + """; + XmlHolder holder = new XmlHolder(validSoap); + assertEquals("Hello Andrew !", holder.get(xpathStr)); + } + + @Test + void testWithNamespaceInXpath() throws Exception { + String xpathStr = """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """; + XmlHolder holder = new XmlHolder(validSoap); + assertEquals("Hello Andrew !", holder.get(xpathStr)); + } + + @Test + void testWithNamespaceInHolder() throws Exception { + String xpathStr = """ + //ser:sayHelloResponse/sayHello + """; + XmlHolder holder = new XmlHolder(validSoap); + holder.declareNamespace("ser", "http://www.example.com/hello"); + assertEquals("Hello Andrew !", holder.get(xpathStr)); + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/SimpleContainsAssertionTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/SimpleContainsAssertionTest.java new file mode 100644 index 000000000..1714adc81 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/SimpleContainsAssertionTest.java @@ -0,0 +1,114 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for class SimpleContainsAssertion. + * @author laurent + */ +class SimpleContainsAssertionTest { + + private String validSoap = """ + + + + + Hello Andrew! + + + + """; + + private String invalidSoap = """ + + + + + Andrew! + + + + """; + + @Test + void testExactMatch() { + // Passing case. + Map configParams = Map.of(SimpleContainsAssertion.TOKEN_PARAM, "Hello Andrew!", + SimpleContainsAssertion.IGNORE_CASE_PARAM, "false"); + SoapUIAssertion assertion = AssertionFactory.intializeAssertion(AssertionFactory.SIMPLE_CONTAINS, configParams); + AssertionStatus status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // Failing case. + status = assertion.assertResponse(new RequestResponseExchange(null, null, invalidSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + assertEquals(1, assertion.getErrorMessages().size()); + assertTrue(assertion.getErrorMessages().get(0).contains("Missing")); + } + + @Test + void testCaseSensitiveness() { + // Passing case. + Map configParams = Map.of(SimpleContainsAssertion.TOKEN_PARAM, "HeLlO AnDreW!", + SimpleContainsAssertion.IGNORE_CASE_PARAM, "true"); + SoapUIAssertion assertion = AssertionFactory.intializeAssertion(AssertionFactory.SIMPLE_CONTAINS, configParams); + AssertionStatus status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // Failing case. + status = assertion.assertResponse(new RequestResponseExchange(null, null, invalidSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + assertEquals(1, assertion.getErrorMessages().size()); + assertTrue(assertion.getErrorMessages().get(0).contains("Missing")); + } + + @Test + void testRegularExpression() { + // Passing case. + Map configParams = Map.of(SimpleContainsAssertion.TOKEN_PARAM, "Hello\\s(.*)!", + SimpleContainsAssertion.IGNORE_CASE_PARAM, "false", SimpleContainsAssertion.USE_REGEX_PARAM, "true"); + SoapUIAssertion assertion = AssertionFactory.intializeAssertion(AssertionFactory.SIMPLE_CONTAINS, configParams); + AssertionStatus status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // Failing case. + status = assertion.assertResponse(new RequestResponseExchange(null, null, invalidSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + assertEquals(1, assertion.getErrorMessages().size()); + assertTrue(assertion.getErrorMessages().get(0).contains("Missing")); + + // RegExp + case insensitive passing case. + configParams = Map.of(SimpleContainsAssertion.TOKEN_PARAM, "HeLlO\\s(.*)!", + SimpleContainsAssertion.IGNORE_CASE_PARAM, "true", SimpleContainsAssertion.USE_REGEX_PARAM, "true"); + assertion = AssertionFactory.intializeAssertion(AssertionFactory.SIMPLE_CONTAINS, configParams); + status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/SimpleNotContainsAssertionTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/SimpleNotContainsAssertionTest.java new file mode 100644 index 000000000..7f81501aa --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/SimpleNotContainsAssertionTest.java @@ -0,0 +1,119 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This is a test case for class SimpleNotContainsAssertion. + * @author laurent + */ +class SimpleNotContainsAssertionTest { + + private String validSoap = """ + + + + + Hello Andrew! + + + + """; + + private String invalidSoap = """ + + + + + Andrew! + + + + """; + + @Test + void testExactMatch() { + // Passing case. + Map configParams = Map.of(SimpleNotContainsAssertion.TOKEN_PARAM, "Hello Andrew!", + SimpleNotContainsAssertion.IGNORE_CASE_PARAM, "false"); + SoapUIAssertion assertion = AssertionFactory.intializeAssertion(AssertionFactory.SIMPLE_NOT_CONTAINS, + configParams); + AssertionStatus status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + assertEquals(1, assertion.getErrorMessages().size()); + assertTrue(assertion.getErrorMessages().get(0).contains("Response contains")); + + // Failing case. + status = assertion.assertResponse(new RequestResponseExchange(null, null, invalidSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + } + + @Test + void testCaseSensitiveness() { + // Passing case. + Map configParams = Map.of(SimpleNotContainsAssertion.TOKEN_PARAM, "HeLlO AnDreW!", + SimpleNotContainsAssertion.IGNORE_CASE_PARAM, "true"); + SoapUIAssertion assertion = AssertionFactory.intializeAssertion(AssertionFactory.SIMPLE_NOT_CONTAINS, + configParams); + AssertionStatus status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + assertEquals(1, assertion.getErrorMessages().size()); + assertTrue(assertion.getErrorMessages().get(0).contains("Response contains")); + + // Failing case. + status = assertion.assertResponse(new RequestResponseExchange(null, null, invalidSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + } + + @Test + void testRegularExpression() { + // Passing case. + Map configParams = Map.of(SimpleNotContainsAssertion.TOKEN_PARAM, "Hello\\s(.*)!", + SimpleNotContainsAssertion.IGNORE_CASE_PARAM, "false", SimpleNotContainsAssertion.USE_REGEX_PARAM, "true"); + SoapUIAssertion assertion = AssertionFactory.intializeAssertion(AssertionFactory.SIMPLE_NOT_CONTAINS, + configParams); + AssertionStatus status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + assertEquals(1, assertion.getErrorMessages().size()); + assertTrue(assertion.getErrorMessages().get(0).contains("Response contains")); + + // Failing case. + status = assertion.assertResponse(new RequestResponseExchange(null, null, invalidSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // RegExp + case insensitive passing case. + configParams = Map.of(SimpleNotContainsAssertion.TOKEN_PARAM, "HeLlO\\s(.*)!", + SimpleNotContainsAssertion.IGNORE_CASE_PARAM, "true", SimpleNotContainsAssertion.USE_REGEX_PARAM, "true"); + assertion = AssertionFactory.intializeAssertion(AssertionFactory.SIMPLE_NOT_CONTAINS, configParams); + status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + assertEquals(1, assertion.getErrorMessages().size()); + assertTrue(assertion.getErrorMessages().get(0).contains("Response contains")); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/XPathContainsAssertionTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/XPathContainsAssertionTest.java new file mode 100644 index 000000000..ac0e03537 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/util/soapui/assertions/XPathContainsAssertionTest.java @@ -0,0 +1,142 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.util.soapui.assertions; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test case for class XPathContainsAssertion. + * @author laurent + */ +class XPathContainsAssertionTest { + + private String validSoap = """ + + + + + Hello Andrew ! + + + + """; + + private String invalidSoap = """ + + + + + Andrew! + + + + """; + + @Test + void testExactMatch() { + // Passing case. + Map configParams = Map.of(XPathContainsAssertion.PATH_PARAM, """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """, XPathContainsAssertion.EXPECTED_CONTENT_PARAM, "Hello Andrew !"); + SoapUIAssertion assertion = AssertionFactory.intializeAssertion(AssertionFactory.XPATH_CONTAINS, configParams); + AssertionStatus status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // Failing case. + configParams = Map.of(XPathContainsAssertion.PATH_PARAM, """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """, XPathContainsAssertion.EXPECTED_CONTENT_PARAM, "Andrew"); + assertion.configure(configParams); + status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + } + + @Test + void testFuzzyMatch() { + // Passing case with starting *. + Map configParams = Map.of(XPathContainsAssertion.PATH_PARAM, """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """, XPathContainsAssertion.EXPECTED_CONTENT_PARAM, "*Andrew*", XPathContainsAssertion.ALLOW_WILDCARDS, + "true"); + SoapUIAssertion assertion = AssertionFactory.intializeAssertion(AssertionFactory.XPATH_CONTAINS, configParams); + AssertionStatus status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // Passing case with middle *. + configParams = Map.of(XPathContainsAssertion.PATH_PARAM, """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """, XPathContainsAssertion.EXPECTED_CONTENT_PARAM, "Hello*!", XPathContainsAssertion.ALLOW_WILDCARDS, + "true"); + assertion = AssertionFactory.intializeAssertion(AssertionFactory.XPATH_CONTAINS, configParams); + status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // Passing case with ending *. + configParams = Map.of(XPathContainsAssertion.PATH_PARAM, """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """, XPathContainsAssertion.EXPECTED_CONTENT_PARAM, "Hello Andrew*", XPathContainsAssertion.ALLOW_WILDCARDS, + "true"); + assertion = AssertionFactory.intializeAssertion(AssertionFactory.XPATH_CONTAINS, configParams); + status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // Passing case with * everywhere. + configParams = Map.of(XPathContainsAssertion.PATH_PARAM, """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """, XPathContainsAssertion.EXPECTED_CONTENT_PARAM, "*Hello*Andrew*", + XPathContainsAssertion.ALLOW_WILDCARDS, "true"); + assertion = AssertionFactory.intializeAssertion(AssertionFactory.XPATH_CONTAINS, configParams); + status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.VALID, status); + + // Failing case. + configParams = Map.of(XPathContainsAssertion.PATH_PARAM, """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """, XPathContainsAssertion.EXPECTED_CONTENT_PARAM, "*Andrew", XPathContainsAssertion.ALLOW_WILDCARDS, + "true"); + assertion.configure(configParams); + status = assertion.assertResponse(new RequestResponseExchange(null, null, invalidSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + + configParams = Map.of(XPathContainsAssertion.PATH_PARAM, """ + declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello + """, XPathContainsAssertion.EXPECTED_CONTENT_PARAM, "Andrew*", XPathContainsAssertion.ALLOW_WILDCARDS, + "true"); + assertion.configure(configParams); + status = assertion.assertResponse(new RequestResponseExchange(null, null, validSoap, 100L), + new ExchangeContext(null, null, null, null)); + assertEquals(AssertionStatus.FAILED, status); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/AbstractBaseIT.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/AbstractBaseIT.java new file mode 100644 index 000000000..f9630687b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/AbstractBaseIT.java @@ -0,0 +1,110 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.MicrocksApplication; +import io.github.microcks.repository.ServiceRepository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.utility.DockerImageName; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Base class for Integration tests using Http layers as well as testcontainers for MongoDB persistence. + * @author laurent + */ +@SpringBootTest(classes = MicrocksApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("it") +@TestPropertySource(locations = { "classpath:/config/test.properties" }) +public abstract class AbstractBaseIT { + + /** A simple logger for diagnostic messages. */ + private static final Logger log = LoggerFactory.getLogger(AbstractBaseIT.class); + + @LocalServerPort + private int port; + + @Autowired + protected TestRestTemplate restTemplate; + + @Autowired + protected ServiceRepository serviceRepository; + + private static final MongoDBContainer mongoDBContainer; + + static { + mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4")).withReuse(false); + mongoDBContainer.start(); + } + + @DynamicPropertySource + public static void setDatasourceProperties(final DynamicPropertyRegistry registry) { + String url = "mongodb://" + mongoDBContainer.getHost() + ":" + mongoDBContainer.getMappedPort(27017) + + "/microcksIT"; + registry.add("spring.data.mongodb.uri", () -> url); + } + + public String getServerUrl() { + return "http://localhost:" + port; + } + + /** */ + protected void uploadArtifactFile(String artifactFilePath, boolean isMainArtifact) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", new FileSystemResource(new File(artifactFilePath))); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity response; + if (isMainArtifact) { + response = restTemplate.postForEntity("/api/artifact/upload", requestEntity, String.class); + } else { + response = restTemplate.postForEntity("/api/artifact/upload?mainArtifact=false", requestEntity, String.class); + } + + assertEquals(201, response.getStatusCode().value()); + log.info("Just uploaded: {}", response.getBody()); + } + + protected void assertResponseIsOkAndContains(ResponseEntity response, String substring) { + assertEquals(200, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().contains(substring)); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/ControllerTestsConfiguration.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/ControllerTestsConfiguration.java new file mode 100644 index 000000000..b550392ea --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/ControllerTestsConfiguration.java @@ -0,0 +1,24 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.ComponentScan; + +@TestConfiguration +@ComponentScan(basePackages = { "io.github.microcks.web" }) +public class ControllerTestsConfiguration { +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/GraphQLControllerIT.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/GraphQLControllerIT.java new file mode 100644 index 000000000..fec398ca1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/GraphQLControllerIT.java @@ -0,0 +1,220 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; + +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for all the GraphQL mock controller. + * @author laurent + */ +class GraphQLControllerIT extends AbstractBaseIT { + + @Test + void testFilmsGraphQLAPIMocking() { + // Upload the 2 required reference artifacts. + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films.graphql", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films-postman.json", false); + // Adding some more metadata for mutation dispatching. + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films-metadata.yml", false); + + // Check its different mocked operations. + String query = "{\"query\": \"query allFilms {\\n" + " allFilms {\\n" + " films {\\n" + " id\\n" + + " title\\n" + " }\\n" + " }\\n" + "}\"}"; + ResponseEntity response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\n" + " \"data\": {\n" + " \"allFilms\": {\n" + " \"films\": [\n" + + " {\n" + " \"id\": \"ZmlsbXM6MQ==\",\n" + " \"title\": \"A New Hope\"\n" + + " },\n" + " {\n" + " \"id\": \"ZmlsbXM6Mg==\",\n" + + " \"title\": \"The Empire Strikes Back\"\n" + " },\n" + " {\n" + + " \"id\": \"ZmlsbXM6Mw==\",\n" + " \"title\": \"Return of the Jedi\"\n" + + " },\n" + " {\n" + " \"id\": \"ZmlsbXM6NA==\",\n" + + " \"title\": \"The Phantom Menace\"\n" + " },\n" + " {\n" + + " \"id\": \"ZmlsbXM6NQ==\",\n" + " \"title\": \"Attack of the Clones\"\n" + + " },\n" + " {\n" + " \"id\": \"ZmlsbXM6Ng==\",\n" + + " \"title\": \"Revenge of the Sith\"\n" + " }\n" + " ]\n" + " }\n" + " }\n" + + "}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Check query with inlined argument. + query = "{\"query\": \"query film($id: String) {\\n" + " film(id: \\\"ZmlsbXM6MQ==\\\") {\\n" + " id\\n" + + " title\\n" + " episodeID\\n" + " starCount\\n" + " }\\n" + "}\"}"; + response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\n" + " \"data\": {\n" + " \"film\": {\n" + " \"id\": \"ZmlsbXM6MQ==\",\n" + + " \"title\": \"A New Hope\",\n" + " \"episodeID\": 4,\n" + " \"starCount\": 432\n" + + " }\n" + " }\n" + "}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Check query with variable argument. + query = "{\"query\": \"query film($id: String) {\\n" + " film(id: $id) {\\n" + " id\\n" + " title\\n" + + " episodeID\\n" + " starCount\\n" + " }\\n" + "}\", \"variables\": {\"id\": \"ZmlsbXM6MQ==\"}" + + "}"; + response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\n" + " \"data\": {\n" + " \"film\": {\n" + " \"id\": \"ZmlsbXM6MQ==\",\n" + + " \"title\": \"A New Hope\",\n" + " \"episodeID\": 4,\n" + " \"starCount\": 432\n" + + " }\n" + " }\n" + "}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Check query with fragment definition. + query = "{\"query\": \"query film($id: String) {\\n" + " film(id: \\\"ZmlsbXM6MQ==\\\") {\\n" + + " ...filmFields\\n" + " }\\n" + " }\\n" + " fragment filmFields on Film {\\n" + " id\\n" + + " title\\n" + " episodeID\\n" + " starCount\\n" + " }\\n" + "\"}"; + response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\n" + " \"data\": {\n" + " \"film\": {\n" + " \"id\": \"ZmlsbXM6MQ==\",\n" + + " \"title\": \"A New Hope\",\n" + " \"episodeID\": 4,\n" + " \"starCount\": 432\n" + + " }\n" + " }\n" + "}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Check query with multiple selection and aliases + query = "{\"query\": \"{\\n" + " film_one: film(id: \\\"ZmlsbXM6MQ==\\\") {\\n" + " id\\n" + " title\\n" + + " episodeID\\n" + " rating\\n" + " }\\n" + " film_two: film(id: \\\"ZmlsbXM6Mg==\\\") {\\n" + + " id\\n" + " title\\n" + " episodeID\\n" + " director\\n" + " starCount\\n" + " }\\n" + + "}\"}"; + response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\n" + " \"data\": {\n" + " \"film_one\": {\n" + + " \"id\": \"ZmlsbXM6MQ==\",\n" + " \"title\": \"A New Hope\",\n" + + " \"episodeID\": 4,\n" + " \"rating\": 4.3\n" + " },\n" + " \"film_two\": {\n" + + " \"id\": \"ZmlsbXM6Mg==\",\n" + " \"title\": \"The Empire Strikes Back\",\n" + + " \"episodeID\": 5,\n" + " \"director\": \"Irvin Kershner\",\n" + + " \"starCount\": 433\n" + " }\n" + " }\n" + "}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Check query with multiple selection no aliases + query = "{\"query\": \"{\\n" + " film(id: \\\"ZmlsbXM6MQ==\\\") {\\n" + " id\\n" + " title\\n" + + " episodeID\\n" + " rating\\n" + " }\\n" + " allFilms {\\n" + " films {\\n" + " id\\n" + + " title\\n" + " }\\n" + " }\\n" + "}\"}"; + response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\n" + " \"data\": {\n" + " \"film\": {\n" + " \"id\": \"ZmlsbXM6MQ==\",\n" + + " \"title\": \"A New Hope\",\n" + " \"episodeID\": 4,\n" + " \"rating\": 4.3\n" + + " },\n" + " \"allFilms\": {\n" + " \"films\": [\n" + " {\n" + + " \"id\": \"ZmlsbXM6MQ==\",\n" + " \"title\": \"A New Hope\"\n" + " },\n" + + " {\n" + " \"id\": \"ZmlsbXM6Mg==\",\n" + + " \"title\": \"The Empire Strikes Back\"\n" + " },\n" + " {\n" + + " \"id\": \"ZmlsbXM6Mw==\",\n" + " \"title\": \"Return of the Jedi\"\n" + + " },\n" + " {\n" + " \"id\": \"ZmlsbXM6NA==\",\n" + + " \"title\": \"The Phantom Menace\"\n" + " },\n" + " {\n" + + " \"id\": \"ZmlsbXM6NQ==\",\n" + " \"title\": \"Attack of the Clones\"\n" + + " },\n" + " {\n" + " \"id\": \"ZmlsbXM6Ng==\",\n" + + " \"title\": \"Revenge of the Sith\"\n" + " }\n" + " ]\n" + " }\n" + " }\n" + + "}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Check query with __typename and inlined argument. + query = "{\"query\": \"query film($id: String) {\\n" + " __typename\\n film(id: \\\"ZmlsbXM6MQ==\\\") {\\n" + + " id\\n" + " title\\n" + " episodeID\\n" + " starCount\\n" + " }\\n" + "}\"}"; + response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals( + "{\n" + " \"data\": {\n" + " \"__typename\":\"Query\", \"film\": {\n" + + " \"id\": \"ZmlsbXM6MQ==\",\n" + " \"title\": \"A New Hope\",\n" + + " \"episodeID\": 4,\n" + " \"starCount\": 432\n" + " }\n" + " }\n" + "}", + response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + } + + @Test + void testProxy() { + // Upload the required reference artifacts. + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films.graphql", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films-postman.json", false); + + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films-to-test-proxy.graphql", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films-to-test-proxy-postman.json", false); + + // Override the dispatcher to PROXY + Service service = serviceRepository.findByNameAndVersion("Movie Graph API", "1.0"); + Operation operation = service.getOperations().stream().filter((o) -> "film".equals(o.getName())).findFirst() + .orElseThrow(); + operation.setDispatcher("PROXY"); + operation.setDispatcherRules(getServerUrl() + "/graphql/Movie+Graph+Original+API/1.0"); + serviceRepository.save(service); + + // Execute and assert that it was proxy. + String query = """ + {"query": "query film($id: String) {film(id: \\"ZmlsbXM6Mg==\\") {id title episodeID starCount comment}}"}"""; + ResponseEntity response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertResponseIsOkAndContains(response, "\"comment\":\"Original!!!\""); + } + + @Test + void testProxyFallback() { + // Upload the required reference artifacts. + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films.graphql", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films-postman.json", false); + + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films-to-test-proxy.graphql", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/films-to-test-proxy-postman.json", false); + + // Override the dispatcher to PROXY + Service service = serviceRepository.findByNameAndVersion("Movie Graph API", "1.0"); + Operation operation = service.getOperations().stream().filter((o) -> "film".equals(o.getName())).findFirst() + .orElseThrow(); + operation.setDispatcher("PROXY_FALLBACK"); + operation.setDispatcherRules(String.format(""" + {"dispatcher": "QUERY_ARGS", + "dispatcherRules": "id", + "proxyUrl": "%s/graphql/Movie+Graph+Original+API/1.0"}""", getServerUrl())); + serviceRepository.save(service); + + // Execute and assert that it wasn't proxy. + String query = """ + {"query": "query film($id: String) {film(id: \\"ZmlsbXM6Mg==\\") {id title episodeID starCount comment}}"}"""; + ResponseEntity response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertEquals(200, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertFalse(response.getBody().contains("\"comment\":\"Original!!!\"")); + + // Execute and assert that it was proxy. + query = """ + {"query": "query film($id: String) {film(id: \\"ZmlsbXM6MA==\\") {id title episodeID starCount comment}}"}"""; + response = restTemplate.postForEntity("/graphql/Movie+Graph+API/1.0", query, String.class); + assertResponseIsOkAndContains(response, "\"comment\":\"Original!!!\""); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/GrpcServerCallHandlerIT.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/GrpcServerCallHandlerIT.java new file mode 100644 index 000000000..3ca70d3a5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/GrpcServerCallHandlerIT.java @@ -0,0 +1,156 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import org.junit.jupiter.api.Test; +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for the GrpcServerCallHandler. + * @author laurent + */ +class GrpcServerCallHandlerIT extends AbstractBaseIT { + + private static final String GRPCURL_IMAGE = "quay.io/microcks/grpcurl:v1.8.9-alpine"; + + @Test + void testGrpcMockingWithQueryArgs() { + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.postman.json", false); + + Testcontainers.exposeHostPorts(9090); + + GenericContainer grpcurl = new GenericContainer(GRPCURL_IMAGE).withAccessToHost(true); + + try { + grpcurl.start(); + Container.ExecResult result = grpcurl.execInContainer("/bin/grpcurl", "-plaintext", "-d", """ + {"firstname": "Laurent", "lastname": "Broudoux"} + """, "host.testcontainers.internal:9090", "io.github.microcks.grpc.hello.v1.HelloService/greeting"); + + assertTrue(result.getStdout().contains("\"greeting\": \"Hello Laurent Broudoux !\"")); + + result = grpcurl.execInContainer("/bin/grpcurl", "-plaintext", "-d", """ + {"firstname": "Philippe", "lastname": "Huet"} + """, "host.testcontainers.internal:9090", "io.github.microcks.grpc.hello.v1.HelloService/greeting"); + + assertTrue(result.getStdout().contains("\"greeting\": \"Hello Philippe Huet !\"")); + } catch (Exception e) { + fail("No exception should be thrown"); + } finally { + grpcurl.stop(); + } + } + + @Test + void testGrpcMockingWithQueryArgsErrors() { + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/hello-v1-examples-errors.yml", false); + + Testcontainers.exposeHostPorts(9090); + + GenericContainer grpcurl = new GenericContainer(GRPCURL_IMAGE).withAccessToHost(true); + + try { + grpcurl.start(); + Container.ExecResult result = grpcurl.execInContainer("/bin/grpcurl", "-plaintext", "-d", """ + {"firstname": "Laurent", "lastname": "Broudoux"} + """, "host.testcontainers.internal:9090", "io.github.microcks.grpc.hello.v1.HelloService/greeting"); + + // Should be ok but print a warning as we specified a 200 Http code. + assertTrue(result.getStdout().contains("\"greeting\": \"Hello Laurent Broudoux !\"")); + + result = grpcurl.execInContainer("/bin/grpcurl", "-plaintext", "-d", """ + {"firstname": "John", "lastname": "Doe"} + """, "host.testcontainers.internal:9090", "io.github.microcks.grpc.hello.v1.HelloService/greeting"); + + // Should return a NotFound status error. + assertNotEquals(0, result.getExitCode()); + assertTrue(result.getStdout().trim().isEmpty()); + assertTrue(result.getStderr().contains("Code: NotFound")); + assertTrue(result.getStderr().contains("Message: Mocked response status code")); + } catch (Exception e) { + fail("No exception should be thrown"); + } finally { + grpcurl.stop(); + } + } + + + @Test + void testGrpcMockingWithCustomDispatcher() { + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.postman.json", false); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.metadata.yml", false); + + Testcontainers.exposeHostPorts(9090); + + GenericContainer grpcurl = new GenericContainer(GRPCURL_IMAGE).withAccessToHost(true); + + try { + grpcurl.start(); + Container.ExecResult result = grpcurl.execInContainer("/bin/grpcurl", "-plaintext", "-d", """ + {"firstname": "Laurent", "lastname": "Broudoux"} + """, "host.testcontainers.internal:9090", "io.github.microcks.grpc.hello.v1.HelloService/greeting"); + + assertTrue(result.getStdout().contains("\"greeting\": \"Hello Laurent Broudoux !\"")); + + result = grpcurl.execInContainer("/bin/grpcurl", "-plaintext", "-d", """ + {"firstname": "John", "lastname": "Doe"} + """, "host.testcontainers.internal:9090", "io.github.microcks.grpc.hello.v1.HelloService/greeting"); + + assertTrue(result.getStdout().contains("\"greeting\": \"Hello Philippe Huet !\"")); + } catch (Exception e) { + fail("No exception should be thrown"); + } finally { + grpcurl.stop(); + } + } + + @Test + void testGrpcReflection() { + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.postman.json", false); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.metadata.yml", false); + + Testcontainers.exposeHostPorts(9090); + + GenericContainer grpcurl = new GenericContainer(GRPCURL_IMAGE).withAccessToHost(true); + + try { + grpcurl.start(); + Container.ExecResult result = grpcurl.execInContainer("/bin/grpcurl", "-plaintext", + "host.testcontainers.internal:9090", "list"); + + assertTrue(result.getStdout().contains("io.github.microcks.grpc.hello.v1.HelloService")); + + result = grpcurl.execInContainer("/bin/grpcurl", "-plaintext", "host.testcontainers.internal:9090", "describe", + "io.github.microcks.grpc.hello.v1.HelloService.greeting"); + + assertTrue(result.getStdout().contains("io.github.microcks.grpc.hello.v1.HelloService.greeting is a method:")); + assertTrue(result.getStdout().contains( + "rpc greeting ( .io.github.microcks.grpc.hello.v1.HelloRequest ) returns ( .io.github.microcks.grpc.hello.v1.HelloResponse );")); + } catch (Exception e) { + fail("No exception should be thrown"); + } finally { + grpcurl.stop(); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/McpControllerIT.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/McpControllerIT.java new file mode 100644 index 000000000..3ec3e21f1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/McpControllerIT.java @@ -0,0 +1,571 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.util.ai.McpSchema; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for the MCP controller. + * @author laurent + */ +class McpControllerIT extends AbstractBaseIT { + + record McpSSEFrame(String key, String value) { + } + + private final ObjectMapper mapper = new ObjectMapper(); + + private List sseFrames; + private ExecutorService sseClientExecutor; + + @BeforeEach + void setUp() { + sseFrames = new ArrayList<>(); + sseClientExecutor = Executors.newSingleThreadExecutor(); + } + + @AfterEach + void tearDown() { + sseClientExecutor.shutdown(); + } + + @Test + void testOpenAPIHttpSSEEndpoint() throws Exception { + // Update Petstore from the tutorial reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/petstore-1.0.0-openapi.yaml", true); + + // Define the runnable for the SSE client. + Runnable sseClientRunnable = () -> { + try { + restTemplate.execute("/mcp/Petstore+API/1.0.0/sse", HttpMethod.GET, request -> {}, response -> { + String line; + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody()));) { + while ((line = bufferedReader.readLine()) != null) { + parseAndStoreMcpSseFrame(line, sseFrames); + } + } catch (IOException e) { + System.err.println("Caught exception while reading SSE response: " + e.getMessage()); + } + return response; + }); + } catch (Exception e) { + System.err.println("Caught exception while executing SSE client: " + e.getMessage()); + } + }; + sseClientExecutor.execute(sseClientRunnable); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(250); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + String messageEndpoint = null; + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("endpoint", frame.value()); + } else if (frame.key().equals("data")) { + messageEndpoint = frame.value(); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + assertNotNull(messageEndpoint); + + // Now clear the frames and send an initialize request to the message endpoint. + sseFrames.clear(); + String initializeRequest = """ + { + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + } + """; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)); + + ResponseEntity response = restTemplate.postForEntity(messageEndpoint, + new HttpEntity<>(initializeRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.InitializeResult result = mapper.convertValue(rpcResponse.result(), + McpSchema.InitializeResult.class); + assertEquals("Petstore API MCP server", result.serverInfo().name()); + assertEquals("1.0.0", result.serverInfo().version()); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + + // Now clear the frames and send a tools/list request to the message endpoint. + sseFrames.clear(); + String toolsListRequest = """ + { + "jsonrpc": "2.0", + "method": "tools/list" + } + """; + + response = restTemplate.postForEntity(messageEndpoint, new HttpEntity<>(toolsListRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.ListToolsResult result = mapper.convertValue(rpcResponse.result(), + McpSchema.ListToolsResult.class); + assertEquals(4, result.tools().size()); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + + // Now clear the frames and finallu send a tools/call request to the message endpoint. + sseFrames.clear(); + String toolsCallRequest = """ + { + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_pets_id", + "arguments": { + "id": "2" + } + } + } + """; + + response = restTemplate.postForEntity(messageEndpoint, new HttpEntity<>(toolsCallRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.CallToolResult result = mapper.convertValue(rpcResponse.result(), McpSchema.CallToolResult.class); + assertTrue(result.content().getFirst() instanceof McpSchema.TextContent); + McpSchema.TextContent content = (McpSchema.TextContent) result.content().getFirst(); + assertEquals("{\"id\":2,\"name\":\"Tigresse\"}", content.text()); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + } + + @Test + void testGrpcHttpSSEEndpoint() throws Exception { + // Update Petstore from the tutorial reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/petstore-v1.proto", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/petstore-v1-examples.yaml", false); + + // Define the runnable for the SSE client. + Runnable sseClientRunnable = () -> { + try { + restTemplate.execute("/mcp/org.acme.petstore.v1.PetstoreService/v1/sse", HttpMethod.GET, request -> {}, + response -> { + String line; + try (BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(response.getBody()));) { + while ((line = bufferedReader.readLine()) != null) { + parseAndStoreMcpSseFrame(line, sseFrames); + } + } catch (IOException e) { + System.err.println("Caught exception while reading SSE response: " + e.getMessage()); + } + return response; + }); + } catch (Exception e) { + System.err.println("Caught exception while executing SSE client: " + e.getMessage()); + } + }; + sseClientExecutor.execute(sseClientRunnable); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(250); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + String messageEndpoint = null; + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("endpoint", frame.value()); + } else if (frame.key().equals("data")) { + messageEndpoint = frame.value(); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + assertNotNull(messageEndpoint); + + // Now clear the frames and send an initialize request to the message endpoint. + sseFrames.clear(); + String initializeRequest = """ + { + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + } + """; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)); + + ResponseEntity response = restTemplate.postForEntity(messageEndpoint, + new HttpEntity<>(initializeRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.InitializeResult result = mapper.convertValue(rpcResponse.result(), + McpSchema.InitializeResult.class); + assertEquals("org.acme.petstore.v1.PetstoreService MCP server", result.serverInfo().name()); + assertEquals("v1", result.serverInfo().version()); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + + // Now clear the frames and send a tools/list request to the message endpoint. + sseFrames.clear(); + String toolsListRequest = """ + { + "jsonrpc": "2.0", + "method": "tools/list" + } + """; + + response = restTemplate.postForEntity(messageEndpoint, new HttpEntity<>(toolsListRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.ListToolsResult result = mapper.convertValue(rpcResponse.result(), + McpSchema.ListToolsResult.class); + assertEquals(3, result.tools().size()); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + + // Now clear the frames and finallu send a tools/call request to the message endpoint. + sseFrames.clear(); + String toolsCallRequest = """ + { + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "searchPets", + "arguments": { + "name": "k" + } + } + } + """; + + response = restTemplate.postForEntity(messageEndpoint, new HttpEntity<>(toolsCallRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.CallToolResult result = mapper.convertValue(rpcResponse.result(), McpSchema.CallToolResult.class); + assertTrue(result.content().getFirst() instanceof McpSchema.TextContent); + McpSchema.TextContent content = (McpSchema.TextContent) result.content().getFirst(); + assertEquals("{\n" + " \"pets\": [\n" + " {\n" + " \"id\": 3,\n" + " \"name\": \"Maki\"\n" + + " },\n" + " {\n" + " \"id\": 4,\n" + " \"name\": \"Toufik\"\n" + " }\n" + " ]\n" + + "}", content.text()); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + } + + @Test + void testGraphQLHttpSSEEndpoint() throws Exception { + // Update Petstore from the tutorial reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/petstore-1.0.graphql", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/graphql/petstore-1.0-examples.yaml", false); + + // Define the runnable for the SSE client. + Runnable sseClientRunnable = () -> { + try { + restTemplate.execute("/mcp/Petstore+Graph+API/1.0/sse", HttpMethod.GET, request -> {}, response -> { + String line; + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody()));) { + while ((line = bufferedReader.readLine()) != null) { + parseAndStoreMcpSseFrame(line, sseFrames); + } + } catch (IOException e) { + System.err.println("Caught exception while reading SSE response: " + e.getMessage()); + } + return response; + }); + } catch (Exception e) { + System.err.println("Caught exception while executing SSE client: " + e.getMessage()); + } + }; + sseClientExecutor.execute(sseClientRunnable); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(250); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + String messageEndpoint = null; + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("endpoint", frame.value()); + } else if (frame.key().equals("data")) { + messageEndpoint = frame.value(); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + assertNotNull(messageEndpoint); + + // Now clear the frames and send an initialize request to the message endpoint. + sseFrames.clear(); + String initializeRequest = """ + { + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + } + """; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)); + + ResponseEntity response = restTemplate.postForEntity(messageEndpoint, + new HttpEntity<>(initializeRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.InitializeResult result = mapper.convertValue(rpcResponse.result(), + McpSchema.InitializeResult.class); + assertEquals("Petstore Graph API MCP server", result.serverInfo().name()); + assertEquals("1.0", result.serverInfo().version()); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + + // Now clear the frames and send a tools/list request to the message endpoint. + sseFrames.clear(); + String toolsListRequest = """ + { + "jsonrpc": "2.0", + "method": "tools/list" + } + """; + + response = restTemplate.postForEntity(messageEndpoint, new HttpEntity<>(toolsListRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.ListToolsResult result = mapper.convertValue(rpcResponse.result(), + McpSchema.ListToolsResult.class); + assertEquals(4, result.tools().size()); + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + + // Now clear the frames and finallu send a tools/call request to the message endpoint. + sseFrames.clear(); + String toolsCallRequest = """ + { + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "createPet", + "arguments": { + "newPet": { + "name": "Rusty", + "color": "stripped" + } + } + } + } + """; + + response = restTemplate.postForEntity(messageEndpoint, new HttpEntity<>(toolsCallRequest, headers), String.class); + + // SSE emitter is async so wait a few millis before checking. + Thread.sleep(200); + + // Check we got 3 frames and that we're able to extract the message endpoint. + assertEquals(3, sseFrames.size()); + + for (McpSSEFrame frame : sseFrames) { + if (frame.key().equals("event")) { + assertEquals("message", frame.value()); + } else if (frame.key().equals("data")) { + McpSchema.JSONRPCResponse rpcResponse = mapper.readValue(frame.value(), McpSchema.JSONRPCResponse.class); + McpSchema.CallToolResult result = mapper.convertValue(rpcResponse.result(), McpSchema.CallToolResult.class); + assertTrue(result.content().getFirst() instanceof McpSchema.TextContent); + McpSchema.TextContent content = (McpSchema.TextContent) result.content().getFirst(); + + assertTrue(content.text().contains("\"id\":")); + assertTrue(content.text().contains("\"name\":\"Rusty\"")); + assertTrue(content.text().contains("\"color\":\"stripped\"}")); + + } else if (frame.key().equals("id")) { + // Got and id frame, ignore it. + } else { + fail("Unknown SSE frame: " + frame.key()); + } + } + } + + private void parseAndStoreMcpSseFrame(String line, List sseFrames) { + if (line.contains(":")) { + String key = line.substring(0, line.indexOf(':')); + String value = line.substring(line.indexOf(':') + 1).trim(); + sseFrames.add(new McpSSEFrame(key, value)); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/RestControllerIT.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/RestControllerIT.java new file mode 100644 index 000000000..a5d0e41b7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/RestControllerIT.java @@ -0,0 +1,426 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.comparator.ArraySizeComparator; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMapAdapter; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for all the Rest mock controller. + * @author laurent + */ +class RestControllerIT extends AbstractBaseIT { + + @SpyBean + private RestController restController; + + @Test + void testOpenAPIMocking() { + // Upload PetStore reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/petstore-openapi.json", true); + + // Check its different mocked operations. + ResponseEntity response = restTemplate.getForEntity("/rest/PetStore+API/1.0.0/pets", String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("[4]", response.getBody(), new ArraySizeComparator(JSONCompareMode.LENIENT)); + JSONAssert.assertEquals( + "[{\"id\":1,\"name\":\"Zaza\",\"tag\":\"cat\"},{\"id\":2,\"name\":\"Tigresse\",\"tag\":\"cat\"},{\"id\":3,\"name\":\"Maki\",\"tag\":\"cat\"},{\"id\":4,\"name\":\"Toufik\",\"tag\":\"cat\"}]", + response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + response = restTemplate.getForEntity("/rest/PetStore+API/1.0.0/pets/1", String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\"id\":1,\"name\":\"Zaza\",\"tag\":\"cat\"}", response.getBody(), + JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + } + + @Test + void testOpenAPIMockingWithValidation() { + // Upload PetStore reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-details-openapi.yaml", true); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + + // Check its validation endpoint with correct payload + String patchedPastry = "{\"price\":2.6}"; + HttpEntity requestEntity = new HttpEntity<>(patchedPastry, headers); + ResponseEntity response = restTemplate.exchange("/rest-valid/pastry-details/1.0.0/pastry/Eclair+Cafe", + HttpMethod.PATCH, requestEntity, String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals( + "{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.6,\"status\":\"available\"}", + response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Check its validation endpoint with invalid payload + patchedPastry = "{\"price\":\"2.6\"}"; + requestEntity = new HttpEntity<>(patchedPastry, headers); + response = restTemplate.exchange("/rest-valid/pastry-details/1.0.0/pastry/Eclair+Cafe", HttpMethod.PATCH, + requestEntity, String.class); + assertEquals(400, response.getStatusCode().value()); + assertEquals("[string found, number expected]", response.getBody()); + } + + @Test + void testSwaggerMocking() { + // Upload Beer Catalog API swagger and then Postman collection artifacts. + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/beer-catalog-api-swagger.json", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/beer-catalog-api-collection.json", false); + + // Check its different mocked operations. + ResponseEntity response = restTemplate.getForEntity("/rest/Beer+Catalog+API/0.9/beer?page=0", + String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("[3]", response.getBody(), new ArraySizeComparator(JSONCompareMode.LENIENT)); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + response = restTemplate.getForEntity("/rest/Beer+Catalog+API/0.9/beer/Weissbier", String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\n" + " \"name\": \"Weissbier\",\n" + " \"country\": \"Germany\",\n" + + " \"type\": \"Wheat\",\n" + " \"rating\": 4.1,\n" + " \"status\": \"out_of_stock\"\n" + "}", + response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + } + + @Test + void testNoFallbackMatchingWithRegex() { + // Upload modified pastry spec + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-details-openapi.yaml", true); + + ObjectMapper mapper = new ObjectMapper(); + + // Check operation with a defined mock (name: 'Millefeuille') + ResponseEntity response = restTemplate + .getForEntity("/rest/pastry-details/1.0.0/pastry/Millefeuille/details", String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JsonNode details = mapper.readTree(response.getBody()); + String description = details.get("description").asText(); + assertTrue(description.startsWith("Detail -")); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Check operation with an undefined defined mock (name: 'Dummy'), should now return a 400 error as + // per issue #819 and #1132 to have a consistent behaviour, allow proxying support and this kind of stuff. + response = restTemplate.getForEntity("/rest/pastry-details/1.0.0/pastry/Dummy/details", String.class); + assertEquals(400, response.getStatusCode().value()); + } + + @Test + void testHeadersTemplating() { + // Upload modified pastry-with-headers-openapi spec + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-headers-openapi.yaml", true); + + ResponseEntity response = restTemplate.getForEntity("/rest/pastry-headers/1.0.0/pastry", String.class); + assertEquals(200, response.getStatusCode().value()); + assertEquals("some-static-header", response.getHeaders().getFirst("x-some-static-header")); + + String someGenericHeader = response.getHeaders().getFirst("x-some-generic-header"); + assertDoesNotThrow(() -> UUID.fromString(someGenericHeader)); + + response = restTemplate.getForEntity("/rest/pastry-headers/1.0.0/pastry?size=XL", String.class); + String requestBasedHeader = response.getHeaders().getFirst("x-request-based-header"); + assertEquals("XL size", requestBasedHeader); + } + + @Test + void testHeadersOnlyResponse() { + // Upload simple-oidc-redirect-openapi spec + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/simple-oidc-redirect-openapi.yaml", true); + + ResponseEntity response = restTemplate.getForEntity("/rest/Simple+OIDC/1.0/login/oauth/authorize?" + + "response_type=code&client_id=GHCLIENT&scope=openid+user:email&redirect_uri=http://localhost:8080/Login/githubLoginSuccess&state=e956e017-5e13-4c9d-b83b-6dd6337a6a86", + String.class); + assertEquals(302, response.getStatusCode().value()); + + String content = response.getBody(); + assertNull(content); + + String location = response.getHeaders().getFirst("location"); + assertNotNull(location); + assertTrue(location.startsWith("http://localhost:8080/Login/githubLoginSuccess?")); + assertTrue(location.contains("state=e956e017-5e13-4c9d-b83b-6dd6337a6a86")); + } + + @Test + void testProxyFallback() { + // Upload pastry-with-proxy-fallback and pastry-for-proxy specs + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-proxy-fallback-openapi.yaml", + true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-for-proxy-openapi.yaml", true); + + // Set real port to the dispatcher + Service service = serviceRepository.findByNameAndVersion("pastry-proxy", "1.0.0"); + Operation op = service.getOperations().stream().filter(o -> o.getName().endsWith("GET /pastry")).findFirst() + .orElseThrow(); + op.setDispatcherRules(op.getDispatcherRules().replaceFirst("http://localhost", getServerUrl())); + serviceRepository.save(service); + + // If we have the mock, we should get the response from the mock. + ResponseEntity response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry?name=donut", + String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\"name\":\"Mocked One\"}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // If we don't have the mock, we should get the response from real backend. + response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry?name=croissant", String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\"name\":\"Croissant from Real One\"}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + } + + @Test + void testProxyFallbackWithDelay() { + // Upload pastry-with-proxy-fallback and pastry-for-proxy specs + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-proxy-fallback-openapi.yaml", + true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-for-proxy-openapi.yaml", true); + + // Set real port to the dispatcher + Service service = serviceRepository.findByNameAndVersion("pastry-proxy", "1.0.0"); + Operation op = service.getOperations().stream().filter(o -> o.getName().endsWith("GET /pastry")).findFirst() + .orElseThrow(); + op.setDispatcherRules(op.getDispatcherRules().replaceFirst("http://localhost", getServerUrl())); + serviceRepository.save(service); + + // If we have the mock, we should get the response from the mock. + long startTime = System.currentTimeMillis(); + ResponseEntity response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry?name=donut", + String.class); + long mockedResponseTime = System.currentTimeMillis() - startTime; + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\"name\":\"Mocked One\"}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // If we don't have the mock, we should get the response from real backend. + startTime = System.currentTimeMillis(); + response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry?name=croissant", String.class); + long realResponseTime = System.currentTimeMillis() - startTime; + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\"name\":\"Croissant from Real One\"}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // Introduce request delay. + long delay = 150l; + op.setDefaultDelay(delay); + serviceRepository.save(service); + + // If we have the mock, we should get the response from the mock. + startTime = System.currentTimeMillis(); + response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry?name=donut", String.class); + long mockedResponseTimeDelayed = System.currentTimeMillis() - startTime; + // Assert that the response time is greater than the delay and greater that . + assertTrue(mockedResponseTimeDelayed >= delay, + "mocked response time delayed: " + mockedResponseTimeDelayed + "ms"); + assertTrue(mockedResponseTimeDelayed >= mockedResponseTime, + "mocked response time: " + mockedResponseTime + "ms, delayed: " + mockedResponseTimeDelayed + "ms"); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\"name\":\"Mocked One\"}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + + // If we don't have the mock, we should get the response from real backend. + startTime = System.currentTimeMillis(); + response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry?name=croissant", String.class); + long realResponseTimeDelayed = System.currentTimeMillis() - startTime; + // Assert that the response time is greater than the delay and greater that . + assertTrue(realResponseTimeDelayed >= delay, "real response time delayed: " + realResponseTimeDelayed + "ms"); + assertTrue(realResponseTimeDelayed >= realResponseTime, + "real response time: " + mockedResponseTime + "ms, delayed: " + realResponseTimeDelayed + "ms"); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\"name\":\"Croissant from Real One\"}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + } + + @Test + void testProxyFallbackWithEqualsOriginAndExternalUrls() { + // Upload pastry-with-proxy-fallback and pastry-for-proxy specs + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-proxy-fallback-openapi.yaml", + true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-for-proxy-openapi.yaml", true); + + // Set original URL to the dispatcher + Service service = serviceRepository.findByNameAndVersion("pastry-proxy", "1.0.0"); + Operation op = service.getOperations().stream().filter(o -> o.getName().endsWith("GET /pastry")).findFirst() + .orElseThrow(); + op.setDispatcherRules(op.getDispatcherRules().replaceFirst("http://localhost", getServerUrl()) + .replaceFirst("pastry-real", "pastry-proxy")); + serviceRepository.save(service); + + // Check that we don't fall into infinite loop and that we can't locally handle the call (error 400) + ResponseEntity response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry?name=realDonut", + String.class); + assertEquals(400, response.getStatusCode().value()); + verify(restController, times(1)).execute(any(), any(), any(), any(), any(), any(), any()); + } + + @Test + void testProxyFallbackWithHttpError() { + // Upload pastry-with-proxy-fallback and pastry-for-proxy specs + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-proxy-fallback-openapi.yaml", + true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-for-proxy-openapi.yaml", true); + + // Broke external URL in the dispatcher + Service service = serviceRepository.findByNameAndVersion("pastry-proxy", "1.0.0"); + Operation op = service.getOperations().stream().filter(o -> o.getName().endsWith("GET /pastry")).findFirst() + .orElseThrow(); + op.setDispatcherRules(op.getDispatcherRules().replaceFirst("http://localhost", getServerUrl()) + .replaceFirst("pastry-real", "not-found")); + serviceRepository.save(service); + + ResponseEntity response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry?name=realDonut", + String.class); + assertEquals(404, response.getStatusCode().value()); + } + + @Test + void testProxy() { + // Upload pastry-with-proxy and pastry-for-proxy specs + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-proxy-openapi.yaml", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-for-proxy-openapi.yaml", true); + + // Set real port to the dispatcher + Service service = serviceRepository.findByNameAndVersion("pastry-proxy", "1.0.0"); + Operation op = service.getOperations().stream().filter(o -> o.getName().endsWith("GET /pastry/{name}")) + .findFirst().orElseThrow(); + op.setDispatcherRules(op.getDispatcherRules().replaceFirst("http://localhost", getServerUrl())); + serviceRepository.save(service); + + // Event if `donut` is defined on our mock, we should always have the response coming for real backend. + ResponseEntity response = restTemplate.getForEntity("/rest/pastry-proxy/1.0.0/pastry/donut", + String.class); + assertEquals(200, response.getStatusCode().value()); + try { + JSONAssert.assertEquals("{\"name\":\"Real One\"}", response.getBody(), JSONCompareMode.LENIENT); + } catch (Exception e) { + fail("No Exception should be thrown here"); + } + } + + @Test + void testScriptDispatcher() { + // Upload modified pastry spec + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/pastry-with-script-openapi.yaml", true); + + ObjectMapper mapper = new ObjectMapper(); + + // Check operation with a defined mock (name: 'Eclair Cafe') + ResponseEntity response = restTemplate.getForEntity("/rest/pastry-script/1.0.0/pastry/Eclair Cafe/taste", + String.class); + assertEquals(200, response.getStatusCode().value()); + assertEquals("Delicious", response.getBody()); + + // Check operation with a defined mock (name: 'Millefeuille') + response = restTemplate.getForEntity("/rest/pastry-script/1.0.0/pastry/Millefeuille/taste", String.class); + assertEquals(200, response.getStatusCode().value()); + assertEquals("Awesome", response.getBody()); + + // Check operation with an undefined mock (name: 'Dummy') + response = restTemplate.getForEntity("/rest/pastry-script/1.0.0/pastry/Dummy/taste", String.class); + assertEquals(200, response.getStatusCode().value()); + assertEquals("Ok", response.getBody()); + } + + @Test + void testDelay() { + // Upload PetStore reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/openapi/petstore-openapi.json", true); + + // Check a delayed mocked operations. + long startTime = System.currentTimeMillis(); + ResponseEntity response = restTemplate.getForEntity("/rest/PetStore+API/1.0.0/pets?delay=200", + String.class); + long mockedResponseTime = System.currentTimeMillis() - startTime; + // Assert that the response time is greater than the delay and greater that . + assertTrue(mockedResponseTime >= 200, "mocked response time delayed: " + mockedResponseTime + "ms"); + assertEquals(200, response.getStatusCode().value()); + + // Now use a header to specify mock response time. + HttpHeaders headers = new HttpHeaders(); + headers.set("x-microcks-delay", "400"); + HttpEntity requestEntity = new HttpEntity<>(headers); + + startTime = System.currentTimeMillis(); + response = restTemplate.exchange("/rest/PetStore+API/1.0.0/pets?delay=200", HttpMethod.GET, requestEntity, + String.class); + mockedResponseTime = System.currentTimeMillis() - startTime; + // Assert that the response time is greater than the delay and greater that . + assertTrue(mockedResponseTime >= 400, "mocked response time delayed: " + mockedResponseTime + "ms"); + assertEquals(200, response.getStatusCode().value()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/SoapControllerIT.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/SoapControllerIT.java new file mode 100644 index 000000000..54e49e22c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/SoapControllerIT.java @@ -0,0 +1,232 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.domain.Operation; +import io.github.microcks.domain.Service; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for the Soap mock controller. + * @author laurent + */ +class SoapControllerIT extends AbstractBaseIT { + + @Test + void testHelloSoapWSMocking() { + // Upload Hello Service SoapUI project. + uploadArtifactFile("target/test-classes/io/github/microcks/util/soapui/HelloService-soapui-project.xml", true); + + // Create SOAP 1.2 headers for sayHello operation. + HttpHeaders headers = new HttpHeaders(); + headers.put("Content-type", Collections.singletonList("application/soap+xml;action=sayHello")); + + // Build the request. + String request = "\n" + + " \n" + " \n" + " \n" + + " Karla\n" + " \n" + " \n" + + ""; + HttpEntity entity = new HttpEntity<>(request, headers); + + // Execute and assert. + ResponseEntity response = restTemplate.postForEntity("/soap/HelloService+Mock/0.9", entity, String.class); + assertEquals(200, response.getStatusCode().value()); + assertEquals( + "\n" + + " \n" + " \n" + " \n" + + " Hello Karla !\n" + " \n" + + " \n" + "", + response.getBody()); + assertEquals("application/soap+xml;charset=UTF-8", response.getHeaders().getContentType().toString()); + + // Create SOAP 1.1 headers for sayHello operation. + headers = new HttpHeaders(); + headers.put("SOAPAction", Collections.singletonList("\"sayHello\"")); + + // Build the request. + request = "\n" + + " \n" + " \n" + " \n" + + " Andrew\n" + " \n" + " \n" + + ""; + entity = new HttpEntity<>(request, headers); + + // Execute and assert, content-type is different for SOAP 1.1. + response = restTemplate.postForEntity("/soap/HelloService+Mock/0.9", entity, String.class); + assertEquals(200, response.getStatusCode().value()); + assertEquals( + "\n" + + " \n" + " \n" + " \n" + + " Hello Andrew !\n" + " \n" + + " \n" + "", + response.getBody()); + assertEquals("text/xml;charset=UTF-8", response.getHeaders().getContentType().toString()); + + // Test exception case. + request = "\n" + + " \n" + " \n" + " \n" + + " World\n" + " \n" + " \n" + + ""; + entity = new HttpEntity<>(request, headers); + + // Execute and assert. + response = restTemplate.postForEntity("/soap/HelloService+Mock/0.9", entity, String.class); + assertEquals(500, response.getStatusCode().value()); + assertEquals( + "\n" + + " \n" + " \n" + " \n" + + " soapenv:Sender\n" + + " Unknown name\n" + " \n" + + " \n" + " 999\n" + + " \n" + " \n" + " \n" + + " \n" + "", + response.getBody()); + } + + @Test + void testHelloRandomSoapWSMocking() { + // given list of responses + List okResponses = new ArrayList<>(); + List koResponses = new ArrayList<>(); + okResponses.add( + "\n" + + " \n" + " \n" + " \n" + + " Hello Karla !\n" + " \n" + + " \n" + ""); + okResponses.add( + "\n" + + " \n" + " \n" + " \n" + + " Hello Andrew !\n" + " \n" + + " \n" + ""); + koResponses.add( + "\n" + + " \n" + " \n" + " \n" + + " soapenv:Sender\n" + + " Unknown name\n" + " \n" + + " \n" + " 999\n" + + " \n" + " \n" + " \n" + + " \n" + ""); + + + // Upload Hello Service SoapUI project. + uploadArtifactFile("target/test-classes/io/github/microcks/util/soapui/HelloService-random-soapui-project.xml", + true); + + // Create SOAP 1.2 headers for sayHello operation. + HttpHeaders headers = new HttpHeaders(); + headers.put("Content-type", Collections.singletonList("application/soap+xml;action=sayHello")); + + // Build the request. + String request = "\n" + + " \n" + " \n" + " \n" + + " whatever\n" + " \n" + " \n" + + ""; + HttpEntity entity = new HttpEntity<>(request, headers); + + // Execute and assert. + for (int i = 0; i < 10; ++i) { + ResponseEntity response = restTemplate.postForEntity("/soap/HelloService+Mock/0.9", entity, + String.class); + switch (response.getStatusCode().value()) { + case 200: + assertTrue(okResponses.contains(response.getBody())); + break; + case 500: + assertTrue(koResponses.contains(response.getBody())); + break; + default: + fail(); + } + } + } + + @Test + void testProxy() { + // Upload SoapUI projects for proxy test. + uploadArtifactFile( + "target/test-classes/io/github/microcks/util/soapui/HelloService-to-set-proxy-soapui-project.xml", true); + uploadArtifactFile( + "target/test-classes/io/github/microcks/util/soapui/HelloService-to-test-proxy-soapui-project.xml", true); + + // Override the dispatcher to PROXY + Service service = serviceRepository.findByNameAndVersion("HelloService Mock", "0.9"); + Operation operation = service.getOperations().stream().findFirst().orElseThrow(); + operation.setDispatcher("PROXY"); + operation.setDispatcherRules(getServerUrl() + "/soap/HelloService+Real/0.9"); + serviceRepository.save(service); + + // Build the request. + HttpEntity entity = createBaseEntityForName("Andrew"); + + // Execute and assert. + ResponseEntity response = restTemplate.postForEntity("/soap/HelloService+Mock/0.9", entity, String.class); + assertResponseIsOkAndContains(response, "Hello Real Andrew !"); + } + + @Test + void testProxyFallback() { + // Upload SoapUI projects for proxy test. + uploadArtifactFile( + "target/test-classes/io/github/microcks/util/soapui/HelloService-to-set-proxy-soapui-project.xml", true); + uploadArtifactFile( + "target/test-classes/io/github/microcks/util/soapui/HelloService-to-test-proxy-soapui-project.xml", true); + + // Override the dispatcher to PROXY_FALLBACK + Service service = serviceRepository.findByNameAndVersion("HelloService Mock", "0.9"); + Operation operation = service.getOperations().stream().findFirst().orElseThrow(); + operation.setDispatcher("PROXY_FALLBACK"); + operation.setDispatcherRules(String.format(""" + {"dispatcher": "QUERY_MATCH", + "dispatcherRules": "declare namespace ser='http://www.example.com/hello';\\n//ser:sayHello/name", + "proxyUrl": "%s/soap/HelloService+Real/0.9"}""", getServerUrl())); + serviceRepository.save(service); + + // Build the request that matches QUERY_MATCH. + HttpEntity entity = createBaseEntityForName("Andrew"); + + // Execute and assert that it wasn't proxy. + ResponseEntity response = restTemplate.postForEntity("/soap/HelloService+Mock/0.9", entity, String.class); + assertResponseIsOkAndContains(response, "Hello Andrew !"); + + // Build the request that doesn't match QUERY_MATCH. + entity = createBaseEntityForName("Garry"); + + // Execute and assert that it was proxy. + response = restTemplate.postForEntity("/soap/HelloService+Mock/0.9", entity, String.class); + assertResponseIsOkAndContains(response, "Hello Real Garry !"); + } + + private HttpEntity createBaseEntityForName(String name) { + HttpHeaders headers = new HttpHeaders(); + headers.put("Content-type", Collections.singletonList("application/soap+xml;action=sayHello")); + String request = String.format( + """ + + %s + """, + name); + return new HttpEntity<>(request, headers); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/SoapControllerTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/SoapControllerTest.java new file mode 100644 index 000000000..c5eaa6f1f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/SoapControllerTest.java @@ -0,0 +1,141 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * This is a Test for SoapController class. + * @laurent + */ +class SoapControllerTest { + + @Test + void testOriginalOperationParsing() { + String originalPayload = "\n" + + " \n" + " \n" + " \n" + + " Karla\n" + " \n" + " \n" + + ""; + + assertTrue(SoapController.hasPayloadCorrectStructureForOperation(originalPayload, "sayHello")); + + assertEquals("sayHello", SoapController.extractOperationName(originalPayload)); + } + + @Test + void testDashNamespaceOperationParsing() { + String dashNamespacePayload = "\n" + + " \n" + " \n" + " \n" + + " Karla\n" + " \n" + " \n" + + ""; + + assertTrue(SoapController.hasPayloadCorrectStructureForOperation(dashNamespacePayload, "sayHello")); + + assertEquals("sayHello", SoapController.extractOperationName(dashNamespacePayload)); + } + + @Test + void testOriginalOperationWithNSParsing() { + String originalPayloadOpWithNamespace = "\n" + + " \n" + " \n" + + " \n" + " Karla\n" + + " \n" + " \n" + ""; + + assertTrue(SoapController.hasPayloadCorrectStructureForOperation(originalPayloadOpWithNamespace, "sayHello")); + + assertEquals("sayHello", SoapController.extractOperationName(originalPayloadOpWithNamespace)); + } + + @Test + void testDashNamespaceOperationWithNSParsing() { + String dashNamespacePayloadOpWithNamespace = "\n" + + " \n" + " \n" + + " \n" + " Karla\n" + + " \n" + " \n" + ""; + + assertTrue( + SoapController.hasPayloadCorrectStructureForOperation(dashNamespacePayloadOpWithNamespace, "sayHello")); + + assertEquals("sayHello", SoapController.extractOperationName(dashNamespacePayloadOpWithNamespace)); + } + + @Test + void testNegativeOperationMatching() { + String otherOperationPayload = "\n" + + " \n" + " \n" + + " \n" + " Karla\n" + + " \n" + " \n" + ""; + + assertTrue(SoapController.hasPayloadCorrectStructureForOperation(otherOperationPayload, "sayHelloWorld")); + assertFalse(SoapController.hasPayloadCorrectStructureForOperation(otherOperationPayload, "sayHello")); + + assertEquals("sayHelloWorld", SoapController.extractOperationName(otherOperationPayload)); + } + + @Test + void testNoArgOperationMatching() { + String noArgOperationPayload = "\n" + + " \n" + " \n" + " \n" + " \n" + + ""; + + String noArgFullOperationPayload = "\n" + + " \n" + " \n" + " \n" + + " \n" + ""; + + String noArgOperationPayloadWithNamespace = "\n" + + " \n" + " \n" + + " \n" + " \n" + + ""; + + String noArgFullOperationPayloadWithNamespace = "\n" + + " \n" + " \n" + + " \n" + + " \n" + ""; + + assertTrue(SoapController.hasPayloadCorrectStructureForOperation(noArgOperationPayload, "sayHelloWorld")); + assertTrue(SoapController.hasPayloadCorrectStructureForOperation(noArgFullOperationPayload, "sayHelloWorld")); + assertTrue( + SoapController.hasPayloadCorrectStructureForOperation(noArgOperationPayloadWithNamespace, "sayHelloWorld")); + assertTrue(SoapController.hasPayloadCorrectStructureForOperation(noArgFullOperationPayloadWithNamespace, + "sayHelloWorld")); + + assertEquals("sayHelloWorld", SoapController.extractOperationName(noArgOperationPayload)); + assertEquals("sayHelloWorld", SoapController.extractOperationName(noArgFullOperationPayload)); + assertEquals("sayHelloWorld", SoapController.extractOperationName(noArgOperationPayloadWithNamespace)); + assertEquals("sayHelloWorld", SoapController.extractOperationName(noArgFullOperationPayloadWithNamespace)); + } + + @Test + void testConvertSoapUITemplate() { + String soapUITemplate = "${myParam}"; + String microcksTemplate = SoapController.convertSoapUITemplate(soapUITemplate); + assertEquals("{{ myParam }}", microcksTemplate); + + soapUITemplate = "${ myParam}${myOtherParam }"; + microcksTemplate = SoapController.convertSoapUITemplate(soapUITemplate); + assertEquals("{{ myParam }}{{ myOtherParam }}", + microcksTemplate); + + soapUITemplate = "\n" + " ${myParam}\n" + " ${myOtherParam}\n" + + ""; + String expectedResult = "\n" + " {{ myParam }}\n" + + " {{ myOtherParam }}\n" + ""; + microcksTemplate = SoapController.convertSoapUITemplate(soapUITemplate); + assertEquals(expectedResult, microcksTemplate); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/TestControllerIT.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/TestControllerIT.java new file mode 100644 index 000000000..736a30894 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/TestControllerIT.java @@ -0,0 +1,259 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.microcks.web; + +import io.github.microcks.domain.RequestResponsePair; +import io.github.microcks.domain.TestResult; +import io.github.microcks.domain.TestStepResult; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for the Test controller. + * @author laurent + */ +@Testcontainers +class TestControllerIT extends AbstractBaseIT { + + @Container + public static GenericContainer pastryImpl = new GenericContainer("quay.io/microcks/quarkus-api-pastry:latest") + .withExposedPorts(8282); + + @Container + public static GenericContainer helloWorldImpl = new GenericContainer("quay.io/microcks/grpc-hello-world:nightly") + .withExposedPorts(9000); + + @SpyBean + private TestController testController; + + @Test + void testOpenAPITesting() { + // Upload PetStore reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/web/pastry-for-test-openapi.yaml", true); + + String testEndpoint = String.format("http://localhost:%d", pastryImpl.getMappedPort(8282)); + + StringBuilder testRequest = new StringBuilder("{").append("\"serviceId\": \"pastry-for-test:2.0.0\", ") + .append("\"testEndpoint\": \"").append(testEndpoint).append("\", ") + .append("\"runnerType\": \"OPEN_API_SCHEMA\", ").append("\"timeout\": 2000").append("}"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(testRequest.toString(), headers); + + ResponseEntity response = restTemplate.postForEntity("/api/tests", entity, TestResult.class); + assertEquals(201, response.getStatusCode().value()); + + TestResult testResult = response.getBody(); + assertNotNull(testResult); + assertNotNull(testResult.getId()); + assertTrue(testResult.isInProgress()); + assertEquals(testEndpoint, testResult.getTestedEndpoint()); + + // Wait till timeout and re-fetch the result. + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + response = restTemplate.getForEntity("/api/tests/" + testResult.getId(), TestResult.class); + assertEquals(200, response.getStatusCode().value()); + + testResult = response.getBody(); + assertNotNull(testResult); + assertFalse(testResult.isInProgress()); + assertTrue(testResult.isSuccess()); + + // Now try accessing messages for basic operation. + String testCaseId = testResult.getId() + "-" + testResult.getTestNumber() + "-GET%20!pastry"; + + List pairs = testController.getMessagesForTestCase(testResult.getId(), testCaseId); + assertEquals(1, pairs.size()); + assertEquals("pastries_json", pairs.get(0).getRequest().getName()); + } + + @Test + void testSoapUITesting() { + // Upload Hello Service SoapUI project. + uploadArtifactFile("target/test-classes/io/github/microcks/util/soapui/HelloService-soapui-project.xml", true); + + String testEndpoint = getServerUrl() + "/soap/HelloService+Mock/0.9"; + + StringBuilder testRequest = new StringBuilder("{").append("\"serviceId\": \"HelloService Mock:0.9\", ") + .append("\"testEndpoint\": \"").append(testEndpoint).append("\", ").append("\"runnerType\": \"SOAP_UI\", ") + .append("\"timeout\": 2000").append("}"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(testRequest.toString(), headers); + + ResponseEntity response = restTemplate.postForEntity("/api/tests", entity, TestResult.class); + assertEquals(201, response.getStatusCode().value()); + + TestResult testResult = response.getBody(); + assertNotNull(testResult); + assertNotNull(testResult.getId()); + assertTrue(testResult.isInProgress()); + assertEquals(testEndpoint, testResult.getTestedEndpoint()); + + // Wait till timeout and re-fetch the result. + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + response = restTemplate.getForEntity("/api/tests/" + testResult.getId(), TestResult.class); + assertEquals(200, response.getStatusCode().value()); + + testResult = response.getBody(); + assertNotNull(testResult); + assertFalse(testResult.isInProgress()); + // 2 tests steps are ok, 1 is failing. + assertFalse(testResult.isSuccess()); + + assertEquals(1, testResult.getTestCaseResults().size()); + List testStepResults = testResult.getTestCaseResults().getFirst().getTestStepResults(); + for (TestStepResult testStepResult : testStepResults) { + if (testStepResult.getRequestName().equals("Andrew Request")) { + assertFalse(testStepResult.isSuccess()); + assertEquals("Assertion 'XQuery Match' is not managed by Microcks at the moment\n", + testStepResult.getMessage()); + } else if (testStepResult.getRequestName().equals("Karla Request")) { + assertTrue(testStepResult.isSuccess()); + } else if (testStepResult.getRequestName().equals("World Request")) { + assertTrue(testStepResult.isSuccess()); + } else { + fail("Unexpected request name in test step result: " + testStepResult.getRequestName()); + } + } + + // Now try accessing messages for basic operation. + String testCaseId = testResult.getId() + "-" + testResult.getTestNumber() + "-sayHello"; + + List pairs = testController.getMessagesForTestCase(testResult.getId(), testCaseId); + assertEquals(3, pairs.size()); + } + + @Test + void testGRPCTesting() { + // Upload GRPC reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.postman.json", false); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.metadata.yml", false); + + String testEndpoint = String.format("http://localhost:%d", helloWorldImpl.getMappedPort(9000)); + + StringBuilder testRequest = new StringBuilder("{") + .append("\"serviceId\": \"io.github.microcks.grpc.hello.v1.HelloService:v1\", ") + .append("\"testEndpoint\": \"").append(testEndpoint).append("\", ") + .append("\"runnerType\": \"GRPC_PROTOBUF\", ").append("\"timeout\": 2000").append("}"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(testRequest.toString(), headers); + + ResponseEntity response = restTemplate.postForEntity("/api/tests", entity, TestResult.class); + assertEquals(201, response.getStatusCode().value()); + + TestResult testResult = response.getBody(); + assertNotNull(testResult); + assertNotNull(testResult.getId()); + assertTrue(testResult.isInProgress()); + assertEquals(testEndpoint, testResult.getTestedEndpoint()); + + // Wait till timeout and re-fetch the result. + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + response = restTemplate.getForEntity("/api/tests/" + testResult.getId(), TestResult.class); + assertEquals(200, response.getStatusCode().value()); + + testResult = response.getBody(); + assertNotNull(testResult); + assertFalse(testResult.isInProgress()); + assertTrue(testResult.isSuccess()); + + // Now try accessing messages for basic operation. + String testCaseId = testResult.getId() + "-" + testResult.getTestNumber() + "-greeting"; + + List pairs = testController.getMessagesForTestCase(testResult.getId(), testCaseId); + assertEquals(2, pairs.size()); + for (RequestResponsePair pair : pairs) { + assertTrue(pair.getRequest().getName().equals("Laurent") || pair.getRequest().getName().equals("Philippe")); + } + } + + @Test + void testGRPCTestingFailing() { + // Upload GRPC reference artifact. + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/hello-v1.proto", true); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.postman.json", false); + uploadArtifactFile("target/test-classes/io/github/microcks/util/grpc/HelloService.metadata.yml", false); + + String testEndpoint = "http://localhost:50051"; // unreachable address, will lead to UNAVAILABLE + + StringBuilder testRequest = new StringBuilder("{") + .append("\"serviceId\": \"io.github.microcks.grpc.hello.v1.HelloService:v1\", ") + .append("\"testEndpoint\": \"").append(testEndpoint).append("\", ") + .append("\"runnerType\": \"GRPC_PROTOBUF\", ").append("\"timeout\": 2000").append("}"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(testRequest.toString(), headers); + + ResponseEntity response = restTemplate.postForEntity("/api/tests", entity, TestResult.class); + assertEquals(201, response.getStatusCode().value()); + + TestResult testResult = response.getBody(); + assertNotNull(testResult); + assertNotNull(testResult.getId()); + assertTrue(testResult.isInProgress()); + assertEquals(testEndpoint, testResult.getTestedEndpoint()); + + // Wait till timeout and re-fetch the result. + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + response = restTemplate.getForEntity("/api/tests/" + testResult.getId(), TestResult.class); + assertEquals(200, response.getStatusCode().value()); + testResult = response.getBody(); + assertNotNull(testResult); + assertFalse(testResult.isInProgress()); + assertFalse(testResult.isSuccess()); + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/UploadArtifactControllerIT.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/UploadArtifactControllerIT.java new file mode 100644 index 000000000..6571553fc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/UploadArtifactControllerIT.java @@ -0,0 +1,54 @@ +package io.github.microcks.web; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@AutoConfigureMockMvc +public class UploadArtifactControllerIT extends AbstractBaseIT { + + @Autowired + public MockMvc mockMvc; + + + @Test + public void shouldNotCreateWhenUrlIsEmpty() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/api/artifact/download").param("url", "")) + .andExpect(MockMvcResultMatchers.status().is(HttpStatus.NO_CONTENT.value())); + } + + @Test + public void shouldCreateServiceFromOpenAPIImporter() throws Exception { + String apiPastry = "https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml"; + mockMvc.perform(MockMvcRequestBuilders.post("/api/artifact/download").param("url", apiPastry)) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.jsonPath("$.name", Matchers.is("API Pastry - 2.0:2.0.0"))); + } + + @Test + public void shouldCreateServiceFromPostmanCorrectly() throws Exception { + String beerCatalogAPI = "https://raw.githubusercontent.com/microcks/microcks/master/samples/BeerCatalogAPI-collection.json"; + mockMvc.perform(MockMvcRequestBuilders.post("/api/artifact/download").param("url", beerCatalogAPI)) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.jsonPath("$.name", Matchers.is("Beer Catalog API:0.99"))); + } + + @Test + public void shouldNotCreateServiceWhenTheUrlIsWrong() throws Exception { + String wrongUrl = "https://raw.githubusercontent.com/microcks/microcks/master/samples/wrong-collection.json"; + mockMvc.perform(MockMvcRequestBuilders.post("/api/artifact/download").param("url", wrongUrl)) + .andExpect(MockMvcResultMatchers.status().isInternalServerError()).andExpect(MockMvcResultMatchers.content() + .string(Matchers.containsString("Exception while retrieving remote item"))); + } + + @Test + public void shouldNotCreateServiceWithoutUrl() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/api/artifact/download")) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/UploadArtifactControllerTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/UploadArtifactControllerTest.java new file mode 100644 index 000000000..074211508 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/UploadArtifactControllerTest.java @@ -0,0 +1,114 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web; + +import io.github.microcks.repository.SecretRepository; +import io.github.microcks.service.ArtifactInfo; +import io.github.microcks.service.ServiceService; +import io.github.microcks.util.MockRepositoryImportException; +import io.github.microcks.util.ReferenceResolver; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.io.File; +import java.util.Collections; + +@ExtendWith(MockitoExtension.class) +class UploadArtifactControllerTest { + + @Mock + private ServiceService serviceService; + + @Mock + private SecretRepository secretRepository; + + @InjectMocks + private UploadArtifactController sut; + + @Test + void shouldReturnBadRequest() throws MockRepositoryImportException { + // arrange + String apiPastry = "https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml"; + + Mockito.when(serviceService.importServiceDefinition(Mockito.any(File.class), Mockito.any(ReferenceResolver.class), + Mockito.any(ArtifactInfo.class))).thenThrow(new MockRepositoryImportException("Intentional error")); + + // act + ResponseEntity responseEntity = sut.importArtifact(apiPastry, false, null); + + // assert + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + softly.assertThat(responseEntity.getBody()).contains("Intentional error"); + }); + } + + @Test + @DisplayName("Should return 500 when there is an error retrieving remote item") + void shouldReturnInternalServerError() throws MockRepositoryImportException { + // arrange + String wrongUrl = "https://raw.githubusercontent.com/microcks/microcks/master/samples/wrong-openapi.yaml"; + + // act + ResponseEntity responseEntity = sut.importArtifact(wrongUrl, false, null); + + // assert + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + softly.assertThat(responseEntity.getBody()).contains("Exception while retrieving remote item"); + }); + } + + @Test + void shouldReturnNoContentWhenTheServiceHasNotBeenCreated() throws MockRepositoryImportException { + // arrange + Mockito.when(serviceService.importServiceDefinition(Mockito.any(File.class), Mockito.any(ReferenceResolver.class), + Mockito.any(ArtifactInfo.class))).thenReturn(Collections.emptyList()); + + String wrongUrl = "https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml"; + + // act + ResponseEntity responseEntity = sut.importArtifact(wrongUrl, false, null); + + // assert + Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @Test + void shouldReturnNoContentWhenTheServiceHasNotBeenCreatedNullValue() throws MockRepositoryImportException { + // arrange + Mockito.when(serviceService.importServiceDefinition(Mockito.any(File.class), Mockito.any(ReferenceResolver.class), + Mockito.any(ArtifactInfo.class))).thenReturn(null); + + String wrongUrl = "https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml"; + + // act + ResponseEntity responseEntity = sut.importArtifact(wrongUrl, false, null); + + // assert + Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/filter/DynamicCorsFilterTest.java b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/filter/DynamicCorsFilterTest.java new file mode 100644 index 000000000..bc2192f87 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/java/io/github/microcks/web/filter/DynamicCorsFilterTest.java @@ -0,0 +1,125 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.microcks.web.filter; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import graphql.language.Argument; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Vector; + +public class DynamicCorsFilterTest { + + HttpServletRequest request; + HttpServletResponse response; + FilterChain chain; + + @BeforeEach + void setUp() { + // mock HttpServletRequest, HttpServletResponse, and FilterChain + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + chain = mock(FilterChain.class); + } + + @Nested + class WithAllowCredentials { + + private final DynamicCorsFilter filter = new DynamicCorsFilter("http://allowed-origin.com", true); + + @Test + void shouldSetAllowCredentialsHeader() throws IOException, ServletException { + when(request.getHeader("Origin")).thenReturn("http://example.com"); + filter.doFilter(request, response, chain); + verify(response).setHeader("Access-Control-Allow-Credentials", "true"); + } + + @Test + void shouldSetOriginHeader() throws IOException, ServletException { + when(request.getHeader("Origin")).thenReturn("http://example.com"); + filter.doFilter(request, response, chain); + verify(response).setHeader("Access-Control-Allow-Origin", "http://example.com"); + } + + @Test + void shouldSetAccessControlAllowHeaders() throws IOException, ServletException { + Vector headerNames = new Vector<>(); + headerNames.add("Content-Type"); + headerNames.add("Authorization"); + Enumeration headerNamesEnum = headerNames.elements(); + when(request.getHeaderNames()).thenReturn(headerNamesEnum); + + filter.doFilter(request, response, chain); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(response).setHeader(eq("Access-Control-Allow-Headers"), captor.capture()); + String value = captor.getValue(); + assert value.contains("Content-Type"); + assert value.contains("Authorization"); + } + + @Test + void shouldAddRequestHeadersToAccessControlAllowHeaders() throws IOException, ServletException { + Vector headerNames = new Vector<>(); + headerNames.add("Content-Type"); + headerNames.add("Authorization"); + headerNames.add("X-Custom-Header"); + headerNames.add("Access-Control-Request-Headers"); + Enumeration headerNamesEnum = headerNames.elements(); + when(request.getHeaderNames()).thenReturn(headerNamesEnum); + + when(request.getHeader("Access-Control-Request-Headers")).thenReturn("X-Custom-Header-1, X-Custom-Header-2"); + + filter.doFilter(request, response, chain); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + + verify(response).setHeader(eq("Access-Control-Allow-Headers"), captor.capture()); + String value = captor.getValue(); + assert value.contains("Content-Type"); + assert value.contains("Authorization"); + assert value.contains("X-Custom-Header"); + assert value.contains("X-Custom-Header-1"); + assert value.contains("X-Custom-Header-2"); + assert value.contains("Access-Control-Request-Headers"); + } + } + + @Nested + class WithoutAllowCredentials { + + private final DynamicCorsFilter filter = new DynamicCorsFilter("http://allowed-origin.com", false); + + @Test + void shouldNotSetAllowCredentialsHeader() throws IOException, ServletException { + when(request.getHeader("Origin")).thenReturn("http://example.com"); + filter.doFilter(request, response, chain); + verify(response, never()).setHeader(eq("Access-Control-Allow-Credentials"), anyString()); + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/config/fuzz.properties b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/config/fuzz.properties new file mode 100644 index 000000000..7b35515d6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/config/fuzz.properties @@ -0,0 +1,7 @@ +# Keycloak adapter configuration properties +keycloak.enabled=false + +# Async mocking support. +async-api.enabled=false +async-api.default-binding=KAFKA +async-api.default-frequency=10 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/config/test.properties b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/config/test.properties new file mode 100644 index 000000000..7b35515d6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/config/test.properties @@ -0,0 +1,7 @@ +# Keycloak adapter configuration properties +keycloak.enabled=false + +# Async mocking support. +async-api.enabled=false +async-api.default-binding=KAFKA +async-api.default-frequency=10 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/filled-templates/asyncapi-2.4.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/filled-templates/asyncapi-2.4.yaml new file mode 100644 index 000000000..e17bb4740 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/filled-templates/asyncapi-2.4.yaml @@ -0,0 +1,37 @@ +asyncapi: '2.4.0' +id: 'urn:io.microcks.generic.Book Service-1.0.0' +info: + title: Book Service + version: '1.0.0' + description: This is a generic Event Driven API definition that sends Book events. +defaultContentType: application/json +channels: + Book: + bindings: + ws: + method: POST + subscribe: + message: + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + '$ref': '#/components/messages/BookMsg' +components: + messages: + BookMsg: + payload: + '$ref': '#/components/schemas/BookType' + examples: + - name: Reference + payload: {"title":"Example Title","author":"Example Author","isbn":"Example ISBN"} + schemas: + BookType: + type: object + properties: + title: + type: string + author: + type: string + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/filled-templates/openapi-3.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/filled-templates/openapi-3.0.yaml new file mode 100644 index 000000000..198cd2373 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/filled-templates/openapi-3.0.yaml @@ -0,0 +1,81 @@ +--- +openapi: 3.0.0 +info: + title: Book Service + description: This is a generic API definition for manipulation of Book resources. It contains basic CRUD operations for Book resources. + version: 1.0.0 +paths: + /Book: + get: + summary: Retrieve Book resources. + description: Retrieve a bunch of Book resources. Specify example resource as body payload. + operationId: getBookList + responses: + 200: + description: Get an array of Book resources. + post: + summary: Create new Book resource. + description: Create a new Book resource. Specify payload within request body. + operationId: createBook + requestBody: + description: The payload of resource Book to create. + content: + application/json: + schema: + $ref: '#/components/schema/BookType' + responses: + 201: + description: Get the newly created Book resource. + /Book/{id}: + get: + summary: Retrieve a Book resource. + description: Retrieve an already existing Book resource having the specified id. + operationId: getBookById + responses: + 200: + description: Book resource having specified id. + content: + application/json: + schema: + $ref: '#/components/schema/BookType' + put: + summary: Update a Book resource. + description: Update an already existing Book resource having the specified id. + operationId: updateBookById + requestBody: + description: The payload of resource Book to update. + content: + application/json: + schema: + $ref: '#/components/schema/BookType' + responses: + 200: + description: Updated Book resource. + content: + application/json: + schema: + $ref: '#/components/schema/BookType' + delete: + summary: Delete a Book resource. + description: Remove an existing Book resource having the specified id. + operationId: deleteBookById + responses: + 204: + description: Resource Book with specified id has been removed. + parameters: + - name: id + in: path + description: Id of resource + required: true + schema: + type: string +components: + schemas: + BookType: + type: object + properties: + title: + type: string + author: + type: string + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/security/myrealm-realm.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/security/myrealm-realm.json new file mode 100644 index 000000000..bf93b751f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/security/myrealm-realm.json @@ -0,0 +1,168 @@ +{ + "id": "myrealm", + "realm": "myrealm", + "displayName": "My Realm", + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "users" : [ + { + "username" : "admin", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "myrealm123" } + ], + "realmRoles": [], + "applicationRoles": { + "realm-management": [ "manage-users", "manage-clients" ], + "account": [ "manage-account" ], + "myrealm-app": [ "user", "manager", "admin"] + } + } + ], + "roles": { + "realm": [], + "client": { + "myrealm-app": [ + { + "name": "user", + "composite": false, + "clientRole": true, + "containerId": "myrealm" + }, + { + "name": "admin", + "composite": false, + "clientRole": true, + "containerId": "myrealm" + }, + { + "name": "manager", + "composite": false, + "clientRole": true, + "containerId": "myrealm" + } + ] + } + }, + "groups": [ + { + "name": "myrealm", + "path": "/myrealm", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [ + { + "name": "manager", + "path": "/myrealm/manager", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + } + ] + } + ], + "defaultRoles": [], + "requiredCredentials": [ "password" ], + "scopeMappings": [], + "clientScopeMappings": { + "myrealm-app": [ + { + "client": "myrealm-app-js", + "roles": [ + "manager", + "admin", + "user" + ] + } + ], + "realm-management": [ + { + "client": "myrealm-app-js", + "roles": [ + "manage-users", + "manage-clients" + ] + } + ] + }, + "clients": [ + { + "clientId": "myrealm-app-js", + "enabled": true, + "publicClient": true, + "redirectUris": [ + "http://localhost:8080/*" + ], + "webOrigins": [ + "+" + ], + "fullScopeAllowed": false, + "protocolMappers": [ + { + "name": "myrealm-group-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "myrealm-groups", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "clientId": "myrealm-test", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "QAzrPEJJeDkjKtePKoXyzkqY6exkBauh", + "publicClient": false, + "protocol": "openid-connect", + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false + } + ], + "applications": [ + { + "name": "myrealm-app", + "enabled": true, + "bearerOnly": true, + "defaultRoles": [ + "user" + ] + }, + { + "name": "myrealm-serviceaccount", + "secret": "ab54d329-e435-41ae-a900-ec6b3fe15c54", + "enabled": true, + "bearerOnly": false, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "clientAuthenticatorType": "client-secret" + } + ], + "requiredActions": [ + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": false, + "defaultAction": false, + "priority": 90, + "config": {} + } + ], + "keycloakVersion": "10.0.1" +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/custom-service-primary-openapi.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/custom-service-primary-openapi.json new file mode 100644 index 000000000..6bb2a2dcb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/custom-service-primary-openapi.json @@ -0,0 +1,55 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "custom-service", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "paths": { + "/secondary/mediatypetextplain/example02": { + "get": { + "tags": [ + "mediatype-text-plain-resource" + ], + "operationId": "example02_1", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/secondary/mediatypetextplain/example01": { + "get": { + "tags": [ + "mediatype-text-plain-resource" + ], + "operationId": "example01_3", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/custom-service-secondary-soapui.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/custom-service-secondary-soapui.xml new file mode 100644 index 000000000..8f9ea8e42 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/custom-service-secondary-soapui.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + SEQUENTIAL + + + + + UTF-8 + + + + + + + + + + + version + 1.0.0 + + + + OK Response + SEQUENCE + + result01 + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-metadata.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-metadata.yaml new file mode 100644 index 000000000..541a80503 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-metadata.yaml @@ -0,0 +1,19 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIMetadata +metadata: + name: WeatherForecast API + version: 1.1.0 + labels: + domain: weather + status: GA + team: Team C +operations: + 'GET /forecast/{region}': + delay: 100 + dispatcher: FALLBACK + dispatcherRules: |- + { + "dispatcher": "URI_PARTS", + "dispatcherRules": "region", + "fallback": "Unknown" + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-openapi.yaml new file mode 100644 index 000000000..a007eb0dc --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-openapi.yaml @@ -0,0 +1,105 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: '#/components/schemas/Forecast' + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + value: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + value: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." +components: + schemas: + Forecast: + title: Root Type for Forecast + description: A weather forecast for a requested region + type: object + properties: + region: + type: string + temp: + format: double + type: number + weather: + type: string + visibility: + format: int32 + type: integer + example: + region: west + temp: 25.2 + weather: cloudy + visibility: 1000 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-postman.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-postman.json new file mode 100644 index 000000000..a403c2dfd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-postman.json @@ -0,0 +1,208 @@ +{ + "info": { + "_postman_id": "c60c547f-69c3-45e7-b985-3f23439d4edf", + "name": "WeatherForecast API", + "description": "version=1.1.0 - A simple API for demonstrating dispatching capabilities in Microcks", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "forecast", + "item": [ + { + "name": "Get Forecast", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http:///forecast/{region}", + "protocol": "http", + "path": [ + "forecast", + "{region}" + ], + "variable": [ + { + "key": "region", + "value": "" + } + ] + } + }, + "response": [ + { + "name": "Unknown", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http:///forecast/:region", + "protocol": "http", + "path": [ + "forecast", + ":region" + ], + "variable": [ + { + "key": "region", + "value": "other" + } + ] + } + }, + "code": 404, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "Region is unknown. Choose in north, west, east or south." + }, + { + "name": "East", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http:///forecast/:region", + "protocol": "http", + "path": [ + "forecast", + ":region" + ], + "variable": [ + { + "key": "region", + "value": "east" + } + ] + } + }, + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"region\": \"east\",\n \"temp\": -6.6,\n \"weather\": \"frosty\",\n \"visibility\": 523\n}" + }, + { + "name": "North", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http:///forecast/:region", + "protocol": "http", + "path": [ + "forecast", + ":region" + ], + "variable": [ + { + "key": "region", + "value": "north" + } + ] + } + }, + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"region\": \"north\",\n \"temp\": -1.5,\n \"weather\": \"snowy\",\n \"visibility\": 25\n}" + }, + { + "name": "South", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http:///forecast/:region", + "protocol": "http", + "path": [ + "forecast", + ":region" + ], + "variable": [ + { + "key": "region", + "value": "south" + } + ] + } + }, + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"region\": \"south\",\n \"temp\": 28.3,\n \"weather\": \"sunny\",\n \"visibility\": 1500\n}" + }, + { + "name": "West", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http:///forecast/:region", + "protocol": "http", + "path": [ + "forecast", + ":region" + ], + "variable": [ + { + "key": "region", + "value": "west" + } + ] + } + }, + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"region\": \"west\",\n \"temp\": 12.1,\n \"weather\": \"rainy\",\n \"visibility\": 300\n}" + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-raw-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-raw-openapi.yaml new file mode 100644 index 000000000..f6ad10a0f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/service/weather-forecast-raw-openapi.yaml @@ -0,0 +1,66 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.1.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: '#/components/schemas/Forecast' + "404": + description: Region is unknown + content: + application/json: + schema: + type: string +components: + schemas: + Forecast: + title: Root Type for Forecast + description: A weather forecast for a requested region + type: object + properties: + region: + type: string + temp: + format: double + type: number + weather: + type: string + visibility: + format: int32 + type: integer + example: + region: west + temp: 25.2 + weather: cloudy + visibility: 1000 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/openwealth-custodyServicesAPI.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/openwealth-custodyServicesAPI.yaml new file mode 100644 index 000000000..a304050f1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/openwealth-custodyServicesAPI.yaml @@ -0,0 +1,1871 @@ +openapi: 3.0.0 +info: + version: 3.1.0 + title: Custody Services + description: | + This API is part of the OpenWealth APIs for the connectivity between custody banks and WealthTechs (e.g. Portfolio Management Systems). + This API allows the user to receive data from custody banks regarding accounts and positions. The API is designed to be used for either + update end of day data batches or single near-realtime account/position information. Furthermore, this API allows the user to receive + data from custody banks regarding transactions. + contact: + email: openwealth@synpulse.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + +servers: + - description: Your Server URL + url: https://api.openwealth.ch + +externalDocs: + description: Find out more about OpenWealth API specifications. + url: https://openwealth.ch + +tags: + - name: accounts + description: Accounts operations. + - name: customers + description: Customer operations. + - name: positions + description: Position operations. + - name: transactions + description: Transaction operations. + +security: [] + +paths: + /customers: + get: + tags: + - customers + summary: Returns all customers (business partners) accessible for the logged in user + description: | + This endpoint returns a highlevel list of customers accessible for the logged in user. Paging is done based on the customer object, + i.e. if limit is set to 1, then 1 customer will be returned per page. + operationId: getCustomers + parameters: + - $ref: '#/components/parameters/cursor' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Returns a list with all customers. + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + nextCursor: + $ref: '#/components/headers/X-Next-Cursor' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Customer' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /customers/{customerId}: + get: + tags: + - customers + summary: Returns a specific customer accessible for the logged in user + description: This endpoint returns a single customer. + operationId: getCustomersByCustomerId + parameters: + - $ref: '#/components/parameters/path_customerId' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Returns a the specified customer. + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /customers/{customerId}/accounts: + get: + tags: + - accounts + summary: Returns the accounts for a specific customer accessible to the querying user + description: | + Returns all accounts for a specific customer. Paging is done based on the account object, i.e. if limit is set to 1, + then 1 account will be returned per page. + operationId: getCustomerAccountsByCustomerId + parameters: + - $ref: '#/components/parameters/path_customerId' + - $ref: '#/components/parameters/cursor' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Account List + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + nextCursor: + $ref: '#/components/headers/X-Next-Cursor' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Account' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /customers/{customerId}/accounts/{accountId}: + get: + tags: + - accounts + summary: Returns an account by id for a specific customer + description: Returns an account by id for a specific customer. + operationId: getCustomerAccountById + parameters: + - $ref: '#/components/parameters/path_customerId' + - $ref: '#/components/parameters/path_accountId' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Account + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + nextCursor: + $ref: '#/components/headers/X-Next-Cursor' + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /customers/{customerId}/positions: + get: + tags: + - positions + summary: Returns the positions for a specific customer + description: | + Returns all positions for a specific customer. Paging is done based on the position object, i.e. if limit is set to 1, + then 1 position will be returned per page. + operationId: getCustomerPositionByCustomerId + parameters: + - $ref: '#/components/parameters/path_customerId' + - $ref: '#/components/parameters/date' + - $ref: '#/components/parameters/end_of_day_indicator' + - $ref: '#/components/parameters/cursor' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Position List + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + nextCursor: + $ref: '#/components/headers/X-Next-Cursor' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ValuatedPosition' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /customers/{customerId}/positions/{positionId}: + get: + tags: + - positions + summary: Returns a positions by id for a specific customer + description: Returns a positions for a specific customer. + operationId: getCustomerPositionById + parameters: + - $ref: '#/components/parameters/path_customerId' + - $ref: '#/components/parameters/path_positionId' + - $ref: '#/components/parameters/date' + - $ref: '#/components/parameters/end_of_day_indicator' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Position + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + content: + application/json: + schema: + $ref: '#/components/schemas/ValuatedPosition' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /accounts/{accountId}/positions: + get: + tags: + - positions + summary: Returns the positions for a specific account + description: | + Returns all positions for a specific account. Paging is done based on the position object, i.e. if limit is set to 1, + then 1 position will be returned per page. + operationId: getAccountPositionByAccountId + parameters: + - $ref: '#/components/parameters/path_accountId' + - $ref: '#/components/parameters/date' + - $ref: '#/components/parameters/end_of_day_indicator' + - $ref: '#/components/parameters/cursor' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Position List + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + nextCursor: + $ref: '#/components/headers/X-Next-Cursor' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ValuatedPosition' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /accounts/{accountId}/positions/{positionId}: + get: + tags: + - positions + summary: Returns a positions by id for a specific account + description: Returns a positions for a specific account. + operationId: getAccountPositionById + parameters: + - $ref: '#/components/parameters/path_accountId' + - $ref: '#/components/parameters/path_positionId' + - $ref: '#/components/parameters/date' + - $ref: '#/components/parameters/end_of_day_indicator' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Position + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + content: + application/json: + schema: + $ref: '#/components/schemas/ValuatedPosition' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /customers/{customerId}/transactions: + get: + tags: + - transactions + summary: Returns a transactions for a specific customer + description: | + Returns all transactions for the a specific customer. Paging is done based on the transaction object, i.e. if limit is set to 1, + then 1 transaction will be returned per page. + operationId: getTransactionByCustomerId + parameters: + - $ref: '#/components/parameters/path_customerId' + - $ref: '#/components/parameters/date' + - $ref: '#/components/parameters/end_of_day_indicator' + - $ref: '#/components/parameters/cursor' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Returns a transaction list for a specific customer. + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + nextCursor: + $ref: '#/components/headers/X-Next-Cursor' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Transaction' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /customers/{customerId}/transactions/{transactionId}: + get: + tags: + - transactions + summary: Returns a transaction for a specific customer + description: Returns a transaction instance by id. + operationId: getTransactionByTransactionId + parameters: + - $ref: '#/components/parameters/path_customerId' + - $ref: '#/components/parameters/path_transactionId' + - $ref: '#/components/parameters/x_correlation_id_in_header' + responses: + '200': + description: Returns a transaction list for a specific customer. + headers: + X-Correlation-ID: + $ref: '#/components/headers/X-Correlation-Id' + content: + application/json: + schema: + $ref: '#/components/schemas/Transaction' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '500': + $ref: '#/components/responses/500' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' +components: + schemas: + Currency: + title: Currency + type: string + description: ISO 4217 code. + pattern: '^[A-Z]{3}$' + example: CHF + Customer: + title: Customer + type: object + description: Overview of the customer with the respective accounts. + required: + - id + properties: + id: + type: string + maxLength: 128 + description: Unique and unambiguous identification (i.e. UUID) used by the bank for the customer. + example: MTIzNDUtNg + number: + type: string + description: Contains the customers custody proprietary customer number if available. + maxLength: 70 + example: 12345-6 + referenceCurrency: + $ref: '#/components/schemas/Currency' + CommonErrorType: + title: Common Error Type + description: Error Types for CommonErrorResponse. + type: string + enum: + - /problems/INVALID_PAYLOAD + - /problems/MALFORMED_PAYLOAD + - /problems/INVALID_TOKEN + - /problems/EXPIRED_TOKEN + - /problems/INSUFFICIENT_PRIVILEGES + - /problems/NO_ACCESS_TO_RESOURCE + - /problems/RESOURCE_DOES_NOT_EXIST + - /problems/RESOURCE_NOT_READY + - /problems/RESOURCE_TOO_LARGE + - /problems/WRONG_METHOD + - /problems/OPERATION_NOT_ALLOWED + - /problems/TECHNICAL_ERROR + - /problems/NOT_IMPLEMENTED + - /problems/SERVICE_UNAVAILABLE + example: /problems/TECHNICAL_ERROR + CommonErrorResponse: + title: CommonErrorResponse + type: object + description: Common error response. + properties: + type: + $ref: '#/components/schemas/CommonErrorType' + title: + type: string + description: Title of the error response. + example: This is the general problem description + detail: + type: string + description: Detailed description of the error response. + example: Detailed problem description with respect to the current request + instance: + type: string + description: Entity instance that threw the error. + example: path/to/corresponding/resource + PortfolioInformation: + title: PortfolioInformation + type: object + description: Information about the portfolio. + properties: + identification: + title: PortfolioIdentification + type: string + minLength: 1 + maxLength: 128 + example: 87654-3219 + description: Unique and unambiguous identification for the portfolio between the portfolio owner and the portfolio servicer. + referenceCurrency: + $ref: '#/components/schemas/Currency' + Account: + title: Account + type: object + description: Account entity. + required: + - id + - type + - referenceCurrency + properties: + id: + type: string + maxLength: 128 + example: ODc2NTQzMi0xOQ + description: Unique and unambiguous identification for the account. The IBAN should NOT be the account identifier. + type: + type: string + description: | + Indicates the type of the account. If the account type is cashAccount, no information on the financial instrument is + provided in the respective position in the account. + example: cashAccount + enum: + - cashAccount + - safekeepingAccount + - other + name: + type: string + maxLength: 70 + description: | + Name of the account. It provides an additional means of identification, and is designated by the account servicer in + agreement with the account owner. + example: Household account + referenceCurrency: + $ref: '#/components/schemas/Currency' + iban: + type: string + description: Contains the accounts International Banking Account Number (IBAN) for an account if available. + maxLength: 34 + example: CH123456789 + number: + type: string + description: Contains the accounts custody proprietary account number for an account if available. + maxLength: 70 + example: 123-1234-234 + designation: + type: string + maxLength: 70 + description: Supplementary information on the account. Designated by the account servicer. + example: Current Account CHF + portfolioInformation: + $ref: '#/components/schemas/PortfolioInformation' + Cash: + title: Cash + description: Cash. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + Bond: + title: Bond + description: Bond. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + properties: + interestRate: + $ref: '#/components/schemas/InterestRate' + maturityDate: + description: | + Planned date, at the time of issuance, on which an interest bearing financial instrument becomes due and principal + is repaid by the issuer to the investor. + $ref: '#/components/schemas/Date' + issueDate: + description: Date at which the security was made available. + $ref: '#/components/schemas/Date' + conversionPrice: + description: Price of one target security in the conversion. + $ref: '#/components/schemas/Price' + currencyOfDenomination: + $ref: '#/components/schemas/Currency' + minimumDenomination: + type: number + example: 10000 + description: Indicates the minimum denomination of a security. + minimumIncrement: + type: number + example: 1000 + description: Indicates the minimum tradable increments of a security. + underlyingFinancialInstrument: + description: Financial instrument that would be subject to a conversion. + $ref: '#/components/schemas/FinancialInstrument' + Equity: + title: Equity + description: | + Equity, typically referred to as shareholders' equity (or owners' equity for privately held companies), represents the amount + of money that would be returned to a company's shareholders if all of the assets were liquidated and all of the company's debt + was paid off in the case of liquidation. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + Fund: + title: Fund + description: | + An investment fund is a supply of capital belonging to numerous investors used to collectively purchase securities while each + investor retains ownership and control of his own shares. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + Index: + title: Index + description: A financial index produces a numeric score based on inputs such as a variety of asset prices. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + Commodity: + title: Commodity + description: Commodity. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + Option: + title: Option + description: | + An option is a financial instrument that gives the holder the right, but not the obligation, to buy or sell an underlying + asset at a predetermined price within a given time frame. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - underlyingFinancialInstrument + properties: + expiryDate: + description: | + Date on which an order, a privilege, an entitlement or an offer terminates. For an interest bearing asset, it is the + date at which it becomes due and has to be repaid. + $ref: '#/components/schemas/Date' + exercisePrice: + description: Predetermined price at which the holder of a derivative will buy or sell the underlying instrument. + $ref: '#/components/schemas/Price' + contractSize: + $ref: '#/components/schemas/ContractSize' + optionType: + $ref: '#/components/schemas/OptionType' + optionStyle: + $ref: '#/components/schemas/OptionStyle' + underlyingFinancialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + Future: + title: Future + description: Futures are derivative financial contracts that obligate parties to buy or sell an asset at a predetermined future date and price. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - underlyingFinancialInstrument + properties: + expiryDate: + description: | + Date on which an order, a privilege, an entitlement or an offer terminates. For an interest bearing asset, it is the + date at which it becomes due and has to be repaid. + $ref: '#/components/schemas/Date' + contractSize: + $ref: '#/components/schemas/ContractSize' + underlyingFinancialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + exercisePrice: + description: Predetermined price at which the holder of a derivative will buy or sell the underlying instrument. + $ref: '#/components/schemas/Price' + FxForward: + title: FxForward + description: A forward contract is a customized contract between two parties to buy or sell currencies at a specified price on a future date. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - amountPaid + - amountReceived + - maturityDate + properties: + amountPaid: + description: Amount paid at the maturity of a contract (i.e. FX forward). + $ref: '#/components/schemas/CurrencyAmount' + amountReceived: + description: Amount received at the maturity of a contract (i.e. FX forward). + $ref: '#/components/schemas/CurrencyAmount' + maturityDate: + description: | + Planned date, at the time of issuance, on which an interest bearing financial instrument becomes due and principal + is repaid by the issuer to the investor. + $ref: '#/components/schemas/Date' + FxSwap: + title: FxSwap + description: | + A foreign currency swap is an agreement between two foreign parties to swap interest payments on a loan made in one currency + for interest payments on a loan made in another currency. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - nearAmountPaid + - nearAmountReceived + - farAmountPaid + - farAmountReceived + - nearMaturityDate + - maturityDate + properties: + nearAmountPaid: + description: Amount paid at the earlier part of an FX swap. + $ref: '#/components/schemas/CurrencyAmount' + nearAmountReceived: + description: Amount received at the earlier part of an FX swap. + $ref: '#/components/schemas/CurrencyAmount' + farAmountPaid: + description: Amount paid at the maturity of an FX swap. + $ref: '#/components/schemas/CurrencyAmount' + farAmountReceived: + description: Amount received at the maturity of an FX swap. + $ref: '#/components/schemas/CurrencyAmount' + nearMaturityDate: + description: Date of the settlement of the near leg. + $ref: '#/components/schemas/Date' + maturityDate: + description: | + Planned date, at the time of issuance, on which an interest bearing financial instrument becomes due and principal + is repaid by the issuer to the investor. + $ref: '#/components/schemas/Date' + FxOption: + title: FxOption + description: | + An FX option is a contract that gives the buyer the right, but not the obligation, to buy or sell a certain currency at a + specified exchange rate on or before a specified date. For this right, a premium is paid to the seller. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - expiryDateTime + - underlyingAmount + - counterAmount + properties: + expiryDateTime: + description: | + Date on which an order, a privilege, an entitlement or an offer terminates. For an interest bearing asset, it is the + date/time at which it becomes due and has to be repaid. + $ref: '#/components/schemas/DateTime' + underlyingAmount: + description: | + This field specifies the underlying currency and amount of the contract. The underlying currency is the currency which + is to be exchanged for the counter currency. + $ref: '#/components/schemas/CurrencyAmount' + counterAmount: + description: | + This field specifies the counter currency and amount of the contract. The counter currency is the currency + which is to be exchanged for the underlying currency. + $ref: '#/components/schemas/CurrencyAmount' + premium: + $ref: '#/components/schemas/CurrencyAmount' + optionType: + $ref: '#/components/schemas/OptionType' + optionStyle: + $ref: '#/components/schemas/OptionStyle' + Mortgage: + title: Mortgage + description: A mortgage is a type of loan used to purchase or maintain a home, plot of land, or other types of real estate. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - principalAmount + properties: + principalAmount: + description: The current amount subject to loan on which interest is calculated. + $ref: '#/components/schemas/CurrencyAmount' + interestRate: + $ref: '#/components/schemas/InterestRate' + limit: + type: number + example: 100000 + description: Maximum principal amount allowed by the credit contract. + Credit: + title: Credit + description: Credit. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - principalAmount + properties: + principalAmount: + description: The current amount subject to loan or borrowing on which interest is calculated. + $ref: '#/components/schemas/CurrencyAmount' + interestRate: + $ref: '#/components/schemas/InterestRate' + limit: + type: number + example: 100000 + description: Maximum principal amount allowed by the credit contract. + underlyingFinancialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + MoneyMarket: + title: MoneyMarket + description: The money market schema covers both fixed and callable loans and deposits. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - principalAmount + properties: + principalAmount: + description: The current amount subject to loan or borrowing on which interest is calculated. + $ref: '#/components/schemas/CurrencyAmount' + interestRate: + $ref: '#/components/schemas/InterestRate' + maturityDate: + description: | + Planned date, at the time of issuance, on which an interest bearing financial instrument becomes due and principal + is repaid by the issuer to the investor. + $ref: '#/components/schemas/Date' + InterestRateSwap: + title: InterestRateSwap + description: | + An interest rate swap is a forward contract in which one stream of future interest payments is exchanged for another based + on a specified principal amount. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - notionalAmount + - interestRatePaid + - interestRateReceived + properties: + notionalAmount: + description: The notional amount serves as the foundation for calculating interest and the returns on an investment. + $ref: '#/components/schemas/CurrencyAmount' + interestRatePaid: + description: Per annum ratio of interest paid to the principal amount of the financial instrument for a specific period of time. + $ref: '#/components/schemas/InterestRate' + interestRateReceived: + description: Per annum ratio of interest received to the principal amount of the financial instrument for a specific period of time. + $ref: '#/components/schemas/InterestRate' + maturityDate: + description: | + Planned date, at the time of issuance, on which an interest bearing financial instrument becomes due and principal + is repaid by the issuer to the investor. + $ref: '#/components/schemas/Date' + TotalReturnSwap: + title: TotalReturnSwap + description: | + A swap agreement in which Party A pays fees to Party B in exchange for the income or return generated by an asset owned by + Party B. If the private client is party A and the bank party B the property 'interestRatePaid' should be used to describe + the interest, for opposite parties 'interestRateReceived'. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - notionalAmount + - underlyingFinancialInstrument + properties: + notionalAmount: + description: The notional amount serves as the foundation for calculating interest and the returns on an investment. + $ref: '#/components/schemas/CurrencyAmount' + interestRatePaid: + description: Per annum ratio of interest paid to the principal amount of the financial instrument for a specific period of time. + $ref: '#/components/schemas/InterestRate' + interestRateReceived: + description: Per annum ratio of interest received to the principal amount of the financial instrument for a specific period of time. + $ref: '#/components/schemas/InterestRate' + maturityDate: + description: | + Planned date, at the time of issuance, on which an interest bearing financial instrument becomes due and principal + is repaid by the issuer to the investor. + $ref: '#/components/schemas/Date' + underlyingFinancialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + CreditDefaultSwap: + title: CreditDefaultSwap + description: A derivative product which serves as a form of insurance against the default of an underlying borrower or debt instrument. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + required: + - notionalAmount + properties: + notionalAmount: + description: The notional amount is the amount the risk 'buyer' agrees to pay the counter party in case of a default. + $ref: '#/components/schemas/CurrencyAmount' + maturityDate: + description: | + Planned date, at the time of issuance, on which an interest bearing financial instrument becomes due and principal + is repaid by the issuer to the investor. + $ref: '#/components/schemas/Date' + underlyingFinancialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + CryptoAsset: + title: CryptoAsset + description: Crypto Asset. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + OtherFinancialInstrument: + title: Other + type: object + description: Other financial instrument. + allOf: + - $ref: '#/components/schemas/FinancialInstrumentBase' + - type: object + properties: + underlyingFinancialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + FinancialInstrumentType: + title: FinancialInstrumentType + type: string + description: Type of the financial instrument. + example: equity + enum: + - cash + - bond + - equity + - fund + - index + - commodity + - option + - future + - fxForward + - fxSwap + - fxOption + - mortgage + - credit + - fixedLoan + - fixedDeposit + - callableLoan + - callableDeposit + - interestRateSwap + - totalReturnSwap + - creditDefaultSwap + - cryptoAsset + - other + FinancialInstrumentIdentification: + title: FinancialInstrumentIdentification + type: object + description: Financial instrument identification in the form of a key value pair. + required: + - identifier + - type + properties: + identifier: + type: string + maxLength: 128 + example: CH0012005267 + description: Instrument identification. + type: + type: string + example: isin + description: Type of the instrument ID. isin is preferred. + enum: + - isin + - sedol + - cusip + - ric + - tickerSymbol + - bloomberg + - cta + - quick + - wertpapier + - dutch + - valoren + - sicovam + - belgian + - common + - iso3 + - otherProprietaryIdentification + CfiCode: + title: CFICode + type: string + description: | + Indicates the type of the financial instrument. Must follow the ISO 10962, which is also known as CFI (classification of + financial instruments). At least indicate the CFI Category (1st character) and the CFI Group (2nd character). The CFI + attributes 1-4 (3rd to 6th character in the string) are optional. + pattern: '^[A-Z]{2,6}$' + minLength: 2 + maxLength: 6 + example: ESVUFR + FinancialInstrumentBase: + title: FinancialInstrumentBase + description: Base schema for a financial instrument. + type: object + required: + - type + - name + properties: + type: + $ref: '#/components/schemas/FinancialInstrumentType' + name: + type: string + maxLength: 170 + example: Novartis AG + description: Name of the financial instrument in free format text. + identificationList: + type: array + description: List of identification objects. + items: + $ref: '#/components/schemas/FinancialInstrumentIdentification' + minItems: 1 + cfiCode: + $ref: '#/components/schemas/CfiCode' + currencyOfDenomination: + $ref: '#/components/schemas/Currency' + hasFactor: + type: boolean + description: | + Indicates if there is a factor present for this financial instrument. If this indicator is set to TRUE, but the factor + attribute is not present, might indicate, that the factor cannot be delivered or is currently not available. + example: true + factor: + type: number + example: 0.85 + description: Information regarding the factor. + additionalDetails: + type: string + maxLength: 70 + description: Provides additional information about the financial instrument in narrative form. + example: Group contract number 129959961 + DayCountBasis: + title: DayCountBasis + type: string + example: u30_360 + description: Interest method of the instrument. + enum: + - act_360 + - act_365 + - act_actIcma + - act_actIsda + - act_actAfb + - act_365L + - bus_252 + - u30_360 + - u30E_360Icma + - u30E_360Isda + - u30E_360 + - u30U_360 + Date: + title: Date + type: string + description: Date according to ISO 8601. + format: date + example: '2018-04-13' + InterestRate: + title: InterestRate + type: object + description: Per annum ratio of interest received or paid to the principal amount of the contract for a specific period of time. + required: + - type + properties: + type: + type: string + description: | + Indicates the type of interest, where + - 'fixed' denotes a fixed interest rate for the agreed period. + - 'variable' denotes a rate that fluctuates over time because it is based on an underlying benchmark ('basis') interest rate + that changes periodically with the market. + - 'staggered' denotes a rate that is set at different levels for different periods of time or different underlying principal amount. + example: fixed + enum: + - fixed + - variable + - staggered + value: + type: number + example: 0.00125 + description: Current rate as decimal. + dayCountBasis: + $ref: '#/components/schemas/DayCountBasis' + paymentDate: + description: Date of the next interest payment. + $ref: '#/components/schemas/Date' + paymentFrequency: + type: string + description: Specifies the frequency of an interest payment. + example: quarterly + enum: + - annual + - monthly + - quarterly + - semiAnnual + - weekly + - atMaturity + - other + basis: + type: string + description: Benchmark by which floating rate will adjust in accordance with market conditions, such as LIBOR, EURIBOR. + example: LIBOR + spread: + type: number + description: The floating rate will be equal to the base rate (basis) plus the spread. + example: 0.001 + Price: + type: object + description: Price entity. + required: + - type + - value + properties: + type: + type: string + description: Indicates whether the price amount is depicted as actual currency amount per unit or as percentage. + example: percentage + enum: + - actual + - percentage + value: + type: number + example: 12000 + description: Signed decimal number. + currency: + $ref: '#/components/schemas/Currency' + FinancialInstrument: + title: FinancialInstrument + type: object + description: Financial instrument entity. + oneOf: + - $ref: '#/components/schemas/Cash' + - $ref: '#/components/schemas/Bond' + - $ref: '#/components/schemas/Equity' + - $ref: '#/components/schemas/Fund' + - $ref: '#/components/schemas/Index' + - $ref: '#/components/schemas/Commodity' + - $ref: '#/components/schemas/Option' + - $ref: '#/components/schemas/Future' + - $ref: '#/components/schemas/FxForward' + - $ref: '#/components/schemas/FxSwap' + - $ref: '#/components/schemas/FxOption' + - $ref: '#/components/schemas/Mortgage' + - $ref: '#/components/schemas/Credit' + - $ref: '#/components/schemas/MoneyMarket' + - $ref: '#/components/schemas/InterestRateSwap' + - $ref: '#/components/schemas/TotalReturnSwap' + - $ref: '#/components/schemas/CreditDefaultSwap' + - $ref: '#/components/schemas/CryptoAsset' + - $ref: '#/components/schemas/OtherFinancialInstrument' + discriminator: + propertyName: type + mapping: + cash: '#/components/schemas/Cash' + bond: '#/components/schemas/Bond' + equity: '#/components/schemas/Equity' + fund: '#/components/schemas/Fund' + index: '#/components/schemas/Index' + commodity: '#/components/schemas/Commodity' + option: '#/components/schemas/Option' + future: '#/components/schemas/Future' + fxForward: '#/components/schemas/FxForward' + fxSwap: '#/components/schemas/FxSwap' + fxOption: '#/components/schemas/FxOption' + mortgage: '#/components/schemas/Mortgage' + credit: '#/components/schemas/Credit' + fixedLoan: '#/components/schemas/MoneyMarket' + fixedDeposit: '#/components/schemas/MoneyMarket' + callableLoan: '#/components/schemas/MoneyMarket' + callableDeposit: '#/components/schemas/MoneyMarket' + interestRateSwap: '#/components/schemas/InterestRateSwap' + totalReturnSwap: '#/components/schemas/TotalReturnSwap' + creditDefaultSwap: '#/components/schemas/CreditDefaultSwap' + cryptoAsset: '#/components/schemas/CryptoAsset' + other: '#/components/schemas/OtherFinancialInstrument' + ContractSize: + title: ContractSize + type: number + description: Contract size of the instrument. + example: 100 + default: 1 + OptionType: + title: OptionType + type: string + description: | + Specifies whether it is a Call option (right to purchase a specific underlying asset) + or a Put option (right to sell a specific underlying asset). + example: call + enum: + - call + - put + OptionStyle: + title: OptionStyle + type: string + description: Specifies how an option can be exercised. + example: american + enum: + - american + - european + - bermudan + - asian + CurrencyAmount: + title: CurrencyAmount + type: object + description: Amount denoted in a given currency. + required: + - amount + - currency + properties: + amount: + type: number + example: 12000 + description: Signed amount. + currency: + $ref: '#/components/schemas/Currency' + DateTime: + title: DateTime + type: string + description: DateTime according to ISO 8601. + format: date-time + example: '2018-04-13T16:00:00Z' + Position: + title: Position + type: object + description: Position entity. + required: + - id + - financialInstrument + - account + properties: + id: + type: string + maxLength: 128 + example: ODc2LTU0MzIxOQ + description: Identification for the position given by the bank. + financialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + account: + $ref: '#/components/schemas/Account' + name: + type: string + maxLength: 80 + example: Stark Industries Inc. + description: Name of the position. + currency: + $ref: '#/components/schemas/Currency' + safekeepingPlace: + type: string + pattern: '^[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}$' + example: INSECHZZXXX + description: BIC of the place where the securities are safe-kept, physically or notionally. + additionalCustodianInformation: + type: string + pattern: '^[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}$' + example: INSECHZZXXX + description: | + Used for special use cases where safekeepingPlace is not sufficient. BIC of the place where the securities are safe-kept, + physically or notionally. + additionalDetails: + type: string + maxLength: 70 + description: Provides additional information on the position. + example: Belongs to contract 129959959 + Quantity: + title: Quantity + type: object + description: Quantity entity. + required: + - type + - value + properties: + type: + type: string + description: Specifies the type of the amount. + example: unit + enum: + - unit + - faceAmount + - amortisedValue + - digitalTokenUnit + value: + type: number + example: 12000 + description: Signed decimal number. + Valuation: + title: Valuation + type: object + description: Detailed information about the valuation of a position. + required: + - valueInPositionCurrency + properties: + valueInPositionCurrency: + $ref: '#/components/schemas/CurrencyAmount' + valueInReferenceCurrency: + $ref: '#/components/schemas/CurrencyAmount' + valuationDate: + description: Date when the position was valuated. + $ref: '#/components/schemas/Date' + ValuationPrice: + title: ValuationPrice + description: Price used for valuation purpose. + allOf: + - $ref: '#/components/schemas/Price' + - type: object + properties: + priceDate: + description: Date of the price. + allOf: + - $ref: '#/components/schemas/Date' + sourceOfPrice: + type: string + maxLength: 70 + description: Indicates the source of the (market)price. + example: SIX Swiss Exchange + ForeignExchangeRate: + title: ForeignExchangeRate + type: object + description: An exchange rate is a rate at which one currency will be exchanged for another currency. + required: + - value + - sourceCurrency + - targetCurrency + properties: + value: + type: number + description: Current rate as decimal. + example: 0.98 + sourceCurrency: + $ref: '#/components/schemas/Currency' + targetCurrency: + $ref: '#/components/schemas/Currency' + ValuationForeignExchangeRate: + title: ValuationForeignExchangeRate + description: Foreign exchange rate of given by a defined source and used for valuation. + allOf: + - $ref: '#/components/schemas/ForeignExchangeRate' + - type: object + properties: + rateDate: + description: Date of rate. + allOf: + - $ref: '#/components/schemas/Date' + sourceOfRate: + type: string + maxLength: 70 + description: Indicates the source of the (market) price. + example: Reuters 4 o'clock + ValuatedPosition: + title: ValuatedPosition + type: object + description: Valuated position entity. + required: + - endOfDayIndicator + - positionDate + - quantity + - valuation + allOf: + - $ref: '#/components/schemas/Position' + properties: + endOfDayIndicator: + description: Indicates if the position has been confirmed by the end-of-day (eod) processing. + type: boolean + example: true + positionDate: + description: Date when the valuated position entity is exposed in the API (corresponding to the dateParam). + $ref: '#/components/schemas/Date' + quantity: + $ref: '#/components/schemas/Quantity' + valuation: + $ref: '#/components/schemas/Valuation' + price: + $ref: '#/components/schemas/ValuationPrice' + foreignExchangeRate: + $ref: '#/components/schemas/ValuationForeignExchangeRate' + costPrice: + $ref: '#/components/schemas/Price' + costForeignExchangeRate: + $ref: '#/components/schemas/ForeignExchangeRate' + accruedInterest: + $ref: '#/components/schemas/CurrencyAmount' + numberOfDaysAccrued: + type: integer + example: 34 + description: Number of days used for calculating the accrued interest amount. + blockedQuantity: + description: | + Indicates the amount of the position which is blocked, i.e. not freely available, e.g. for trading. + The amount is of the same amountType as the position itself. + $ref: '#/components/schemas/Quantity' + TransactionType: + title: TransactionType + type: string + description: Type of the transaction. + example: buy + enum: + - accumulation + - additionalPayment + - adjustNotional + - amortizationAndInterestPayment + - assignment + - assimilation + - bonus + - buy + - buyToClose + - capitalIncrease + - closeContract + - conversionBondShare + - coupon + - creditEvent + - decreasePrincipal + - deliveryFreeOfPayment + - deliveryVsPayment + - dividendCash + - dividendChoice + - dividendReinvestment + - dividendStock + - exercise + - expiration + - fees + - finalLiquidationPayment + - fxSpot + - increasePrincipal + - inflowCash + - instrumentExchange + - interestPayment + - internalTransfer + - liquidationPayment + - merger + - openContract + - other + - outflowCash + - premium + - prepaymentSubstitution + - receiveFreeOfPayment + - receiveVsPayment + - redemption + - redemptionPartial + - redemptionPrior + - reductionOfNominal + - resetPayment + - rightDistribution + - sell + - sellToOpen + - spinOff + - stockSplit + - subscription + - taxCorrections + - taxes + - transferMetalPhysical + - unwind + - variationMargin + PlaceOfTrade: + title: PlaceOfTrade + type: object + description: Market in which a trade transaction is to be or has been executed. + properties: + marketIdentificationCode: + type: string + maxLength: 4 + description: | + Market Identifier Code. Identification of a financial market, as stipulated in the norm ISOMarket Identifier Code. + Identification of a financial market, as stipulated in the norm ISO 10383 "Codes for exchanges and market identifications". + example: XSWX + marketDescription: + type: string + maxLength: 70 + description: Description of the market when no Market Identification Code is available. + example: OTC + MovementType: + title: MovementType + type: string + description: Describes which kind of movement is reported from a banks perspective. + example: cash + enum: + - accruedInterest + - additionalWithholdingTax + - asset + - brokerageFee + - capitalGainTax + - cash + - commission + - custodyFee + - exchangeFee + - financialTransactionTax + - interest + - managementFee + - otherFee + - other + - otherTax + - premium + - reclaimableTax + - reinvestmentAmount + - stampDuty + - thirdPartyFee + - transactionFee + - valueAddedTax + - withholdingTax + Movement: + title: Movement + type: object + description: Describes a single movement/booking/flow within a transaction. Every transaction contains in the minimum one movement. + required: + - type + - movementDate + - financialInstrument + - account + - quantity + - positionId + properties: + type: + $ref: '#/components/schemas/MovementType' + movementDate: + description: | + Date when the movement was confirmed in the custodians ledger. + This typically corresponds with the booking date (or posting date) by the bank. + Note that balances of end of day positions should be consistent with the aggregation of movements up to the particular date. + $ref: '#/components/schemas/Date' + financialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + account: + $ref: '#/components/schemas/Account' + quantity: + $ref: '#/components/schemas/Quantity' + positionId: + type: string + maxLength: 128 + description: Identification for the position given by the bank. + example: 1234566-12-1 + positionCurrency: + $ref: '#/components/schemas/Currency' + valueDate: + description: Date when calculating economic benefit for a cash amount. + $ref: '#/components/schemas/Date' + price: + $ref: '#/components/schemas/Price' + foreignExchangeRate: + $ref: '#/components/schemas/ForeignExchangeRate' + movementTypeAdditionalInformation: + type: string + maxLength: 35 + example: Fees for Reporting Service + description: Provides further details on an informative level, which goes beyond the granularity of the movementType. + PostingAmount: + title: PostingAmount + type: object + description: Total amount of money that is to be/was posted to the account in the account currency. + required: + - amount + - currency + - account + properties: + amount: + type: number + description: Signed amount of the cash transaction. + example: 13023 + currency: + $ref: '#/components/schemas/Currency' + account: + $ref: '#/components/schemas/Account' + Transaction: + title: Transaction + type: object + description: Transaction entity. + required: + - id + - type + - transactionDate + - customerId + - reversalIndicator + - endOfDayIndicator + properties: + id: + type: string + readOnly: true + maxLength: 128 + example: OTg3Ni01NDMyMQ + description: Transaction ID given by the bank. + type: + $ref: '#/components/schemas/TransactionType' + transactionDate: + description: Date when the transaction entity is exposed in the API (corresponding to the dateParam). + $ref: '#/components/schemas/Date' + customerId: + type: string + description: Unique and unambiguous identification used by the bank for the customer. + example: 12345-6 + reversalIndicator: + description: Indicates whether it is the reversal of a previously reported movement. + type: boolean + default: false + example: false + endOfDayIndicator: + description: Indicates if the transaction has been confirmed by the end-of-day (eod) processing. + type: boolean + example: true + reference: + type: string + description: | + Transaction reference as used in the transaction statement. This could be equal to the id of the transaction + but may not if GUID are used to identify the transaction. + example: XS12345678 + description: + type: string + description: Human readable description of the transaction. Often referred to as 'Buchungs-Text' in german transaction statements. + example: Buy 500 Apple Inc at 176 USD + placeOfTrade: + $ref: '#/components/schemas/PlaceOfTrade' + reversedTransactionId: + type: string + maxLength: 128 + description: States the identification of the transaction that was reversed. + example: 2134123-415 + tradeDate: + description: Specifies the date/time on which the trade was executed. + $ref: '#/components/schemas/Date' + settlementDate: + description: Date and time at which the securities are to be delivered or received. + $ref: '#/components/schemas/Date' + triggeringFinancialInstrument: + $ref: '#/components/schemas/FinancialInstrument' + triggeringQuantity: + $ref: '#/components/schemas/Quantity' + triggeringPrice: + $ref: '#/components/schemas/Price' + movementList: + type: array + description: List of movements belonging to a transaction from a banks perspective. + items: + $ref: '#/components/schemas/Movement' + postingAmountList: + type: array + description: List of total amounts of money that is to be/was posted to respective accounts in the account currency. + items: + $ref: '#/components/schemas/PostingAmount' + settlementCurrency: + $ref: '#/components/schemas/Currency' + additionalDetails: + type: string + maxLength: 70 + description: Provides additional details on the transaction which can not be included within the structured fields of the message. + example: Replaces transaction nr. 12234567489 + parameters: + path_customerId: + in: path + name: customerId + required: true + schema: + type: string + minLength: 1 + maxLength: 128 + example: 123123-456 + description: Unique and unambiguous identification used by the bank for the customer. + description: Customer ID Parameter. + path_accountId: + in: path + name: accountId + required: true + schema: + type: string + maxLength: 128 + description: The customer's account identification, this should NOT be an IBAN. + description: Account ID parameter. + path_positionId: + in: path + name: positionId + required: true + schema: + type: string + maxLength: 128 + description: Identification for the position given by the bank. + example: 1234566-12-1 + description: Position ID parameter. + path_transactionId: + in: path + name: transactionId + required: true + schema: + type: string + maxLength: 128 + description: Identification for the transaction given by the bank. + example: 1234566-12-7 + description: Transaction ID. + cursor: + in: query + name: cursor + required: false + schema: + type: string + description: An opaque string value used for pagination. + limit: + in: query + name: limit + required: false + schema: + type: integer + format: int32 + minimum: 1 + description: Maximum number of items to return. + date: + in: query + name: date + required: true + schema: + type: string + format: date + description: Full-date according to ISO 8601 i.e. YYYY-MM-DD format. + example: '2019-12-31' + description: A date value. + end_of_day_indicator: + in: query + name: end_of_day_indicator + required: false + schema: + type: boolean + example: true + description: | + Indicates if the resources (positions & transactions) have been confirmed by the end-of-day (eod) processing. + - If the parameter is not set, all resources will be delivered. + - If the parameter is set to true, only resources with with the particular property set to true will be delivered. + - If the parameter is set to false, only resources with with the particular property set to false will be delivered. + x_correlation_id_in_header: + in: header + name: X-Correlation-ID + required: true + schema: + type: string + description: Unique ID (defined by the caller) which will be reflected back in the response. + securitySchemes: {} + responses: + '400': + headers: + Content-Type: + $ref: '#/components/headers/Problem-Content-Type' + Content-Language: + $ref: '#/components/headers/Content-Language' + X-Correlation-Id: + $ref: '#/components/headers/X-Correlation-Id' + description: | + Bad Request - The server cannot or will not process the request due to something that is perceived to be a client error + as malformed request syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/CommonErrorResponse' + '401': + headers: + Content-Type: + $ref: '#/components/headers/Problem-Content-Type' + Content-Language: + $ref: '#/components/headers/Content-Language' + X-Correlation-Id: + $ref: '#/components/headers/X-Correlation-Id' + description: Unauthorized - The request has not been applied because it lacks valid authentication credentials for the target resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/CommonErrorResponse' + '403': + headers: + Content-Type: + $ref: '#/components/headers/Problem-Content-Type' + Content-Language: + $ref: '#/components/headers/Content-Language' + X-Correlation-Id: + $ref: '#/components/headers/X-Correlation-Id' + description: Forbidden - The server understood the request but refuses to authorize it. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/CommonErrorResponse' + '404': + headers: + Content-Type: + $ref: '#/components/headers/Problem-Content-Type' + Content-Language: + $ref: '#/components/headers/Content-Language' + X-Correlation-Id: + $ref: '#/components/headers/X-Correlation-Id' + description: | + Not Found - The origin server did not find a current representation for the target resource + or is not willing to disclose that one exists. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/CommonErrorResponse' + '405': + headers: + Content-Type: + $ref: '#/components/headers/Problem-Content-Type' + Content-Language: + $ref: '#/components/headers/Content-Language' + X-Correlation-Id: + $ref: '#/components/headers/X-Correlation-Id' + description: | + Method Not Allowed - The method received in the request-line is known by the origin server + but not supported by the target resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/CommonErrorResponse' + '500': + headers: + Content-Type: + $ref: '#/components/headers/Problem-Content-Type' + Content-Language: + $ref: '#/components/headers/Content-Language' + X-Correlation-Id: + $ref: '#/components/headers/X-Correlation-Id' + description: Internal Server Error - The server encountered an unexpected condition that prevented it from fulfilling the request. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/CommonErrorResponse' + '501': + headers: + Content-Type: + $ref: '#/components/headers/Problem-Content-Type' + Content-Language: + $ref: '#/components/headers/Content-Language' + X-Correlation-Id: + $ref: '#/components/headers/X-Correlation-Id' + description: Not Implemented - The server does not support the functionality required to fulfill the request. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/CommonErrorResponse' + '503': + headers: + Content-Type: + $ref: '#/components/headers/Problem-Content-Type' + Content-Language: + $ref: '#/components/headers/Content-Language' + X-Correlation-Id: + $ref: '#/components/headers/X-Correlation-Id' + description: Service Unavailable - The server is currently unable to handle the request due to a temporary overload or scheduled maintenance. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/CommonErrorResponse' + headers: + X-Correlation-Id: + description: Client defined ID from request to correlates HTTP requests between a client and server. + schema: + type: string + example: f058ebd6-02f7-4d3f-942e-904344e8cde5 + X-Next-Cursor: + description: An opaque string value, or an empty string if there are no more results. + required: false + schema: + type: string + Problem-Content-Type: + description: application/problem+json; charset=utf-8 according to RFC7807. + schema: + type: string + example: application/problem+json + Content-Language: + description: Response language - always en. + schema: + type: string + example: en \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/stripe-light.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/stripe-light.yaml new file mode 100644 index 000000000..2cd5185a8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/stripe-light.yaml @@ -0,0 +1,105 @@ +openapi: 3.0.0 +paths: + /v1/products: + get: + requestBody: + content: + application/x-www-form-urlencoded: + encoding: { } + schema: + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/product' + type: array + type: object + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' +components: + schemas: + product: + type: object + properties: + id: + type: string + default_price: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/price' + price: + type: object + properties: + active: + type: boolean + error: + type: object + properties: + error: + $ref: '#/components/schemas/api_errors' + otherError: + $ref: '#/components/schemas/api_errors' + required: + - error + api_errors: + type: object + properties: + firstFile: + $ref: '#/components/schemas/file' + secondFile: + $ref: '#/components/schemas/file' + file: + type: object + properties: + created: + type: integer + expires_at: + type: integer + filename: + type: string + id: + type: string + links: + title: FileResourceFileLinkList + type: object + properties: + data: + items: + $ref: '#/components/schemas/file_link' + type: array + has_more: + type: boolean + url: + type: string + file_link: + type: object + title: FileLink + properties: + id: + type: string + created: + type: integer + file: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + livemode: + type: boolean + url: + type: string diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/stripe-spec3.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/stripe-spec3.yaml new file mode 100644 index 000000000..511209b48 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/ai/stripe-spec3.yaml @@ -0,0 +1,68393 @@ +openapi: 3.0.0 +components: + schemas: + account: + description: >- + This is an object representing a Stripe account. You can retrieve it to + see + + properties on the account like its current requirements or if the + account is + + enabled to make live charges or receive payouts. + + + For accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + + is `application`, which includes Custom accounts, the properties below + are always + + returned. + + + For accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + + is `stripe`, which includes Standard and Express accounts, some + properties are only returned + + until you create an [Account Link](/api/account_links) or [Account + Session](/api/account_sessions) + + to start Connect Onboarding. Learn about the [differences between + accounts](/connect/accounts). + properties: + business_profile: + anyOf: + - $ref: '#/components/schemas/account_business_profile' + description: Business information about the account. + nullable: true + business_type: + description: The business type. + enum: + - company + - government_entity + - individual + - non_profit + nullable: true + type: string + x-stripeBypassValidation: true + capabilities: + $ref: '#/components/schemas/account_capabilities' + charges_enabled: + description: Whether the account can process charges. + type: boolean + company: + $ref: '#/components/schemas/legal_entity_company' + controller: + $ref: '#/components/schemas/account_unification_account_controller' + country: + description: The account's country. + maxLength: 5000 + type: string + created: + description: >- + Time at which the account was connected. Measured in seconds since + the Unix epoch. + format: unix-time + type: integer + default_currency: + description: >- + Three-letter ISO currency code representing the default currency for + the account. This must be a currency that [Stripe supports in the + account's country](https://stripe.com/docs/payouts). + maxLength: 5000 + type: string + details_submitted: + description: >- + Whether account details have been submitted. Accounts with Stripe + Dashboard access, which includes Standard accounts, cannot receive + payouts before this is true. Accounts where this is false should be + directed to [an onboarding flow](/connect/onboarding) to finish + submitting account details. + type: boolean + email: + description: >- + An email address associated with the account. It's not used for + authentication and Stripe doesn't market to this field without + explicit approval from the platform. + maxLength: 5000 + nullable: true + type: string + external_accounts: + description: >- + External accounts (bank accounts and debit cards) currently attached + to this account. External accounts are only returned for requests + where `controller[is_controller]` is true. + properties: + data: + description: >- + The list contains all external accounts that have been attached + to the Stripe account. These may be bank accounts or cards. + items: + anyOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + title: Polymorphic + x-stripeBypassValidation: true + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: ExternalAccountList + type: object + x-expandableFields: + - data + future_requirements: + $ref: '#/components/schemas/account_future_requirements' + groups: + anyOf: + - $ref: '#/components/schemas/account_group_membership' + description: The groups associated with the account. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + individual: + $ref: '#/components/schemas/person' + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - account + type: string + payouts_enabled: + description: Whether the funds in this account can be paid out. + type: boolean + requirements: + $ref: '#/components/schemas/account_requirements' + settings: + anyOf: + - $ref: '#/components/schemas/account_settings' + description: Options for customizing how the account functions within Stripe. + nullable: true + tos_acceptance: + $ref: '#/components/schemas/account_tos_acceptance' + type: + description: >- + The Stripe account type. Can be `standard`, `express`, `custom`, or + `none`. + enum: + - custom + - express + - none + - standard + type: string + required: + - id + - object + title: Account + type: object + x-expandableFields: + - business_profile + - capabilities + - company + - controller + - external_accounts + - future_requirements + - groups + - individual + - requirements + - settings + - tos_acceptance + x-resourceId: account + account_annual_revenue: + description: '' + properties: + amount: + description: >- + A non-negative integer representing the amount in the [smallest + currency unit](/currencies#zero-decimal). + nullable: true + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + nullable: true + type: string + fiscal_year_end: + description: >- + The close-out date of the preceding fiscal year in ISO 8601 format. + E.g. 2023-12-31 for the 31st of December, 2023. + maxLength: 5000 + nullable: true + type: string + title: AccountAnnualRevenue + type: object + x-expandableFields: [] + account_bacs_debit_payments_settings: + description: '' + properties: + display_name: + description: >- + The Bacs Direct Debit display name for this account. For payments + made with Bacs Direct Debit, this name appears on the mandate as the + statement descriptor. Mobile banking apps display it as the name of + the business. To use custom branding, set the Bacs Direct Debit + Display Name during or right after creation. Custom branding incurs + an additional monthly fee for the platform. The fee appears 5 + business days after requesting Bacs. If you don't set the display + name before requesting Bacs capability, it's automatically set as + "Stripe" and the account is onboarded to Stripe branding, which is + free. + maxLength: 5000 + nullable: true + type: string + service_user_number: + description: >- + The Bacs Direct Debit Service user number for this account. For + payments made with Bacs Direct Debit, this number is a unique + identifier of the account with our banking partners. + maxLength: 5000 + nullable: true + type: string + title: AccountBacsDebitPaymentsSettings + type: object + x-expandableFields: [] + account_branding_settings: + description: '' + properties: + icon: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + An icon for the account. Must be square and at least 128px x 128px. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + logo: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + A logo for the account that will be used in Checkout instead of the + icon and without the account's name next to it if provided. Must be + at least 128px x 128px. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + primary_color: + description: >- + A CSS hex color value representing the primary branding color for + this account + maxLength: 5000 + nullable: true + type: string + secondary_color: + description: >- + A CSS hex color value representing the secondary branding color for + this account + maxLength: 5000 + nullable: true + type: string + title: AccountBrandingSettings + type: object + x-expandableFields: + - icon + - logo + account_business_profile: + description: '' + properties: + annual_revenue: + anyOf: + - $ref: '#/components/schemas/account_annual_revenue' + description: The applicant's gross annual revenue for its preceding fiscal year. + nullable: true + estimated_worker_count: + description: >- + An estimated upper bound of employees, contractors, vendors, etc. + currently working for the business. + nullable: true + type: integer + mcc: + description: >- + [The merchant category code for the account](/connect/setting-mcc). + MCCs are used to classify businesses based on the goods or services + they provide. + maxLength: 5000 + nullable: true + type: string + minority_owned_business_designation: + description: >- + Whether the business is a minority-owned, women-owned, and/or + LGBTQI+-owned business. + items: + enum: + - lgbtqi_owned_business + - minority_owned_business + - none_of_these_apply + - prefer_not_to_answer + - women_owned_business + type: string + nullable: true + type: array + monthly_estimated_revenue: + $ref: '#/components/schemas/account_monthly_estimated_revenue' + name: + description: The customer-facing business name. + maxLength: 5000 + nullable: true + type: string + product_description: + description: >- + Internal-only description of the product sold or service provided by + the business. It's used by Stripe for risk and underwriting + purposes. + maxLength: 40000 + nullable: true + type: string + support_address: + anyOf: + - $ref: '#/components/schemas/address' + description: A publicly available mailing address for sending support issues to. + nullable: true + support_email: + description: A publicly available email address for sending support issues to. + maxLength: 5000 + nullable: true + type: string + support_phone: + description: A publicly available phone number to call with support issues. + maxLength: 5000 + nullable: true + type: string + support_url: + description: A publicly available website for handling support issues. + maxLength: 5000 + nullable: true + type: string + url: + description: The business's publicly available website. + maxLength: 5000 + nullable: true + type: string + title: AccountBusinessProfile + type: object + x-expandableFields: + - annual_revenue + - monthly_estimated_revenue + - support_address + account_capabilities: + description: '' + properties: + acss_debit_payments: + description: >- + The status of the Canadian pre-authorized debits payments capability + of the account, or whether the account can directly process Canadian + pre-authorized debits charges. + enum: + - active + - inactive + - pending + type: string + affirm_payments: + description: >- + The status of the Affirm capability of the account, or whether the + account can directly process Affirm charges. + enum: + - active + - inactive + - pending + type: string + afterpay_clearpay_payments: + description: >- + The status of the Afterpay Clearpay capability of the account, or + whether the account can directly process Afterpay Clearpay charges. + enum: + - active + - inactive + - pending + type: string + alma_payments: + description: >- + The status of the Alma capability of the account, or whether the + account can directly process Alma payments. + enum: + - active + - inactive + - pending + type: string + amazon_pay_payments: + description: >- + The status of the AmazonPay capability of the account, or whether + the account can directly process AmazonPay payments. + enum: + - active + - inactive + - pending + type: string + au_becs_debit_payments: + description: >- + The status of the BECS Direct Debit (AU) payments capability of the + account, or whether the account can directly process BECS Direct + Debit (AU) charges. + enum: + - active + - inactive + - pending + type: string + bacs_debit_payments: + description: >- + The status of the Bacs Direct Debits payments capability of the + account, or whether the account can directly process Bacs Direct + Debits charges. + enum: + - active + - inactive + - pending + type: string + bancontact_payments: + description: >- + The status of the Bancontact payments capability of the account, or + whether the account can directly process Bancontact charges. + enum: + - active + - inactive + - pending + type: string + bank_transfer_payments: + description: >- + The status of the customer_balance payments capability of the + account, or whether the account can directly process + customer_balance charges. + enum: + - active + - inactive + - pending + type: string + billie_payments: + description: >- + The status of the Billie capability of the account, or whether the + account can directly process Billie payments. + enum: + - active + - inactive + - pending + type: string + blik_payments: + description: >- + The status of the blik payments capability of the account, or + whether the account can directly process blik charges. + enum: + - active + - inactive + - pending + type: string + boleto_payments: + description: >- + The status of the boleto payments capability of the account, or + whether the account can directly process boleto charges. + enum: + - active + - inactive + - pending + type: string + card_issuing: + description: >- + The status of the card issuing capability of the account, or whether + you can use Issuing to distribute funds on cards + enum: + - active + - inactive + - pending + type: string + card_payments: + description: >- + The status of the card payments capability of the account, or + whether the account can directly process credit and debit card + charges. + enum: + - active + - inactive + - pending + type: string + cartes_bancaires_payments: + description: >- + The status of the Cartes Bancaires payments capability of the + account, or whether the account can directly process Cartes + Bancaires card charges in EUR currency. + enum: + - active + - inactive + - pending + type: string + cashapp_payments: + description: >- + The status of the Cash App Pay capability of the account, or whether + the account can directly process Cash App Pay payments. + enum: + - active + - inactive + - pending + type: string + eps_payments: + description: >- + The status of the EPS payments capability of the account, or whether + the account can directly process EPS charges. + enum: + - active + - inactive + - pending + type: string + fpx_payments: + description: >- + The status of the FPX payments capability of the account, or whether + the account can directly process FPX charges. + enum: + - active + - inactive + - pending + type: string + gb_bank_transfer_payments: + description: >- + The status of the GB customer_balance payments (GBP currency) + capability of the account, or whether the account can directly + process GB customer_balance charges. + enum: + - active + - inactive + - pending + type: string + giropay_payments: + description: >- + The status of the giropay payments capability of the account, or + whether the account can directly process giropay charges. + enum: + - active + - inactive + - pending + type: string + grabpay_payments: + description: >- + The status of the GrabPay payments capability of the account, or + whether the account can directly process GrabPay charges. + enum: + - active + - inactive + - pending + type: string + ideal_payments: + description: >- + The status of the iDEAL payments capability of the account, or + whether the account can directly process iDEAL charges. + enum: + - active + - inactive + - pending + type: string + india_international_payments: + description: >- + The status of the india_international_payments capability of the + account, or whether the account can process international charges + (non INR) in India. + enum: + - active + - inactive + - pending + type: string + jcb_payments: + description: >- + The status of the JCB payments capability of the account, or whether + the account (Japan only) can directly process JCB credit card + charges in JPY currency. + enum: + - active + - inactive + - pending + type: string + jp_bank_transfer_payments: + description: >- + The status of the Japanese customer_balance payments (JPY currency) + capability of the account, or whether the account can directly + process Japanese customer_balance charges. + enum: + - active + - inactive + - pending + type: string + kakao_pay_payments: + description: >- + The status of the KakaoPay capability of the account, or whether the + account can directly process KakaoPay payments. + enum: + - active + - inactive + - pending + type: string + klarna_payments: + description: >- + The status of the Klarna payments capability of the account, or + whether the account can directly process Klarna charges. + enum: + - active + - inactive + - pending + type: string + konbini_payments: + description: >- + The status of the konbini payments capability of the account, or + whether the account can directly process konbini charges. + enum: + - active + - inactive + - pending + type: string + kr_card_payments: + description: >- + The status of the KrCard capability of the account, or whether the + account can directly process KrCard payments. + enum: + - active + - inactive + - pending + type: string + legacy_payments: + description: The status of the legacy payments capability of the account. + enum: + - active + - inactive + - pending + type: string + link_payments: + description: >- + The status of the link_payments capability of the account, or + whether the account can directly process Link charges. + enum: + - active + - inactive + - pending + type: string + mobilepay_payments: + description: >- + The status of the MobilePay capability of the account, or whether + the account can directly process MobilePay charges. + enum: + - active + - inactive + - pending + type: string + multibanco_payments: + description: >- + The status of the Multibanco payments capability of the account, or + whether the account can directly process Multibanco charges. + enum: + - active + - inactive + - pending + type: string + mx_bank_transfer_payments: + description: >- + The status of the Mexican customer_balance payments (MXN currency) + capability of the account, or whether the account can directly + process Mexican customer_balance charges. + enum: + - active + - inactive + - pending + type: string + naver_pay_payments: + description: >- + The status of the NaverPay capability of the account, or whether the + account can directly process NaverPay payments. + enum: + - active + - inactive + - pending + type: string + nz_bank_account_becs_debit_payments: + description: >- + The status of the New Zealand BECS Direct Debit payments capability + of the account, or whether the account can directly process New + Zealand BECS Direct Debit charges. + enum: + - active + - inactive + - pending + type: string + oxxo_payments: + description: >- + The status of the OXXO payments capability of the account, or + whether the account can directly process OXXO charges. + enum: + - active + - inactive + - pending + type: string + p24_payments: + description: >- + The status of the P24 payments capability of the account, or whether + the account can directly process P24 charges. + enum: + - active + - inactive + - pending + type: string + pay_by_bank_payments: + description: >- + The status of the pay_by_bank payments capability of the account, or + whether the account can directly process pay_by_bank charges. + enum: + - active + - inactive + - pending + type: string + payco_payments: + description: >- + The status of the Payco capability of the account, or whether the + account can directly process Payco payments. + enum: + - active + - inactive + - pending + type: string + paynow_payments: + description: >- + The status of the paynow payments capability of the account, or + whether the account can directly process paynow charges. + enum: + - active + - inactive + - pending + type: string + pix_payments: + description: >- + The status of the pix payments capability of the account, or whether + the account can directly process pix charges. + enum: + - active + - inactive + - pending + type: string + promptpay_payments: + description: >- + The status of the promptpay payments capability of the account, or + whether the account can directly process promptpay charges. + enum: + - active + - inactive + - pending + type: string + revolut_pay_payments: + description: >- + The status of the RevolutPay capability of the account, or whether + the account can directly process RevolutPay payments. + enum: + - active + - inactive + - pending + type: string + samsung_pay_payments: + description: >- + The status of the SamsungPay capability of the account, or whether + the account can directly process SamsungPay payments. + enum: + - active + - inactive + - pending + type: string + satispay_payments: + description: >- + The status of the Satispay capability of the account, or whether the + account can directly process Satispay payments. + enum: + - active + - inactive + - pending + type: string + sepa_bank_transfer_payments: + description: >- + The status of the SEPA customer_balance payments (EUR currency) + capability of the account, or whether the account can directly + process SEPA customer_balance charges. + enum: + - active + - inactive + - pending + type: string + sepa_debit_payments: + description: >- + The status of the SEPA Direct Debits payments capability of the + account, or whether the account can directly process SEPA Direct + Debits charges. + enum: + - active + - inactive + - pending + type: string + sofort_payments: + description: >- + The status of the Sofort payments capability of the account, or + whether the account can directly process Sofort charges. + enum: + - active + - inactive + - pending + type: string + swish_payments: + description: >- + The status of the Swish capability of the account, or whether the + account can directly process Swish payments. + enum: + - active + - inactive + - pending + type: string + tax_reporting_us_1099_k: + description: >- + The status of the tax reporting 1099-K (US) capability of the + account. + enum: + - active + - inactive + - pending + type: string + tax_reporting_us_1099_misc: + description: >- + The status of the tax reporting 1099-MISC (US) capability of the + account. + enum: + - active + - inactive + - pending + type: string + transfers: + description: >- + The status of the transfers capability of the account, or whether + your platform can transfer funds to the account. + enum: + - active + - inactive + - pending + type: string + treasury: + description: >- + The status of the banking capability, or whether the account can + have bank accounts. + enum: + - active + - inactive + - pending + type: string + twint_payments: + description: >- + The status of the TWINT capability of the account, or whether the + account can directly process TWINT charges. + enum: + - active + - inactive + - pending + type: string + us_bank_account_ach_payments: + description: >- + The status of the US bank account ACH payments capability of the + account, or whether the account can directly process US bank account + charges. + enum: + - active + - inactive + - pending + type: string + us_bank_transfer_payments: + description: >- + The status of the US customer_balance payments (USD currency) + capability of the account, or whether the account can directly + process US customer_balance charges. + enum: + - active + - inactive + - pending + type: string + zip_payments: + description: >- + The status of the Zip capability of the account, or whether the + account can directly process Zip charges. + enum: + - active + - inactive + - pending + type: string + title: AccountCapabilities + type: object + x-expandableFields: [] + account_capability_future_requirements: + description: '' + properties: + alternatives: + description: >- + Fields that are due and can be satisfied by providing the + corresponding alternative fields instead. + items: + $ref: '#/components/schemas/account_requirements_alternative' + nullable: true + type: array + current_deadline: + description: >- + Date on which `future_requirements` becomes the main `requirements` + hash and `future_requirements` becomes empty. After the transition, + `currently_due` requirements may immediately become `past_due`, but + the account may also be given a grace period depending on the + capability's enablement state prior to transitioning. + format: unix-time + nullable: true + type: integer + currently_due: + description: >- + Fields that need to be collected to keep the capability enabled. If + not collected by `future_requirements[current_deadline]`, these + fields will transition to the main `requirements` hash. + items: + maxLength: 5000 + type: string + type: array + disabled_reason: + description: >- + This is typed as an enum for consistency with + `requirements.disabled_reason`, but it safe to assume + `future_requirements.disabled_reason` is null because fields in + `future_requirements` will never disable the account. + enum: + - other + - paused.inactivity + - pending.onboarding + - pending.review + - platform_disabled + - platform_paused + - rejected.inactivity + - rejected.other + - rejected.unsupported_business + - requirements.fields_needed + nullable: true + type: string + errors: + description: >- + Fields that are `currently_due` and need to be collected again + because validation or verification failed. + items: + $ref: '#/components/schemas/account_requirements_error' + type: array + eventually_due: + description: >- + Fields you must collect when all thresholds are reached. As they + become required, they appear in `currently_due` as well. + items: + maxLength: 5000 + type: string + type: array + past_due: + description: >- + Fields that weren't collected by `requirements.current_deadline`. + These fields need to be collected to enable the capability on the + account. New fields will never appear here; + `future_requirements.past_due` will always be a subset of + `requirements.past_due`. + items: + maxLength: 5000 + type: string + type: array + pending_verification: + description: >- + Fields that might become required depending on the results of + verification or review. It's an empty array unless an asynchronous + verification is pending. If verification fails, these fields move to + `eventually_due` or `currently_due`. Fields might appear in + `eventually_due` or `currently_due` and in `pending_verification` if + verification fails but another verification is still pending. + items: + maxLength: 5000 + type: string + type: array + required: + - currently_due + - errors + - eventually_due + - past_due + - pending_verification + title: AccountCapabilityFutureRequirements + type: object + x-expandableFields: + - alternatives + - errors + account_capability_requirements: + description: '' + properties: + alternatives: + description: >- + Fields that are due and can be satisfied by providing the + corresponding alternative fields instead. + items: + $ref: '#/components/schemas/account_requirements_alternative' + nullable: true + type: array + current_deadline: + description: >- + The date by which all required account information must be both + submitted and verified. This includes fields listed in + `currently_due` as well as those in `pending_verification`. If any + required information is missing or unverified by this date, the + account may be disabled. Note that `current_deadline` may change if + additional `currently_due` requirements are requested. + format: unix-time + nullable: true + type: integer + currently_due: + description: >- + Fields that need to be collected to keep the capability enabled. If + not collected by `current_deadline`, these fields appear in + `past_due` as well, and the capability is disabled. + items: + maxLength: 5000 + type: string + type: array + disabled_reason: + description: >- + Description of why the capability is disabled. [Learn more about + handling verification + issues](https://stripe.com/docs/connect/handling-api-verification). + enum: + - other + - paused.inactivity + - pending.onboarding + - pending.review + - platform_disabled + - platform_paused + - rejected.inactivity + - rejected.other + - rejected.unsupported_business + - requirements.fields_needed + nullable: true + type: string + errors: + description: >- + Fields that are `currently_due` and need to be collected again + because validation or verification failed. + items: + $ref: '#/components/schemas/account_requirements_error' + type: array + eventually_due: + description: >- + Fields you must collect when all thresholds are reached. As they + become required, they appear in `currently_due` as well, and + `current_deadline` becomes set. + items: + maxLength: 5000 + type: string + type: array + past_due: + description: >- + Fields that weren't collected by `current_deadline`. These fields + need to be collected to enable the capability on the account. + items: + maxLength: 5000 + type: string + type: array + pending_verification: + description: >- + Fields that might become required depending on the results of + verification or review. It's an empty array unless an asynchronous + verification is pending. If verification fails, these fields move to + `eventually_due`, `currently_due`, or `past_due`. Fields might + appear in `eventually_due`, `currently_due`, or `past_due` and in + `pending_verification` if verification fails but another + verification is still pending. + items: + maxLength: 5000 + type: string + type: array + required: + - currently_due + - errors + - eventually_due + - past_due + - pending_verification + title: AccountCapabilityRequirements + type: object + x-expandableFields: + - alternatives + - errors + account_card_issuing_settings: + description: '' + properties: + tos_acceptance: + $ref: '#/components/schemas/card_issuing_account_terms_of_service' + title: AccountCardIssuingSettings + type: object + x-expandableFields: + - tos_acceptance + account_card_payments_settings: + description: '' + properties: + decline_on: + $ref: '#/components/schemas/account_decline_charge_on' + statement_descriptor_prefix: + description: >- + The default text that appears on credit card statements when a + charge is made. This field prefixes any dynamic + `statement_descriptor` specified on the charge. + `statement_descriptor_prefix` is useful for maximizing descriptor + space for the dynamic portion. + maxLength: 5000 + nullable: true + type: string + statement_descriptor_prefix_kana: + description: >- + The Kana variation of the default text that appears on credit card + statements when a charge is made (Japan only). This field prefixes + any dynamic `statement_descriptor_suffix_kana` specified on the + charge. `statement_descriptor_prefix_kana` is useful for maximizing + descriptor space for the dynamic portion. + maxLength: 5000 + nullable: true + type: string + statement_descriptor_prefix_kanji: + description: >- + The Kanji variation of the default text that appears on credit card + statements when a charge is made (Japan only). This field prefixes + any dynamic `statement_descriptor_suffix_kanji` specified on the + charge. `statement_descriptor_prefix_kanji` is useful for maximizing + descriptor space for the dynamic portion. + maxLength: 5000 + nullable: true + type: string + title: AccountCardPaymentsSettings + type: object + x-expandableFields: + - decline_on + account_dashboard_settings: + description: '' + properties: + display_name: + description: >- + The display name for this account. This is used on the Stripe + Dashboard to differentiate between accounts. + maxLength: 5000 + nullable: true + type: string + timezone: + description: >- + The timezone used in the Stripe Dashboard for this account. A list + of possible time zone values is maintained at the [IANA Time Zone + Database](http://www.iana.org/time-zones). + maxLength: 5000 + nullable: true + type: string + title: AccountDashboardSettings + type: object + x-expandableFields: [] + account_decline_charge_on: + description: '' + properties: + avs_failure: + description: >- + Whether Stripe automatically declines charges with an incorrect ZIP + or postal code. This setting only applies when a ZIP or postal code + is provided and they fail bank verification. + type: boolean + cvc_failure: + description: >- + Whether Stripe automatically declines charges with an incorrect CVC. + This setting only applies when a CVC is provided and it fails bank + verification. + type: boolean + required: + - avs_failure + - cvc_failure + title: AccountDeclineChargeOn + type: object + x-expandableFields: [] + account_future_requirements: + description: '' + properties: + alternatives: + description: >- + Fields that are due and can be satisfied by providing the + corresponding alternative fields instead. + items: + $ref: '#/components/schemas/account_requirements_alternative' + nullable: true + type: array + current_deadline: + description: >- + Date on which `future_requirements` becomes the main `requirements` + hash and `future_requirements` becomes empty. After the transition, + `currently_due` requirements may immediately become `past_due`, but + the account may also be given a grace period depending on its + enablement state prior to transitioning. + format: unix-time + nullable: true + type: integer + currently_due: + description: >- + Fields that need to be collected to keep the account enabled. If not + collected by `future_requirements[current_deadline]`, these fields + will transition to the main `requirements` hash. + items: + maxLength: 5000 + type: string + nullable: true + type: array + disabled_reason: + description: >- + This is typed as an enum for consistency with + `requirements.disabled_reason`. + enum: + - action_required.requested_capabilities + - listed + - other + - platform_paused + - rejected.fraud + - rejected.incomplete_verification + - rejected.listed + - rejected.other + - rejected.platform_fraud + - rejected.platform_other + - rejected.platform_terms_of_service + - rejected.terms_of_service + - requirements.past_due + - requirements.pending_verification + - under_review + nullable: true + type: string + errors: + description: >- + Fields that are `currently_due` and need to be collected again + because validation or verification failed. + items: + $ref: '#/components/schemas/account_requirements_error' + nullable: true + type: array + eventually_due: + description: >- + Fields you must collect when all thresholds are reached. As they + become required, they appear in `currently_due` as well. + items: + maxLength: 5000 + type: string + nullable: true + type: array + past_due: + description: >- + Fields that weren't collected by `requirements.current_deadline`. + These fields need to be collected to enable the capability on the + account. New fields will never appear here; + `future_requirements.past_due` will always be a subset of + `requirements.past_due`. + items: + maxLength: 5000 + type: string + nullable: true + type: array + pending_verification: + description: >- + Fields that might become required depending on the results of + verification or review. It's an empty array unless an asynchronous + verification is pending. If verification fails, these fields move to + `eventually_due` or `currently_due`. Fields might appear in + `eventually_due` or `currently_due` and in `pending_verification` if + verification fails but another verification is still pending. + items: + maxLength: 5000 + type: string + nullable: true + type: array + title: AccountFutureRequirements + type: object + x-expandableFields: + - alternatives + - errors + account_group_membership: + description: '' + properties: + payments_pricing: + description: >- + The group the account is in to determine their payments pricing, and + null if the account is on customized pricing. [See the Platform + pricing tool + documentation](https://stripe.com/docs/connect/platform-pricing-tools) + for details. + maxLength: 5000 + nullable: true + type: string + title: AccountGroupMembership + type: object + x-expandableFields: [] + account_invoices_settings: + description: '' + properties: + default_account_tax_ids: + description: >- + The list of default Account Tax IDs to automatically include on + invoices. Account Tax IDs get added when an invoice is finalized. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_id' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_id' + nullable: true + type: array + hosted_payment_method_save: + description: >- + Whether payment methods should be saved when a payment is completed + for a one-time invoices on a hosted invoice page. + enum: + - always + - never + - offer + nullable: true + type: string + title: AccountInvoicesSettings + type: object + x-expandableFields: + - default_account_tax_ids + account_link: + description: >- + Account Links are the means by which a Connect platform grants a + connected account permission to access + + Stripe-hosted applications, such as Connect Onboarding. + + + Related guide: [Connect + Onboarding](https://stripe.com/docs/connect/custom/hosted-onboarding) + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + expires_at: + description: The timestamp at which this account link will expire. + format: unix-time + type: integer + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - account_link + type: string + url: + description: The URL for the account link. + maxLength: 5000 + type: string + required: + - created + - expires_at + - object + - url + title: AccountLink + type: object + x-expandableFields: [] + x-resourceId: account_link + account_monthly_estimated_revenue: + description: '' + properties: + amount: + description: >- + A non-negative integer representing how much to charge in the + [smallest currency unit](/currencies#zero-decimal). + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + required: + - amount + - currency + title: AccountMonthlyEstimatedRevenue + type: object + x-expandableFields: [] + account_payments_settings: + description: '' + properties: + statement_descriptor: + description: >- + The default text that appears on credit card statements when a + charge is made. This field prefixes any dynamic + `statement_descriptor` specified on the charge. + maxLength: 5000 + nullable: true + type: string + statement_descriptor_kana: + description: >- + The Kana variation of `statement_descriptor` used for charges in + Japan. Japanese statement descriptors have [special + requirements](https://docs.stripe.com/get-started/account/statement-descriptors#set-japanese-statement-descriptors). + maxLength: 5000 + nullable: true + type: string + statement_descriptor_kanji: + description: >- + The Kanji variation of `statement_descriptor` used for charges in + Japan. Japanese statement descriptors have [special + requirements](https://docs.stripe.com/get-started/account/statement-descriptors#set-japanese-statement-descriptors). + maxLength: 5000 + nullable: true + type: string + title: AccountPaymentsSettings + type: object + x-expandableFields: [] + account_payout_settings: + description: '' + properties: + debit_negative_balances: + description: >- + A Boolean indicating if Stripe should try to reclaim negative + balances from an attached bank account. See [Understanding Connect + account balances](/connect/account-balances) for details. The + default value is `false` when + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `application`, which includes Custom accounts, otherwise `true`. + type: boolean + schedule: + $ref: '#/components/schemas/transfer_schedule' + statement_descriptor: + description: >- + The text that appears on the bank account statement for payouts. If + not set, this defaults to the platform's bank descriptor as set in + the Dashboard. + maxLength: 5000 + nullable: true + type: string + required: + - debit_negative_balances + - schedule + title: AccountPayoutSettings + type: object + x-expandableFields: + - schedule + account_requirements: + description: '' + properties: + alternatives: + description: >- + Fields that are due and can be satisfied by providing the + corresponding alternative fields instead. + items: + $ref: '#/components/schemas/account_requirements_alternative' + nullable: true + type: array + current_deadline: + description: >- + Date by which the fields in `currently_due` must be collected to + keep the account enabled. These fields may disable the account + sooner if the next threshold is reached before they are collected. + format: unix-time + nullable: true + type: integer + currently_due: + description: >- + Fields that need to be collected to keep the account enabled. If not + collected by `current_deadline`, these fields appear in `past_due` + as well, and the account is disabled. + items: + maxLength: 5000 + type: string + nullable: true + type: array + disabled_reason: + description: >- + If the account is disabled, this enum describes why. [Learn more + about handling verification + issues](https://stripe.com/docs/connect/handling-api-verification). + enum: + - action_required.requested_capabilities + - listed + - other + - platform_paused + - rejected.fraud + - rejected.incomplete_verification + - rejected.listed + - rejected.other + - rejected.platform_fraud + - rejected.platform_other + - rejected.platform_terms_of_service + - rejected.terms_of_service + - requirements.past_due + - requirements.pending_verification + - under_review + nullable: true + type: string + errors: + description: >- + Fields that are `currently_due` and need to be collected again + because validation or verification failed. + items: + $ref: '#/components/schemas/account_requirements_error' + nullable: true + type: array + eventually_due: + description: >- + Fields you must collect when all thresholds are reached. As they + become required, they appear in `currently_due` as well, and + `current_deadline` becomes set. + items: + maxLength: 5000 + type: string + nullable: true + type: array + past_due: + description: >- + Fields that weren't collected by `current_deadline`. These fields + need to be collected to enable the account. + items: + maxLength: 5000 + type: string + nullable: true + type: array + pending_verification: + description: >- + Fields that might become required depending on the results of + verification or review. It's an empty array unless an asynchronous + verification is pending. If verification fails, these fields move to + `eventually_due`, `currently_due`, or `past_due`. Fields might + appear in `eventually_due`, `currently_due`, or `past_due` and in + `pending_verification` if verification fails but another + verification is still pending. + items: + maxLength: 5000 + type: string + nullable: true + type: array + title: AccountRequirements + type: object + x-expandableFields: + - alternatives + - errors + account_requirements_alternative: + description: '' + properties: + alternative_fields_due: + description: >- + Fields that can be provided to satisfy all fields in + `original_fields_due`. + items: + maxLength: 5000 + type: string + type: array + original_fields_due: + description: >- + Fields that are due and can be satisfied by providing all fields in + `alternative_fields_due`. + items: + maxLength: 5000 + type: string + type: array + required: + - alternative_fields_due + - original_fields_due + title: AccountRequirementsAlternative + type: object + x-expandableFields: [] + account_requirements_error: + description: '' + properties: + code: + description: The code for the type of error. + enum: + - information_missing + - invalid_address_city_state_postal_code + - invalid_address_highway_contract_box + - invalid_address_private_mailbox + - invalid_business_profile_name + - invalid_business_profile_name_denylisted + - invalid_company_name_denylisted + - invalid_dob_age_over_maximum + - invalid_dob_age_under_18 + - invalid_dob_age_under_minimum + - invalid_product_description_length + - invalid_product_description_url_match + - invalid_representative_country + - invalid_signator + - invalid_statement_descriptor_business_mismatch + - invalid_statement_descriptor_denylisted + - invalid_statement_descriptor_length + - invalid_statement_descriptor_prefix_denylisted + - invalid_statement_descriptor_prefix_mismatch + - invalid_street_address + - invalid_tax_id + - invalid_tax_id_format + - invalid_tos_acceptance + - invalid_url_denylisted + - invalid_url_format + - invalid_url_web_presence_detected + - invalid_url_website_business_information_mismatch + - invalid_url_website_empty + - invalid_url_website_inaccessible + - invalid_url_website_inaccessible_geoblocked + - invalid_url_website_inaccessible_password_protected + - invalid_url_website_incomplete + - invalid_url_website_incomplete_cancellation_policy + - invalid_url_website_incomplete_customer_service_details + - invalid_url_website_incomplete_legal_restrictions + - invalid_url_website_incomplete_refund_policy + - invalid_url_website_incomplete_return_policy + - invalid_url_website_incomplete_terms_and_conditions + - invalid_url_website_incomplete_under_construction + - invalid_url_website_other + - invalid_value_other + - verification_directors_mismatch + - verification_document_address_mismatch + - verification_document_address_missing + - verification_document_corrupt + - verification_document_country_not_supported + - verification_document_directors_mismatch + - verification_document_dob_mismatch + - verification_document_duplicate_type + - verification_document_expired + - verification_document_failed_copy + - verification_document_failed_greyscale + - verification_document_failed_other + - verification_document_failed_test_mode + - verification_document_fraudulent + - verification_document_id_number_mismatch + - verification_document_id_number_missing + - verification_document_incomplete + - verification_document_invalid + - verification_document_issue_or_expiry_date_missing + - verification_document_manipulated + - verification_document_missing_back + - verification_document_missing_front + - verification_document_name_mismatch + - verification_document_name_missing + - verification_document_nationality_mismatch + - verification_document_not_readable + - verification_document_not_signed + - verification_document_not_uploaded + - verification_document_photo_mismatch + - verification_document_too_large + - verification_document_type_not_supported + - verification_extraneous_directors + - verification_failed_address_match + - verification_failed_authorizer_authority + - verification_failed_business_iec_number + - verification_failed_document_match + - verification_failed_id_number_match + - verification_failed_keyed_identity + - verification_failed_keyed_match + - verification_failed_name_match + - verification_failed_other + - verification_failed_representative_authority + - verification_failed_residential_address + - verification_failed_tax_id_match + - verification_failed_tax_id_not_issued + - verification_legal_entity_structure_mismatch + - verification_missing_directors + - verification_missing_executives + - verification_missing_owners + - verification_rejected_ownership_exemption_reason + - verification_requires_additional_memorandum_of_associations + - verification_requires_additional_proof_of_registration + - verification_supportability + type: string + x-stripeBypassValidation: true + reason: + description: >- + An informative message that indicates the error type and provides + additional details about the error. + maxLength: 5000 + type: string + requirement: + description: >- + The specific user onboarding requirement field (in the requirements + hash) that needs to be resolved. + maxLength: 5000 + type: string + required: + - code + - reason + - requirement + title: AccountRequirementsError + type: object + x-expandableFields: [] + account_sepa_debit_payments_settings: + description: '' + properties: + creditor_id: + description: >- + SEPA creditor identifier that identifies the company making the + payment. + maxLength: 5000 + type: string + title: AccountSepaDebitPaymentsSettings + type: object + x-expandableFields: [] + account_session: + description: >- + An AccountSession allows a Connect platform to grant access to a + connected account in Connect embedded components. + + + We recommend that you create an AccountSession each time you need to + display an embedded component + + to your user. Do not save AccountSessions to your database as they + expire relatively + + quickly, and cannot be used more than once. + + + Related guide: [Connect embedded + components](https://stripe.com/docs/connect/get-started-connect-embedded-components) + properties: + account: + description: The ID of the account the AccountSession was created for + maxLength: 5000 + type: string + client_secret: + description: >- + The client secret of this AccountSession. Used on the client to set + up secure access to the given `account`. + + + The client secret can be used to provide access to `account` from + your frontend. It should not be stored, logged, or exposed to anyone + other than the connected account. Make sure that you have TLS + enabled on any page that includes the client secret. + + + Refer to our docs to [setup Connect embedded + components](https://stripe.com/docs/connect/get-started-connect-embedded-components) + and learn about how `client_secret` should be handled. + maxLength: 5000 + type: string + components: + $ref: >- + #/components/schemas/connect_embedded_account_session_create_components + expires_at: + description: The timestamp at which this AccountSession will expire. + format: unix-time + type: integer + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - account_session + type: string + required: + - account + - client_secret + - components + - expires_at + - livemode + - object + title: ConnectEmbeddedMethodAccountSessionCreateMethodAccountSession + type: object + x-expandableFields: + - components + x-resourceId: account_session + account_settings: + description: '' + properties: + bacs_debit_payments: + $ref: '#/components/schemas/account_bacs_debit_payments_settings' + branding: + $ref: '#/components/schemas/account_branding_settings' + card_issuing: + $ref: '#/components/schemas/account_card_issuing_settings' + card_payments: + $ref: '#/components/schemas/account_card_payments_settings' + dashboard: + $ref: '#/components/schemas/account_dashboard_settings' + invoices: + $ref: '#/components/schemas/account_invoices_settings' + payments: + $ref: '#/components/schemas/account_payments_settings' + payouts: + $ref: '#/components/schemas/account_payout_settings' + sepa_debit_payments: + $ref: '#/components/schemas/account_sepa_debit_payments_settings' + treasury: + $ref: '#/components/schemas/account_treasury_settings' + required: + - branding + - card_payments + - dashboard + - payments + title: AccountSettings + type: object + x-expandableFields: + - bacs_debit_payments + - branding + - card_issuing + - card_payments + - dashboard + - invoices + - payments + - payouts + - sepa_debit_payments + - treasury + account_terms_of_service: + description: '' + properties: + date: + description: >- + The Unix timestamp marking when the account representative accepted + the service agreement. + nullable: true + type: integer + ip: + description: >- + The IP address from which the account representative accepted the + service agreement. + maxLength: 5000 + nullable: true + type: string + user_agent: + description: >- + The user agent of the browser from which the account representative + accepted the service agreement. + maxLength: 5000 + type: string + title: AccountTermsOfService + type: object + x-expandableFields: [] + account_tos_acceptance: + description: '' + properties: + date: + description: >- + The Unix timestamp marking when the account representative accepted + their service agreement + format: unix-time + nullable: true + type: integer + ip: + description: >- + The IP address from which the account representative accepted their + service agreement + maxLength: 5000 + nullable: true + type: string + service_agreement: + description: The user's service agreement type + maxLength: 5000 + type: string + user_agent: + description: >- + The user agent of the browser from which the account representative + accepted their service agreement + maxLength: 5000 + nullable: true + type: string + title: AccountTOSAcceptance + type: object + x-expandableFields: [] + account_treasury_settings: + description: '' + properties: + tos_acceptance: + $ref: '#/components/schemas/account_terms_of_service' + title: AccountTreasurySettings + type: object + x-expandableFields: + - tos_acceptance + account_unification_account_controller: + description: '' + properties: + fees: + $ref: '#/components/schemas/account_unification_account_controller_fees' + is_controller: + description: >- + `true` if the Connect application retrieving the resource controls + the account and can therefore exercise [platform + controls](https://stripe.com/docs/connect/platform-controls-for-standard-accounts). + Otherwise, this field is null. + type: boolean + losses: + $ref: '#/components/schemas/account_unification_account_controller_losses' + requirement_collection: + description: >- + A value indicating responsibility for collecting requirements on + this account. Only returned when the Connect application retrieving + the resource controls the account. + enum: + - application + - stripe + type: string + stripe_dashboard: + $ref: >- + #/components/schemas/account_unification_account_controller_stripe_dashboard + type: + description: >- + The controller type. Can be `application`, if a Connect application + controls the account, or `account`, if the account controls itself. + enum: + - account + - application + type: string + required: + - type + title: AccountUnificationAccountController + type: object + x-expandableFields: + - fees + - losses + - stripe_dashboard + account_unification_account_controller_fees: + description: '' + properties: + payer: + description: >- + A value indicating the responsible payer of a bundle of Stripe fees + for pricing-control eligible products on this account. Learn more + about [fee behavior on connected + accounts](https://docs.stripe.com/connect/direct-charges-fee-payer-behavior). + enum: + - account + - application + - application_custom + - application_express + type: string + x-stripeBypassValidation: true + required: + - payer + title: AccountUnificationAccountControllerFees + type: object + x-expandableFields: [] + account_unification_account_controller_losses: + description: '' + properties: + payments: + description: >- + A value indicating who is liable when this account can't pay back + negative balances from payments. + enum: + - application + - stripe + type: string + required: + - payments + title: AccountUnificationAccountControllerLosses + type: object + x-expandableFields: [] + account_unification_account_controller_stripe_dashboard: + description: '' + properties: + type: + description: >- + A value indicating the Stripe dashboard this account has access to + independent of the Connect application. + enum: + - express + - full + - none + type: string + required: + - type + title: AccountUnificationAccountControllerStripeDashboard + type: object + x-expandableFields: [] + address: + description: '' + properties: + city: + description: 'City, district, suburb, town, or village.' + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + nullable: true + type: string + line1: + description: 'Address line 1 (e.g., street, PO Box, or company name).' + maxLength: 5000 + nullable: true + type: string + line2: + description: 'Address line 2 (e.g., apartment, suite, unit, or building).' + maxLength: 5000 + nullable: true + type: string + postal_code: + description: ZIP or postal code. + maxLength: 5000 + nullable: true + type: string + state: + description: 'State, county, province, or region.' + maxLength: 5000 + nullable: true + type: string + title: Address + type: object + x-expandableFields: [] + amazon_pay_underlying_payment_method_funding_details: + description: '' + properties: + card: + $ref: '#/components/schemas/payment_method_details_passthrough_card' + type: + description: funding type of the underlying payment method. + enum: + - card + nullable: true + type: string + title: amazon_pay_underlying_payment_method_funding_details + type: object + x-expandableFields: + - card + api_errors: + description: '' + properties: + advice_code: + description: >- + For card errors resulting from a card issuer decline, a short string + indicating [how to proceed with an + error](https://stripe.com/docs/declines#retrying-issuer-declines) if + they provide one. + maxLength: 5000 + type: string + charge: + description: 'For card errors, the ID of the failed charge.' + maxLength: 5000 + type: string + code: + description: >- + For some errors that could be handled programmatically, a short + string indicating the [error + code](https://stripe.com/docs/error-codes) reported. + maxLength: 5000 + type: string + decline_code: + description: >- + For card errors resulting from a card issuer decline, a short string + indicating the [card issuer's reason for the + decline](https://stripe.com/docs/declines#issuer-declines) if they + provide one. + maxLength: 5000 + type: string + doc_url: + description: >- + A URL to more information about the [error + code](https://stripe.com/docs/error-codes) reported. + maxLength: 5000 + type: string + message: + description: >- + A human-readable message providing more details about the error. For + card errors, these messages can be shown to your users. + maxLength: 40000 + type: string + network_advice_code: + description: >- + For card errors resulting from a card issuer decline, a 2 digit code + which indicates the advice given to merchant by the card network on + how to proceed with an error. + maxLength: 5000 + type: string + network_decline_code: + description: >- + For card errors resulting from a card issuer decline, a brand + specific 2, 3, or 4 digit code which indicates the reason the + authorization failed. + maxLength: 5000 + type: string + param: + description: >- + If the error is parameter-specific, the parameter related to the + error. For example, you can use this to display a message near the + correct form field. + maxLength: 5000 + type: string + payment_intent: + $ref: '#/components/schemas/payment_intent' + payment_method: + $ref: '#/components/schemas/payment_method' + payment_method_type: + description: >- + If the error is specific to the type of payment method, the payment + method type that had a problem. This field is only populated for + invoice-related errors. + maxLength: 5000 + type: string + request_log_url: + description: A URL to the request log entry in your dashboard. + maxLength: 5000 + type: string + setup_intent: + $ref: '#/components/schemas/setup_intent' + source: + anyOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + description: >- + The [source object](https://stripe.com/docs/api/sources/object) for + errors returned on a request involving a source. + x-stripeBypassValidation: true + type: + description: >- + The type of error returned. One of `api_error`, `card_error`, + `idempotency_error`, or `invalid_request_error` + enum: + - api_error + - card_error + - idempotency_error + - invalid_request_error + type: string + required: + - type + title: APIErrors + type: object + x-expandableFields: + - payment_intent + - payment_method + - setup_intent + - source + apple_pay_domain: + description: '' + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + domain_name: + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - apple_pay_domain + type: string + required: + - created + - domain_name + - id + - livemode + - object + title: ApplePayDomain + type: object + x-expandableFields: [] + x-resourceId: apple_pay_domain + application: + description: '' + properties: + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + name: + description: The name of the application. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - application + type: string + required: + - id + - object + title: Application + type: object + x-expandableFields: [] + application_fee: + description: '' + properties: + account: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: ID of the Stripe account this fee was taken from. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + amount: + description: 'Amount earned, in cents (or local equivalent).' + type: integer + amount_refunded: + description: >- + Amount in cents (or local equivalent) refunded (can be less than the + amount attribute on the fee if a partial refund was issued) + type: integer + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + description: ID of the Connect application that earned the fee. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + Balance transaction that describes the impact of this collected + application fee on your account balance (not including refunds). + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: ID of the charge that the application fee was taken from. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + fee_source: + anyOf: + - $ref: '#/components/schemas/platform_earning_fee_source' + description: >- + Polymorphic source of the application fee. Includes the ID of the + object the application fee was created from. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - application_fee + type: string + originating_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: >- + ID of the corresponding charge on the platform account, if this fee + was the result of a charge using the `destination` parameter. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + refunded: + description: >- + Whether the fee has been fully refunded. If the fee is only + partially refunded, this attribute will still be false. + type: boolean + refunds: + description: A list of refunds that have been applied to the fee. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/fee_refund' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: FeeRefundList + type: object + x-expandableFields: + - data + required: + - account + - amount + - amount_refunded + - application + - charge + - created + - currency + - id + - livemode + - object + - refunded + - refunds + title: PlatformFee + type: object + x-expandableFields: + - account + - application + - balance_transaction + - charge + - fee_source + - originating_transaction + - refunds + x-resourceId: application_fee + apps.secret: + description: >- + Secret Store is an API that allows Stripe Apps developers to securely + persist secrets for use by UI Extensions and app backends. + + + The primary resource in Secret Store is a `secret`. Other apps can't + view secrets created by an app. Additionally, secrets are scoped to + provide further permission control. + + + All Dashboard users and the app backend share `account` scoped secrets. + Use the `account` scope for secrets that don't change per-user, like a + third-party API key. + + + A `user` scoped secret is accessible by the app backend and one specific + Dashboard user. Use the `user` scope for per-user secrets like per-user + OAuth tokens, where different users might have different permissions. + + + Related guide: [Store data between page + reloads](https://stripe.com/docs/stripe-apps/store-auth-data-custom-objects) + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + deleted: + description: 'If true, indicates that this secret has been deleted' + type: boolean + expires_at: + description: >- + The Unix timestamp for the expiry time of the secret, after which + the secret deletes. + format: unix-time + nullable: true + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + name: + description: A name for the secret that's unique within the scope. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - apps.secret + type: string + payload: + description: The plaintext secret value to be stored. + maxLength: 5000 + nullable: true + type: string + scope: + $ref: '#/components/schemas/secret_service_resource_scope' + required: + - created + - id + - livemode + - name + - object + - scope + title: SecretServiceResourceSecret + type: object + x-expandableFields: + - scope + x-resourceId: apps.secret + automatic_tax: + description: '' + properties: + disabled_reason: + description: 'If Stripe disabled automatic tax, this enum describes why.' + enum: + - finalization_requires_location_inputs + - finalization_system_error + nullable: true + type: string + enabled: + description: >- + Whether Stripe automatically computes tax on this invoice. Note that + incompatible invoice items (invoice items with manually specified + [tax rates](https://stripe.com/docs/api/tax_rates), negative + amounts, or `tax_behavior=unspecified`) cannot be added to automatic + tax invoices. + type: boolean + liability: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The account that's liable for tax. If set, the business address and + tax registrations required to perform the tax calculation are loaded + from this account. The tax transaction is returned in the report of + the connected account. + nullable: true + provider: + description: The tax provider powering automatic tax. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + The status of the most recent automated tax calculation for this + invoice. + enum: + - complete + - failed + - requires_location_inputs + nullable: true + type: string + required: + - enabled + title: AutomaticTax + type: object + x-expandableFields: + - liability + balance: + description: >- + This is an object representing your Stripe balance. You can retrieve it + to see + + the balance currently on your Stripe account. + + + You can also retrieve the balance history, which contains a list of + + [transactions](https://stripe.com/docs/reporting/balance-transaction-types) + that contributed to the balance + + (charges, payouts, and so forth). + + + The available and pending amounts for each currency are broken down + further by + + payment source types. + + + Related guide: [Understanding Connect account + balances](https://stripe.com/docs/connect/account-balances) + properties: + available: + description: >- + Available funds that you can transfer or pay out automatically by + Stripe or explicitly through the [Transfers + API](https://stripe.com/docs/api#transfers) or [Payouts + API](https://stripe.com/docs/api#payouts). You can find the + available balance for each currency and payment type in the + `source_types` property. + items: + $ref: '#/components/schemas/balance_amount' + type: array + connect_reserved: + description: >- + Funds held due to negative balances on connected accounts where + [account.controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `application`, which includes Custom accounts. You can find the + connect reserve balance for each currency and payment type in the + `source_types` property. + items: + $ref: '#/components/schemas/balance_amount' + type: array + instant_available: + description: Funds that you can pay out using Instant Payouts. + items: + $ref: '#/components/schemas/balance_amount_net' + type: array + issuing: + $ref: '#/components/schemas/balance_detail' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - balance + type: string + pending: + description: >- + Funds that aren't available in the balance yet. You can find the + pending balance for each currency and each payment type in the + `source_types` property. + items: + $ref: '#/components/schemas/balance_amount' + type: array + refund_and_dispute_prefunding: + $ref: '#/components/schemas/balance_detail_ungated' + required: + - available + - livemode + - object + - pending + title: Balance + type: object + x-expandableFields: + - available + - connect_reserved + - instant_available + - issuing + - pending + - refund_and_dispute_prefunding + x-resourceId: balance + balance_amount: + description: '' + properties: + amount: + description: Balance amount. + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + source_types: + $ref: '#/components/schemas/balance_amount_by_source_type' + required: + - amount + - currency + title: BalanceAmount + type: object + x-expandableFields: + - source_types + balance_amount_by_source_type: + description: '' + properties: + bank_account: + description: >- + Amount coming from [legacy US ACH + payments](https://docs.stripe.com/ach-deprecated). + type: integer + card: + description: >- + Amount coming from most payment methods, including cards as well as + [non-legacy bank + debits](https://docs.stripe.com/payments/bank-debits). + type: integer + fpx: + description: >- + Amount coming from [FPX](https://docs.stripe.com/payments/fpx), a + Malaysian payment method. + type: integer + title: BalanceAmountBySourceType + type: object + x-expandableFields: [] + balance_amount_net: + description: '' + properties: + amount: + description: Balance amount. + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + net_available: + description: Breakdown of balance by destination. + items: + $ref: '#/components/schemas/balance_net_available' + type: array + source_types: + $ref: '#/components/schemas/balance_amount_by_source_type' + required: + - amount + - currency + title: BalanceAmountNet + type: object + x-expandableFields: + - net_available + - source_types + balance_detail: + description: '' + properties: + available: + description: Funds that are available for use. + items: + $ref: '#/components/schemas/balance_amount' + type: array + required: + - available + title: BalanceDetail + type: object + x-expandableFields: + - available + balance_detail_ungated: + description: '' + properties: + available: + description: Funds that are available for use. + items: + $ref: '#/components/schemas/balance_amount' + type: array + pending: + description: Funds that are pending + items: + $ref: '#/components/schemas/balance_amount' + type: array + required: + - available + - pending + title: BalanceDetailUngated + type: object + x-expandableFields: + - available + - pending + balance_net_available: + description: '' + properties: + amount: + description: 'Net balance amount, subtracting fees from platform-set pricing.' + type: integer + destination: + description: ID of the external account for this net balance (not expandable). + maxLength: 5000 + type: string + source_types: + $ref: '#/components/schemas/balance_amount_by_source_type' + required: + - amount + - destination + title: BalanceNetAvailable + type: object + x-expandableFields: + - source_types + balance_transaction: + description: >- + Balance transactions represent funds moving through your Stripe account. + + Stripe creates them for every type of transaction that enters or leaves + your Stripe account balance. + + + Related guide: [Balance transaction + types](https://stripe.com/docs/reports/balance-transaction-types) + properties: + amount: + description: >- + Gross amount of this transaction (in cents (or local equivalent)). A + positive value represents funds charged to another party, and a + negative value represents funds sent to another party. + type: integer + available_on: + description: >- + The date that the transaction's net funds become available in the + Stripe balance. + format: unix-time + type: integer + balance_type: + description: The balance that this transaction impacts. + enum: + - issuing + - payments + - refund_and_dispute_prefunding + type: string + x-stripeBypassValidation: true + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + exchange_rate: + description: >- + If applicable, this transaction uses an exchange rate. If money + converts from currency A to currency B, then the `amount` in + currency A, multipled by the `exchange_rate`, equals the `amount` in + currency B. For example, if you charge a customer 10.00 EUR, the + PaymentIntent's `amount` is `1000` and `currency` is `eur`. If this + converts to 12.34 USD in your Stripe account, the + BalanceTransaction's `amount` is `1234`, its `currency` is `usd`, + and the `exchange_rate` is `1.234`. + nullable: true + type: number + fee: + description: >- + Fees (in cents (or local equivalent)) paid for this transaction. + Represented as a positive integer when assessed. + type: integer + fee_details: + description: >- + Detailed breakdown of fees (in cents (or local equivalent)) paid for + this transaction. + items: + $ref: '#/components/schemas/fee' + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + net: + description: >- + Net impact to a Stripe balance (in cents (or local equivalent)). A + positive value represents incrementing a Stripe balance, and a + negative value decrementing a Stripe balance. You can calculate the + net impact of a transaction on a balance by `amount` - `fee` + type: integer + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - balance_transaction + type: string + reporting_category: + description: >- + Learn more about how [reporting + categories](https://stripe.com/docs/reports/reporting-categories) + can help you understand balance transactions from an accounting + perspective. + maxLength: 5000 + type: string + source: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application_fee' + - $ref: '#/components/schemas/charge' + - $ref: '#/components/schemas/connect_collection_transfer' + - $ref: '#/components/schemas/customer_cash_balance_transaction' + - $ref: '#/components/schemas/dispute' + - $ref: '#/components/schemas/fee_refund' + - $ref: '#/components/schemas/issuing.authorization' + - $ref: '#/components/schemas/issuing.dispute' + - $ref: '#/components/schemas/issuing.transaction' + - $ref: '#/components/schemas/payout' + - $ref: '#/components/schemas/refund' + - $ref: '#/components/schemas/reserve_transaction' + - $ref: '#/components/schemas/tax_deducted_at_source' + - $ref: '#/components/schemas/topup' + - $ref: '#/components/schemas/transfer' + - $ref: '#/components/schemas/transfer_reversal' + description: This transaction relates to the Stripe object. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application_fee' + - $ref: '#/components/schemas/charge' + - $ref: '#/components/schemas/connect_collection_transfer' + - $ref: '#/components/schemas/customer_cash_balance_transaction' + - $ref: '#/components/schemas/dispute' + - $ref: '#/components/schemas/fee_refund' + - $ref: '#/components/schemas/issuing.authorization' + - $ref: '#/components/schemas/issuing.dispute' + - $ref: '#/components/schemas/issuing.transaction' + - $ref: '#/components/schemas/payout' + - $ref: '#/components/schemas/refund' + - $ref: '#/components/schemas/reserve_transaction' + - $ref: '#/components/schemas/tax_deducted_at_source' + - $ref: '#/components/schemas/topup' + - $ref: '#/components/schemas/transfer' + - $ref: '#/components/schemas/transfer_reversal' + x-stripeBypassValidation: true + status: + description: >- + The transaction's net funds status in the Stripe balance, which are + either `available` or `pending`. + maxLength: 5000 + type: string + type: + description: >- + Transaction type: `adjustment`, `advance`, `advance_funding`, + `anticipation_repayment`, `application_fee`, + `application_fee_refund`, `charge`, `climate_order_purchase`, + `climate_order_refund`, `connect_collection_transfer`, + `contribution`, `issuing_authorization_hold`, + `issuing_authorization_release`, `issuing_dispute`, + `issuing_transaction`, `obligation_outbound`, + `obligation_reversal_inbound`, `payment`, `payment_failure_refund`, + `payment_network_reserve_hold`, `payment_network_reserve_release`, + `payment_refund`, `payment_reversal`, `payment_unreconciled`, + `payout`, `payout_cancel`, `payout_failure`, + `payout_minimum_balance_hold`, `payout_minimum_balance_release`, + `refund`, `refund_failure`, `reserve_transaction`, `reserved_funds`, + `stripe_fee`, `stripe_fx_fee`, `stripe_balance_payment_debit`, + `stripe_balance_payment_debit_reversal`, `tax_fee`, `topup`, + `topup_reversal`, `transfer`, `transfer_cancel`, `transfer_failure`, + or `transfer_refund`. Learn more about [balance transaction types + and what they + represent](https://stripe.com/docs/reports/balance-transaction-types). + To classify transactions for accounting purposes, consider + `reporting_category` instead. + enum: + - adjustment + - advance + - advance_funding + - anticipation_repayment + - application_fee + - application_fee_refund + - charge + - climate_order_purchase + - climate_order_refund + - connect_collection_transfer + - contribution + - issuing_authorization_hold + - issuing_authorization_release + - issuing_dispute + - issuing_transaction + - obligation_outbound + - obligation_reversal_inbound + - payment + - payment_failure_refund + - payment_network_reserve_hold + - payment_network_reserve_release + - payment_refund + - payment_reversal + - payment_unreconciled + - payout + - payout_cancel + - payout_failure + - payout_minimum_balance_hold + - payout_minimum_balance_release + - refund + - refund_failure + - reserve_transaction + - reserved_funds + - stripe_balance_payment_debit + - stripe_balance_payment_debit_reversal + - stripe_fee + - stripe_fx_fee + - tax_fee + - topup + - topup_reversal + - transfer + - transfer_cancel + - transfer_failure + - transfer_refund + type: string + required: + - amount + - available_on + - created + - currency + - fee + - fee_details + - id + - net + - object + - reporting_category + - status + - type + title: BalanceTransaction + type: object + x-expandableFields: + - fee_details + - source + x-resourceId: balance_transaction + bank_account: + description: >- + These bank accounts are payment methods on `Customer` objects. + + + On the other hand [External Accounts](/api#external_accounts) are + transfer + + destinations on `Account` objects for connected accounts. + + They can be bank accounts or debit cards as well, and are documented in + the links above. + + + Related guide: [Bank debits and + transfers](/payments/bank-debits-transfers) + properties: + account: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account this bank account belongs to. Only applicable on + Accounts (not customers or recipients) This property is only + available when returned as an [External + Account](/api/external_account_bank_accounts/object) where + [controller.is_controller](/api/accounts/object#account_object-controller-is_controller) + is `true`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + account_holder_name: + description: The name of the person or business that owns the bank account. + maxLength: 5000 + nullable: true + type: string + account_holder_type: + description: >- + The type of entity that holds the account. This can be either + `individual` or `company`. + maxLength: 5000 + nullable: true + type: string + account_type: + description: >- + The bank account type. This can only be `checking` or `savings` in + most countries. In Japan, this can only be `futsu` or `toza`. + maxLength: 5000 + nullable: true + type: string + available_payout_methods: + description: >- + A set of available payout methods for this bank account. Only values + from this set should be passed as the `method` when creating a + payout. + items: + enum: + - instant + - standard + type: string + nullable: true + type: array + bank_name: + description: >- + Name of the bank associated with the routing number (e.g., `WELLS + FARGO`). + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country the bank account is + located in. + maxLength: 5000 + type: string + currency: + description: >- + Three-letter [ISO code for the + currency](https://stripe.com/docs/payouts) paid out to the bank + account. + format: currency + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: The ID of the customer that the bank account is associated with. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + default_for_currency: + description: >- + Whether this bank account is the default external account for its + currency. + nullable: true + type: boolean + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + future_requirements: + anyOf: + - $ref: '#/components/schemas/external_account_requirements' + description: >- + Information about the [upcoming new requirements for the bank + account](https://stripe.com/docs/connect/custom-accounts/future-requirements), + including what information needs to be collected, and by when. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + last4: + description: The last four digits of the bank account number. + maxLength: 5000 + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - bank_account + type: string + requirements: + anyOf: + - $ref: '#/components/schemas/external_account_requirements' + description: >- + Information about the requirements for the bank account, including + what information needs to be collected. + nullable: true + routing_number: + description: The routing transit number for the bank account. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + For bank accounts, possible values are `new`, `validated`, + `verified`, `verification_failed`, or `errored`. A bank account that + hasn't had any activity or validation performed is `new`. If Stripe + can determine that the bank account exists, its status will be + `validated`. Note that there often isn’t enough information to know + (e.g., for smaller credit unions), and the validation is not always + run. If customer bank account verification has succeeded, the bank + account status will be `verified`. If the verification failed for + any reason, such as microdeposit failure, the status will be + `verification_failed`. If a payout sent to this bank account fails, + we'll set the status to `errored` and will not continue to send + [scheduled payouts](https://stripe.com/docs/payouts#payout-schedule) + until the bank details are updated. + + + For external accounts, possible values are `new`, `errored` and + `verification_failed`. If a payout fails, the status is set to + `errored` and scheduled payouts are stopped until account details + are updated. In the US and India, if we can't [verify the owner of + the bank + account](https://support.stripe.com/questions/bank-account-ownership-verification), + we'll set the status to `verification_failed`. Other validations + aren't run against external accounts because they're only used for + payouts. This means the other statuses don't apply. + maxLength: 5000 + type: string + required: + - country + - currency + - id + - last4 + - object + - status + title: BankAccount + type: object + x-expandableFields: + - account + - customer + - future_requirements + - requirements + x-resourceId: bank_account + bank_connections_resource_accountholder: + description: '' + properties: + account: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The ID of the Stripe account this account belongs to. Should only be + present if `account_holder.type` is `account`. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: >- + ID of the Stripe customer this account belongs to. Present if and + only if `account_holder.type` is `customer`. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + type: + description: Type of account holder that this account belongs to. + enum: + - account + - customer + type: string + required: + - type + title: BankConnectionsResourceAccountholder + type: object + x-expandableFields: + - account + - customer + bank_connections_resource_balance: + description: '' + properties: + as_of: + description: >- + The time that the external institution calculated this balance. + Measured in seconds since the Unix epoch. + format: unix-time + type: integer + cash: + $ref: >- + #/components/schemas/bank_connections_resource_balance_api_resource_cash_balance + credit: + $ref: >- + #/components/schemas/bank_connections_resource_balance_api_resource_credit_balance + current: + additionalProperties: + type: integer + description: >- + The balances owed to (or by) the account holder, before subtracting + any outbound pending transactions or adding any inbound pending + transactions. + + + Each key is a three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. + + + Each value is a integer amount. A positive amount indicates money + owed to the account holder. A negative amount indicates money owed + by the account holder. + type: object + type: + description: >- + The `type` of the balance. An additional hash is included on the + balance with a name matching this value. + enum: + - cash + - credit + type: string + required: + - as_of + - current + - type + title: BankConnectionsResourceBalance + type: object + x-expandableFields: + - cash + - credit + bank_connections_resource_balance_api_resource_cash_balance: + description: '' + properties: + available: + additionalProperties: + type: integer + description: >- + The funds available to the account holder. Typically this is the + current balance after subtracting any outbound pending transactions + and adding any inbound pending transactions. + + + Each key is a three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. + + + Each value is a integer amount. A positive amount indicates money + owed to the account holder. A negative amount indicates money owed + by the account holder. + nullable: true + type: object + title: BankConnectionsResourceBalanceAPIResourceCashBalance + type: object + x-expandableFields: [] + bank_connections_resource_balance_api_resource_credit_balance: + description: '' + properties: + used: + additionalProperties: + type: integer + description: >- + The credit that has been used by the account holder. + + + Each key is a three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. + + + Each value is a integer amount. A positive amount indicates money + owed to the account holder. A negative amount indicates money owed + by the account holder. + nullable: true + type: object + title: BankConnectionsResourceBalanceAPIResourceCreditBalance + type: object + x-expandableFields: [] + bank_connections_resource_balance_refresh: + description: '' + properties: + last_attempted_at: + description: >- + The time at which the last refresh attempt was initiated. Measured + in seconds since the Unix epoch. + format: unix-time + type: integer + next_refresh_available_at: + description: >- + Time at which the next balance refresh can be initiated. This value + will be `null` when `status` is `pending`. Measured in seconds since + the Unix epoch. + format: unix-time + nullable: true + type: integer + status: + description: The status of the last refresh attempt. + enum: + - failed + - pending + - succeeded + type: string + required: + - last_attempted_at + - status + title: BankConnectionsResourceBalanceRefresh + type: object + x-expandableFields: [] + bank_connections_resource_link_account_session_filters: + description: '' + properties: + account_subcategories: + description: >- + Restricts the Session to subcategories of accounts that can be + linked. Valid subcategories are: `checking`, `savings`, `mortgage`, + `line_of_credit`, `credit_card`. + items: + enum: + - checking + - credit_card + - line_of_credit + - mortgage + - savings + type: string + nullable: true + type: array + countries: + description: List of countries from which to filter accounts. + items: + maxLength: 5000 + type: string + nullable: true + type: array + title: BankConnectionsResourceLinkAccountSessionFilters + type: object + x-expandableFields: [] + bank_connections_resource_ownership_refresh: + description: '' + properties: + last_attempted_at: + description: >- + The time at which the last refresh attempt was initiated. Measured + in seconds since the Unix epoch. + format: unix-time + type: integer + next_refresh_available_at: + description: >- + Time at which the next ownership refresh can be initiated. This + value will be `null` when `status` is `pending`. Measured in seconds + since the Unix epoch. + format: unix-time + nullable: true + type: integer + status: + description: The status of the last refresh attempt. + enum: + - failed + - pending + - succeeded + type: string + required: + - last_attempted_at + - status + title: BankConnectionsResourceOwnershipRefresh + type: object + x-expandableFields: [] + bank_connections_resource_transaction_refresh: + description: '' + properties: + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + last_attempted_at: + description: >- + The time at which the last refresh attempt was initiated. Measured + in seconds since the Unix epoch. + format: unix-time + type: integer + next_refresh_available_at: + description: >- + Time at which the next transaction refresh can be initiated. This + value will be `null` when `status` is `pending`. Measured in seconds + since the Unix epoch. + format: unix-time + nullable: true + type: integer + status: + description: The status of the last refresh attempt. + enum: + - failed + - pending + - succeeded + type: string + required: + - id + - last_attempted_at + - status + title: BankConnectionsResourceTransactionRefresh + type: object + x-expandableFields: [] + bank_connections_resource_transaction_resource_status_transitions: + description: '' + properties: + posted_at: + description: >- + Time at which this transaction posted. Measured in seconds since the + Unix epoch. + format: unix-time + nullable: true + type: integer + void_at: + description: >- + Time at which this transaction was voided. Measured in seconds since + the Unix epoch. + format: unix-time + nullable: true + type: integer + title: BankConnectionsResourceTransactionResourceStatusTransitions + type: object + x-expandableFields: [] + billing.alert: + description: >- + A billing alert is a resource that notifies you when a certain usage + threshold on a meter is crossed. For example, you might create a billing + alert to notify you when a certain user made 100 API requests. + properties: + alert_type: + description: Defines the type of the alert. + enum: + - usage_threshold + type: string + x-stripeBypassValidation: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing.alert + type: string + status: + description: 'Status of the alert. This can be active, inactive or archived.' + enum: + - active + - archived + - inactive + nullable: true + type: string + title: + description: Title of the alert. + maxLength: 5000 + type: string + usage_threshold: + anyOf: + - $ref: '#/components/schemas/thresholds_resource_usage_threshold_config' + description: >- + Encapsulates configuration of the alert to monitor usage on a + specific [Billing Meter](https://stripe.com/docs/api/billing/meter). + nullable: true + required: + - alert_type + - id + - livemode + - object + - title + title: ThresholdsResourceAlert + type: object + x-expandableFields: + - usage_threshold + x-resourceId: billing.alert + billing.credit_balance_summary: + description: >- + Indicates the billing credit balance for billing credits granted to a + customer. + properties: + balances: + description: >- + The billing credit balances. One entry per credit grant currency. If + a customer only has credit grants in a single currency, then this + will have a single balance entry. + items: + $ref: '#/components/schemas/credit_balance' + type: array + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: The customer the balance is for. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing.credit_balance_summary + type: string + required: + - balances + - customer + - livemode + - object + title: CreditBalanceSummary + type: object + x-expandableFields: + - balances + - customer + x-resourceId: billing.credit_balance_summary + billing.credit_balance_transaction: + description: >- + A credit balance transaction is a resource representing a transaction + (either a credit or a debit) against an existing credit grant. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + credit: + anyOf: + - $ref: >- + #/components/schemas/billing_credit_grants_resource_balance_credit + description: >- + Credit details for this credit balance transaction. Only present if + type is `credit`. + nullable: true + credit_grant: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/billing.credit_grant' + description: The credit grant associated with this credit balance transaction. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/billing.credit_grant' + debit: + anyOf: + - $ref: >- + #/components/schemas/billing_credit_grants_resource_balance_debit + description: >- + Debit details for this credit balance transaction. Only present if + type is `debit`. + nullable: true + effective_at: + description: The effective time of this credit balance transaction. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing.credit_balance_transaction + type: string + test_clock: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/test_helpers.test_clock' + description: ID of the test clock this credit balance transaction belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/test_helpers.test_clock' + type: + description: The type of credit balance transaction (credit or debit). + enum: + - credit + - debit + nullable: true + type: string + required: + - created + - credit_grant + - effective_at + - id + - livemode + - object + title: CreditBalanceTransaction + type: object + x-expandableFields: + - credit + - credit_grant + - debit + - test_clock + x-resourceId: billing.credit_balance_transaction + billing.credit_grant: + description: >- + A credit grant is an API resource that documents the allocation of some + billing credits to a customer. + + + Related guide: [Billing + credits](https://docs.stripe.com/billing/subscriptions/usage-based/billing-credits) + properties: + amount: + $ref: '#/components/schemas/billing_credit_grants_resource_amount' + applicability_config: + $ref: >- + #/components/schemas/billing_credit_grants_resource_applicability_config + category: + description: >- + The category of this credit grant. This is for tracking purposes and + isn't displayed to the customer. + enum: + - paid + - promotional + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: ID of the customer receiving the billing credits. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + effective_at: + description: >- + The time when the billing credits become effective-when they're + eligible for use. + format: unix-time + nullable: true + type: integer + expires_at: + description: >- + The time when the billing credits expire. If not present, the + billing credits don't expire. + format: unix-time + nullable: true + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + name: + description: A descriptive name shown in dashboard. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing.credit_grant + type: string + priority: + description: >- + The priority for applying this credit grant. The highest priority is + 0 and the lowest is 100. + nullable: true + type: integer + test_clock: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/test_helpers.test_clock' + description: ID of the test clock this credit grant belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/test_helpers.test_clock' + updated: + description: >- + Time at which the object was last updated. Measured in seconds since + the Unix epoch. + format: unix-time + type: integer + voided_at: + description: >- + The time when this credit grant was voided. If not present, the + credit grant hasn't been voided. + format: unix-time + nullable: true + type: integer + required: + - amount + - applicability_config + - category + - created + - customer + - id + - livemode + - metadata + - object + - updated + title: CreditGrant + type: object + x-expandableFields: + - amount + - applicability_config + - customer + - test_clock + x-resourceId: billing.credit_grant + billing.meter: + description: >- + Meters specify how to aggregate meter events over a billing period. + Meter events represent the actions that customers take in your system. + Meters attach to prices and form the basis of the bill. + + + Related guide: [Usage based + billing](https://docs.stripe.com/billing/subscriptions/usage-based) + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer_mapping: + $ref: >- + #/components/schemas/billing_meter_resource_customer_mapping_settings + default_aggregation: + $ref: '#/components/schemas/billing_meter_resource_aggregation_settings' + display_name: + description: The meter's name. + maxLength: 5000 + type: string + event_name: + description: >- + The name of the meter event to record usage for. Corresponds with + the `event_name` field on meter events. + maxLength: 5000 + type: string + event_time_window: + description: 'The time window to pre-aggregate meter events for, if any.' + enum: + - day + - hour + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing.meter + type: string + status: + description: The meter's status. + enum: + - active + - inactive + type: string + status_transitions: + $ref: >- + #/components/schemas/billing_meter_resource_billing_meter_status_transitions + updated: + description: >- + Time at which the object was last updated. Measured in seconds since + the Unix epoch. + format: unix-time + type: integer + value_settings: + $ref: '#/components/schemas/billing_meter_resource_billing_meter_value' + required: + - created + - customer_mapping + - default_aggregation + - display_name + - event_name + - id + - livemode + - object + - status + - status_transitions + - updated + - value_settings + title: BillingMeter + type: object + x-expandableFields: + - customer_mapping + - default_aggregation + - status_transitions + - value_settings + x-resourceId: billing.meter + billing.meter_event: + description: >- + Meter events represent actions that customers take in your system. You + can use meter events to bill a customer based on their usage. Meter + events are associated with billing meters, which define both the + contents of the event’s payload and how to aggregate those events. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + event_name: + description: >- + The name of the meter event. Corresponds with the `event_name` field + on a meter. + maxLength: 100 + type: string + identifier: + description: A unique identifier for the event. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing.meter_event + type: string + payload: + additionalProperties: + maxLength: 100 + type: string + description: >- + The payload of the event. This contains the fields corresponding to + a meter's `customer_mapping.event_payload_key` (default is + `stripe_customer_id`) and `value_settings.event_payload_key` + (default is `value`). Read more about the + [payload](https://stripe.com/docs/billing/subscriptions/usage-based/recording-usage#payload-key-overrides). + type: object + timestamp: + description: >- + The timestamp passed in when creating the event. Measured in seconds + since the Unix epoch. + format: unix-time + type: integer + required: + - created + - event_name + - identifier + - livemode + - object + - payload + - timestamp + title: BillingMeterEvent + type: object + x-expandableFields: [] + x-resourceId: billing.meter_event + billing.meter_event_adjustment: + description: >- + A billing meter event adjustment is a resource that allows you to cancel + a meter event. For example, you might create a billing meter event + adjustment to cancel a meter event that was created in error or attached + to the wrong customer. + properties: + cancel: + anyOf: + - $ref: >- + #/components/schemas/billing_meter_resource_billing_meter_event_adjustment_cancel + description: Specifies which event to cancel. + nullable: true + event_name: + description: >- + The name of the meter event. Corresponds with the `event_name` field + on a meter. + maxLength: 100 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing.meter_event_adjustment + type: string + status: + description: The meter event adjustment's status. + enum: + - complete + - pending + type: string + type: + description: >- + Specifies whether to cancel a single event or a range of events for + a time period. Time period cancellation is not supported yet. + enum: + - cancel + type: string + required: + - event_name + - livemode + - object + - status + - type + title: BillingMeterEventAdjustment + type: object + x-expandableFields: + - cancel + x-resourceId: billing.meter_event_adjustment + billing.meter_event_summary: + description: >- + A billing meter event summary represents an aggregated view of a + customer's billing meter events within a specified timeframe. It + indicates how much + + usage was accrued by a customer for that period. + + + Note: Meters events are aggregated asynchronously so the meter event + summaries provide an eventually consistent view of the reported usage. + properties: + aggregated_value: + description: >- + Aggregated value of all the events within `start_time` (inclusive) + and `end_time` (inclusive). The aggregation strategy is defined on + meter via `default_aggregation`. + type: number + end_time: + description: >- + End timestamp for this event summary (exclusive). Must be aligned + with minute boundaries. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + meter: + description: The meter associated with this event summary. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing.meter_event_summary + type: string + start_time: + description: >- + Start timestamp for this event summary (inclusive). Must be aligned + with minute boundaries. + format: unix-time + type: integer + required: + - aggregated_value + - end_time + - id + - livemode + - meter + - object + - start_time + title: BillingMeterEventSummary + type: object + x-expandableFields: [] + x-resourceId: billing.meter_event_summary + billing_bill_resource_invoice_item_parents_invoice_item_parent: + description: '' + properties: + subscription_details: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoice_item_parents_invoice_item_subscription_parent + description: Details about the subscription that generated this invoice item + nullable: true + type: + description: The type of parent that generated this invoice item + enum: + - subscription_details + type: string + x-stripeBypassValidation: true + required: + - type + title: BillingBillResourceInvoiceItemParentsInvoiceItemParent + type: object + x-expandableFields: + - subscription_details + billing_bill_resource_invoice_item_parents_invoice_item_subscription_parent: + description: '' + properties: + subscription: + description: The subscription that generated this invoice item + maxLength: 5000 + type: string + subscription_item: + description: The subscription item that generated this invoice item + maxLength: 5000 + type: string + required: + - subscription + title: BillingBillResourceInvoiceItemParentsInvoiceItemSubscriptionParent + type: object + x-expandableFields: [] + billing_bill_resource_invoicing_lines_common_credited_items: + description: '' + properties: + invoice: + description: Invoice containing the credited invoice line items + maxLength: 5000 + type: string + invoice_line_items: + description: Credited invoice line items + items: + maxLength: 5000 + type: string + type: array + required: + - invoice + - invoice_line_items + title: BillingBillResourceInvoicingLinesCommonCreditedItems + type: object + x-expandableFields: [] + billing_bill_resource_invoicing_lines_common_proration_details: + description: '' + properties: + credited_items: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_lines_common_credited_items + description: >- + For a credit proration `line_item`, the original debit line_items to + which the credit proration applies. + nullable: true + title: BillingBillResourceInvoicingLinesCommonProrationDetails + type: object + x-expandableFields: + - credited_items + billing_bill_resource_invoicing_lines_parents_invoice_line_item_invoice_item_parent: + description: '' + properties: + invoice_item: + description: The invoice item that generated this line item + maxLength: 5000 + type: string + proration: + description: Whether this is a proration + type: boolean + proration_details: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_lines_common_proration_details + description: Additional details for proration line items + nullable: true + subscription: + description: The subscription that the invoice item belongs to + maxLength: 5000 + nullable: true + type: string + required: + - invoice_item + - proration + title: BillingBillResourceInvoicingLinesParentsInvoiceLineItemInvoiceItemParent + type: object + x-expandableFields: + - proration_details + billing_bill_resource_invoicing_lines_parents_invoice_line_item_parent: + description: '' + properties: + invoice_item_details: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_lines_parents_invoice_line_item_invoice_item_parent + description: Details about the invoice item that generated this line item + nullable: true + subscription_item_details: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_lines_parents_invoice_line_item_subscription_item_parent + description: Details about the subscription item that generated this line item + nullable: true + type: + description: The type of parent that generated this line item + enum: + - invoice_item_details + - subscription_item_details + type: string + x-stripeBypassValidation: true + required: + - type + title: BillingBillResourceInvoicingLinesParentsInvoiceLineItemParent + type: object + x-expandableFields: + - invoice_item_details + - subscription_item_details + billing_bill_resource_invoicing_lines_parents_invoice_line_item_subscription_item_parent: + description: '' + properties: + invoice_item: + description: The invoice item that generated this line item + maxLength: 5000 + nullable: true + type: string + proration: + description: Whether this is a proration + type: boolean + proration_details: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_lines_common_proration_details + description: Additional details for proration line items + nullable: true + subscription: + description: The subscription that the subscription item belongs to + maxLength: 5000 + nullable: true + type: string + subscription_item: + description: The subscription item that generated this line item + maxLength: 5000 + type: string + required: + - proration + - subscription_item + title: >- + BillingBillResourceInvoicingLinesParentsInvoiceLineItemSubscriptionItemParent + type: object + x-expandableFields: + - proration_details + billing_bill_resource_invoicing_parents_invoice_parent: + description: '' + properties: + quote_details: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_parents_invoice_quote_parent + description: Details about the quote that generated this invoice + nullable: true + subscription_details: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_parents_invoice_subscription_parent + description: Details about the subscription that generated this invoice + nullable: true + type: + description: The type of parent that generated this invoice + enum: + - quote_details + - subscription_details + type: string + x-stripeBypassValidation: true + required: + - type + title: BillingBillResourceInvoicingParentsInvoiceParent + type: object + x-expandableFields: + - quote_details + - subscription_details + billing_bill_resource_invoicing_parents_invoice_quote_parent: + description: '' + properties: + quote: + description: The quote that generated this invoice + maxLength: 5000 + type: string + required: + - quote + title: BillingBillResourceInvoicingParentsInvoiceQuoteParent + type: object + x-expandableFields: [] + billing_bill_resource_invoicing_parents_invoice_subscription_parent: + description: '' + properties: + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) + defined as subscription metadata when an invoice is created. Becomes + an immutable snapshot of the subscription metadata at the time of + invoice finalization. + *Note: This attribute is populated only for invoices created on or after June 29, 2023.* + nullable: true + type: object + subscription: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/subscription' + description: The subscription that generated this invoice + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/subscription' + subscription_proration_date: + description: >- + Only set for upcoming invoices that preview prorations. The time + used to calculate prorations. + format: unix-time + type: integer + required: + - subscription + title: BillingBillResourceInvoicingParentsInvoiceSubscriptionParent + type: object + x-expandableFields: + - subscription + billing_bill_resource_invoicing_pricing_pricing: + description: '' + properties: + price_details: + $ref: >- + #/components/schemas/billing_bill_resource_invoicing_pricing_pricing_price_details + type: + description: The type of the pricing details. + enum: + - price_details + type: string + x-stripeBypassValidation: true + unit_amount_decimal: + description: >- + The unit amount (in the `currency` specified) of the item which + contains a decimal value with at most 12 decimal places. + format: decimal + nullable: true + type: string + required: + - type + title: BillingBillResourceInvoicingPricingPricing + type: object + x-expandableFields: + - price_details + billing_bill_resource_invoicing_pricing_pricing_price_details: + description: '' + properties: + price: + description: The ID of the price this item is associated with. + maxLength: 5000 + type: string + product: + description: The ID of the product this item is associated with. + maxLength: 5000 + type: string + required: + - price + - product + title: BillingBillResourceInvoicingPricingPricingPriceDetails + type: object + x-expandableFields: [] + billing_bill_resource_invoicing_taxes_tax: + description: '' + properties: + amount: + description: 'The amount of the tax, in cents (or local equivalent).' + type: integer + tax_behavior: + description: Whether this tax is inclusive or exclusive. + enum: + - exclusive + - inclusive + type: string + tax_rate_details: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_taxes_tax_rate_details + description: >- + Additional details about the tax rate. Only present when `type` is + `tax_rate_details`. + nullable: true + taxability_reason: + description: >- + The reasoning behind this tax, for example, if the product is tax + exempt. The possible values for this field may be extended as new + tax rules are supported. + enum: + - customer_exempt + - not_available + - not_collecting + - not_subject_to_tax + - not_supported + - portion_product_exempt + - portion_reduced_rated + - portion_standard_rated + - product_exempt + - product_exempt_holiday + - proportionally_rated + - reduced_rated + - reverse_charge + - standard_rated + - taxable_basis_reduced + - zero_rated + type: string + x-stripeBypassValidation: true + taxable_amount: + description: >- + The amount on which tax is calculated, in cents (or local + equivalent). + nullable: true + type: integer + type: + description: The type of tax information. + enum: + - tax_rate_details + type: string + required: + - amount + - tax_behavior + - taxability_reason + - type + title: BillingBillResourceInvoicingTaxesTax + type: object + x-expandableFields: + - tax_rate_details + billing_bill_resource_invoicing_taxes_tax_rate_details: + description: '' + properties: + tax_rate: + maxLength: 5000 + type: string + required: + - tax_rate + title: BillingBillResourceInvoicingTaxesTaxRateDetails + type: object + x-expandableFields: [] + billing_clocks_resource_status_details_advancing_status_details: + description: '' + properties: + target_frozen_time: + description: The `frozen_time` that the Test Clock is advancing towards. + format: unix-time + type: integer + required: + - target_frozen_time + title: BillingClocksResourceStatusDetailsAdvancingStatusDetails + type: object + x-expandableFields: [] + billing_clocks_resource_status_details_status_details: + description: '' + properties: + advancing: + $ref: >- + #/components/schemas/billing_clocks_resource_status_details_advancing_status_details + title: BillingClocksResourceStatusDetailsStatusDetails + type: object + x-expandableFields: + - advancing + billing_credit_grants_resource_amount: + description: '' + properties: + monetary: + anyOf: + - $ref: >- + #/components/schemas/billing_credit_grants_resource_monetary_amount + description: The monetary amount. + nullable: true + type: + description: >- + The type of this amount. We currently only support `monetary` + billing credits. + enum: + - monetary + type: string + required: + - type + title: BillingCreditGrantsResourceAmount + type: object + x-expandableFields: + - monetary + billing_credit_grants_resource_applicability_config: + description: '' + properties: + scope: + $ref: '#/components/schemas/billing_credit_grants_resource_scope' + required: + - scope + title: BillingCreditGrantsResourceApplicabilityConfig + type: object + x-expandableFields: + - scope + billing_credit_grants_resource_applicable_price: + description: '' + properties: + id: + description: Unique identifier for the object. + maxLength: 5000 + nullable: true + type: string + title: BillingCreditGrantsResourceApplicablePrice + type: object + x-expandableFields: [] + billing_credit_grants_resource_balance_credit: + description: '' + properties: + amount: + $ref: '#/components/schemas/billing_credit_grants_resource_amount' + credits_application_invoice_voided: + anyOf: + - $ref: >- + #/components/schemas/billing_credit_grants_resource_balance_credits_application_invoice_voided + description: >- + Details of the invoice to which the reinstated credits were + originally applied. Only present if `type` is + `credits_application_invoice_voided`. + nullable: true + type: + description: The type of credit transaction. + enum: + - credits_application_invoice_voided + - credits_granted + type: string + required: + - amount + - type + title: BillingCreditGrantsResourceBalanceCredit + type: object + x-expandableFields: + - amount + - credits_application_invoice_voided + billing_credit_grants_resource_balance_credits_application_invoice_voided: + description: '' + properties: + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: >- + The invoice to which the reinstated billing credits were originally + applied. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + invoice_line_item: + description: >- + The invoice line item to which the reinstated billing credits were + originally applied. + maxLength: 5000 + type: string + required: + - invoice + - invoice_line_item + title: BillingCreditGrantsResourceBalanceCreditsApplicationInvoiceVoided + type: object + x-expandableFields: + - invoice + billing_credit_grants_resource_balance_credits_applied: + description: '' + properties: + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: The invoice to which the billing credits were applied. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + invoice_line_item: + description: The invoice line item to which the billing credits were applied. + maxLength: 5000 + type: string + required: + - invoice + - invoice_line_item + title: BillingCreditGrantsResourceBalanceCreditsApplied + type: object + x-expandableFields: + - invoice + billing_credit_grants_resource_balance_debit: + description: '' + properties: + amount: + $ref: '#/components/schemas/billing_credit_grants_resource_amount' + credits_applied: + anyOf: + - $ref: >- + #/components/schemas/billing_credit_grants_resource_balance_credits_applied + description: >- + Details of how the billing credits were applied to an invoice. Only + present if `type` is `credits_applied`. + nullable: true + type: + description: The type of debit transaction. + enum: + - credits_applied + - credits_expired + - credits_voided + type: string + required: + - amount + - type + title: BillingCreditGrantsResourceBalanceDebit + type: object + x-expandableFields: + - amount + - credits_applied + billing_credit_grants_resource_monetary_amount: + description: '' + properties: + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + value: + description: A positive integer representing the amount. + type: integer + required: + - currency + - value + title: BillingCreditGrantsResourceMonetaryAmount + type: object + x-expandableFields: [] + billing_credit_grants_resource_scope: + description: '' + properties: + price_type: + description: >- + The price type that credit grants can apply to. We currently only + support the `metered` price type. This refers to prices that have a + [Billing Meter](https://docs.stripe.com/api/billing/meter) attached + to them. Cannot be used in combination with `prices`. + enum: + - metered + type: string + prices: + description: >- + The prices that credit grants can apply to. We currently only + support `metered` prices. This refers to prices that have a [Billing + Meter](https://docs.stripe.com/api/billing/meter) attached to them. + Cannot be used in combination with `price_type`. + items: + $ref: >- + #/components/schemas/billing_credit_grants_resource_applicable_price + type: array + title: BillingCreditGrantsResourceScope + type: object + x-expandableFields: + - prices + billing_details: + description: '' + properties: + address: + anyOf: + - $ref: '#/components/schemas/address' + description: Billing address. + nullable: true + email: + description: Email address. + maxLength: 5000 + nullable: true + type: string + name: + description: Full name. + maxLength: 5000 + nullable: true + type: string + phone: + description: Billing phone number (including extension). + maxLength: 5000 + nullable: true + type: string + tax_id: + description: >- + Taxpayer identification number. Used only for transactions between + LATAM buyers and non-LATAM sellers. + maxLength: 5000 + nullable: true + type: string + title: billing_details + type: object + x-expandableFields: + - address + billing_meter_resource_aggregation_settings: + description: '' + properties: + formula: + description: Specifies how events are aggregated. + enum: + - count + - last + - sum + type: string + x-stripeBypassValidation: true + required: + - formula + title: BillingMeterResourceAggregationSettings + type: object + x-expandableFields: [] + billing_meter_resource_billing_meter_event_adjustment_cancel: + description: '' + properties: + identifier: + description: Unique identifier for the event. + maxLength: 100 + nullable: true + type: string + title: BillingMeterResourceBillingMeterEventAdjustmentCancel + type: object + x-expandableFields: [] + billing_meter_resource_billing_meter_status_transitions: + description: '' + properties: + deactivated_at: + description: >- + The time the meter was deactivated, if any. Measured in seconds + since Unix epoch. + format: unix-time + nullable: true + type: integer + title: BillingMeterResourceBillingMeterStatusTransitions + type: object + x-expandableFields: [] + billing_meter_resource_billing_meter_value: + description: '' + properties: + event_payload_key: + description: >- + The key in the meter event payload to use as the value for this + meter. + maxLength: 5000 + type: string + required: + - event_payload_key + title: BillingMeterResourceBillingMeterValue + type: object + x-expandableFields: [] + billing_meter_resource_customer_mapping_settings: + description: '' + properties: + event_payload_key: + description: >- + The key in the meter event payload to use for mapping the event to a + customer. + maxLength: 5000 + type: string + type: + description: The method for mapping a meter event to a customer. + enum: + - by_id + type: string + required: + - event_payload_key + - type + title: BillingMeterResourceCustomerMappingSettings + type: object + x-expandableFields: [] + billing_portal.configuration: + description: >- + A portal configuration describes the functionality and behavior of a + portal session. + properties: + active: + description: >- + Whether the configuration is active and can be used to create portal + sessions. + type: boolean + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + description: ID of the Connect Application that created the configuration. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + business_profile: + $ref: '#/components/schemas/portal_business_profile' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + default_return_url: + description: >- + The default URL to redirect customers to when they click on the + portal's link to return to your website. This can be + [overriden](https://stripe.com/docs/api/customer_portal/sessions/create#create_portal_session-return_url) + when creating the session. + maxLength: 5000 + nullable: true + type: string + features: + $ref: '#/components/schemas/portal_features' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + is_default: + description: >- + Whether the configuration is the default. If `true`, this + configuration can be managed in the Dashboard and portal sessions + will use this configuration unless it is overriden when creating the + session. + type: boolean + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + login_page: + $ref: '#/components/schemas/portal_login_page' + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing_portal.configuration + type: string + updated: + description: >- + Time at which the object was last updated. Measured in seconds since + the Unix epoch. + format: unix-time + type: integer + required: + - active + - business_profile + - created + - features + - id + - is_default + - livemode + - login_page + - object + - updated + title: PortalConfiguration + type: object + x-expandableFields: + - application + - business_profile + - features + - login_page + x-resourceId: billing_portal.configuration + billing_portal.session: + description: >- + The Billing customer portal is a Stripe-hosted UI for subscription and + + billing management. + + + A portal configuration describes the functionality and features that you + + want to provide to your customers through the portal. + + + A portal session describes the instantiation of the customer portal for + + a particular customer. By visiting the session's URL, the customer + + can manage their subscriptions and billing details. For security + reasons, + + sessions are short-lived and will expire if the customer does not visit + the URL. + + Create sessions on-demand when customers intend to manage their + subscriptions + + and billing details. + + + Related guide: [Customer management](/customer-management) + properties: + configuration: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/billing_portal.configuration' + description: >- + The configuration used by this session, describing the features + available. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/billing_portal.configuration' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer: + description: The ID of the customer for this session. + maxLength: 5000 + type: string + flow: + anyOf: + - $ref: '#/components/schemas/portal_flows_flow' + description: >- + Information about a specific flow for the customer to go through. + See the + [docs](https://stripe.com/docs/customer-management/portal-deep-links) + to learn more about using customer portal deep links and flows. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + locale: + description: >- + The IETF language tag of the locale Customer Portal is displayed in. + If blank or auto, the customer’s `preferred_locales` or browser’s + locale is used. + enum: + - auto + - bg + - cs + - da + - de + - el + - en + - en-AU + - en-CA + - en-GB + - en-IE + - en-IN + - en-NZ + - en-SG + - es + - es-419 + - et + - fi + - fil + - fr + - fr-CA + - hr + - hu + - id + - it + - ja + - ko + - lt + - lv + - ms + - mt + - nb + - nl + - pl + - pt + - pt-BR + - ro + - ru + - sk + - sl + - sv + - th + - tr + - vi + - zh + - zh-HK + - zh-TW + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - billing_portal.session + type: string + on_behalf_of: + description: >- + The account for which the session was created on behalf of. When + specified, only subscriptions and invoices with this `on_behalf_of` + account appear in the portal. For more information, see the + [docs](https://stripe.com/docs/connect/separate-charges-and-transfers#settlement-merchant). + Use the [Accounts + API](https://stripe.com/docs/api/accounts/object#account_object-settings-branding) + to modify the `on_behalf_of` account's branding settings, which the + portal displays. + maxLength: 5000 + nullable: true + type: string + return_url: + description: >- + The URL to redirect customers to when they click on the portal's + link to return to your website. + maxLength: 5000 + nullable: true + type: string + url: + description: >- + The short-lived URL of the session that gives customers access to + the customer portal. + maxLength: 5000 + type: string + required: + - configuration + - created + - customer + - id + - livemode + - object + - url + title: PortalSession + type: object + x-expandableFields: + - configuration + - flow + x-resourceId: billing_portal.session + cancellation_details: + description: '' + properties: + comment: + description: >- + Additional comments about why the user canceled the subscription, if + the subscription was canceled explicitly by the user. + maxLength: 5000 + nullable: true + type: string + feedback: + description: >- + The customer submitted reason for why they canceled, if the + subscription was canceled explicitly by the user. + enum: + - customer_service + - low_quality + - missing_features + - other + - switched_service + - too_complex + - too_expensive + - unused + nullable: true + type: string + reason: + description: Why this subscription was canceled. + enum: + - cancellation_requested + - payment_disputed + - payment_failed + nullable: true + type: string + title: CancellationDetails + type: object + x-expandableFields: [] + capability: + description: >- + This is an object representing a capability for a Stripe account. + + + Related guide: [Account + capabilities](https://stripe.com/docs/connect/account-capabilities) + properties: + account: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: The account for which the capability enables functionality. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + future_requirements: + $ref: '#/components/schemas/account_capability_future_requirements' + id: + description: The identifier for the capability. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - capability + type: string + requested: + description: Whether the capability has been requested. + type: boolean + requested_at: + description: >- + Time at which the capability was requested. Measured in seconds + since the Unix epoch. + format: unix-time + nullable: true + type: integer + requirements: + $ref: '#/components/schemas/account_capability_requirements' + status: + description: The status of the capability. + enum: + - active + - disabled + - inactive + - pending + - unrequested + type: string + required: + - account + - id + - object + - requested + - status + title: AccountCapability + type: object + x-expandableFields: + - account + - future_requirements + - requirements + x-resourceId: capability + card: + description: >- + You can store multiple cards on a customer in order to charge the + customer + + later. You can also store multiple debit cards on a recipient in order + to + + transfer to those cards later. + + + Related guide: [Card payments with + Sources](https://stripe.com/docs/sources/cards) + properties: + account: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + address_city: + description: City/District/Suburb/Town/Village. + maxLength: 5000 + nullable: true + type: string + address_country: + description: 'Billing address country, if provided when creating card.' + maxLength: 5000 + nullable: true + type: string + address_line1: + description: Address line 1 (Street address/PO Box/Company name). + maxLength: 5000 + nullable: true + type: string + address_line1_check: + description: >- + If `address_line1` was provided, results of the check: `pass`, + `fail`, `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + address_line2: + description: Address line 2 (Apartment/Suite/Unit/Building). + maxLength: 5000 + nullable: true + type: string + address_state: + description: State/County/Province/Region. + maxLength: 5000 + nullable: true + type: string + address_zip: + description: ZIP or postal code. + maxLength: 5000 + nullable: true + type: string + address_zip_check: + description: >- + If `address_zip` was provided, results of the check: `pass`, `fail`, + `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + allow_redisplay: + description: >- + This field indicates whether this payment method can be shown again + to its customer in a checkout flow. Stripe products such as Checkout + and Elements use this field to determine whether a payment method + can be shown as a saved payment method in a checkout flow. The field + defaults to “unspecified”. + enum: + - always + - limited + - unspecified + nullable: true + type: string + available_payout_methods: + description: >- + A set of available payout methods for this card. Only values from + this set should be passed as the `method` when creating a payout. + items: + enum: + - instant + - standard + type: string + nullable: true + type: array + brand: + description: >- + Card brand. Can be `American Express`, `Diners Club`, `Discover`, + `Eftpos Australia`, `Girocard`, `JCB`, `MasterCard`, `UnionPay`, + `Visa`, or `Unknown`. + maxLength: 5000 + type: string + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + currency: + description: >- + Three-letter [ISO code for + currency](https://www.iso.org/iso-4217-currency-codes.html) in + lowercase. Must be a [supported + currency](https://docs.stripe.com/currencies). Only applicable on + accounts (not customers or recipients). The card can be used as a + transfer destination for funds in this currency. This property is + only available when returned as an [External + Account](/api/external_account_cards/object) where + [controller.is_controller](/api/accounts/object#account_object-controller-is_controller) + is `true`. + format: currency + nullable: true + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: >- + The customer that this card belongs to. This attribute will not be + in the card object if the card belongs to an account or recipient + instead. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + cvc_check: + description: >- + If a CVC was provided, results of the check: `pass`, `fail`, + `unavailable`, or `unchecked`. A result of unchecked indicates that + CVC was provided but hasn't been checked yet. Checks are typically + performed when attaching a card to a Customer object, or when + creating a charge. For more details, see [Check if a card is valid + without a + charge](https://support.stripe.com/questions/check-if-a-card-is-valid-without-a-charge). + maxLength: 5000 + nullable: true + type: string + default_for_currency: + description: >- + Whether this card is the default external account for its currency. + This property is only available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `application`, which includes Custom accounts. + nullable: true + type: boolean + dynamic_last4: + description: >- + (For tokenized numbers only.) The last four digits of the device + account number. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + type: integer + fingerprint: + description: >- + Uniquely identifies this particular card number. You can use this + attribute to check whether two customers who’ve signed up with you + are using the same card number, for example. For payment methods + that tokenize card information (Apple Pay, Google Pay), the + tokenized number might be provided instead of the underlying card + number. + + + *As of May 1, 2021, card fingerprint in India for Connect changed to + allow two fingerprints for the same card---one for India and one for + the rest of the world.* + maxLength: 5000 + nullable: true + type: string + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + iin: + description: Issuer identification number of the card. + maxLength: 5000 + type: string + last4: + description: The last four digits of the card. + maxLength: 5000 + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + name: + description: Cardholder name. + maxLength: 5000 + nullable: true + type: string + networks: + $ref: '#/components/schemas/token_card_networks' + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - card + type: string + regulated_status: + description: Status of a card based on the card issuer. + enum: + - regulated + - unregulated + nullable: true + type: string + status: + description: >- + For external accounts that are cards, possible values are `new` and + `errored`. If a payout fails, the status is set to `errored` and + [scheduled payouts](https://stripe.com/docs/payouts#payout-schedule) + are stopped until account details are updated. + maxLength: 5000 + nullable: true + type: string + tokenization_method: + description: >- + If the card number is tokenized, this is the method that was used. + Can be `android_pay` (includes Google Pay), `apple_pay`, + `masterpass`, `visa_checkout`, or null. + maxLength: 5000 + nullable: true + type: string + required: + - brand + - exp_month + - exp_year + - funding + - id + - last4 + - object + title: Card + type: object + x-expandableFields: + - account + - customer + - networks + x-resourceId: card + card_generated_from_payment_method_details: + description: '' + properties: + card_present: + $ref: '#/components/schemas/payment_method_details_card_present' + type: + description: >- + The type of payment method transaction-specific details from the + transaction that generated this `card` payment method. Always + `card_present`. + maxLength: 5000 + type: string + required: + - type + title: card_generated_from_payment_method_details + type: object + x-expandableFields: + - card_present + card_issuing_account_terms_of_service: + description: '' + properties: + date: + description: >- + The Unix timestamp marking when the account representative accepted + the service agreement. + nullable: true + type: integer + ip: + description: >- + The IP address from which the account representative accepted the + service agreement. + maxLength: 5000 + nullable: true + type: string + user_agent: + description: >- + The user agent of the browser from which the account representative + accepted the service agreement. + maxLength: 5000 + type: string + title: CardIssuingAccountTermsOfService + type: object + x-expandableFields: [] + card_mandate_payment_method_details: + description: '' + properties: {} + title: card_mandate_payment_method_details + type: object + x-expandableFields: [] + cash_balance: + description: >- + A customer's `Cash balance` represents real funds. Customers can add + funds to their cash balance by sending a bank transfer. These funds can + be used for payment and can eventually be paid out to your bank account. + properties: + available: + additionalProperties: + type: integer + description: >- + A hash of all cash balances available to this customer. You cannot + delete a customer with any cash balances, even if the balance is 0. + Amounts are represented in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + nullable: true + type: object + customer: + description: The ID of the customer whose cash balance this object represents. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - cash_balance + type: string + settings: + $ref: '#/components/schemas/customer_balance_customer_balance_settings' + required: + - customer + - livemode + - object + - settings + title: cash_balance + type: object + x-expandableFields: + - settings + x-resourceId: cash_balance + charge: + description: >- + The `Charge` object represents a single attempt to move money into your + Stripe account. + + PaymentIntent confirmation is the most common way to create Charges, but + transferring + + money to a different Stripe account through Connect also creates + Charges. + + Some legacy payment flows create Charges directly, which is not + recommended for new integrations. + properties: + amount: + description: >- + Amount intended to be collected by this payment. A positive integer + representing how much to charge in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal) (e.g., 100 + cents to charge $1.00 or 100 to charge ¥100, a zero-decimal + currency). The minimum amount is $0.50 US or [equivalent in charge + currency](https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts). + The amount value supports up to eight digits (e.g., a value of + 99999999 for a USD charge of $999,999.99). + type: integer + amount_captured: + description: >- + Amount in cents (or local equivalent) captured (can be less than the + amount attribute on the charge if a partial capture was made). + type: integer + amount_refunded: + description: >- + Amount in cents (or local equivalent) refunded (can be less than the + amount attribute on the charge if a partial refund was issued). + type: integer + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + description: ID of the Connect application that created the charge. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + application_fee: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application_fee' + description: >- + The application fee (if any) for the charge. [See the Connect + documentation](https://stripe.com/docs/connect/direct-charges#collect-fees) + for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application_fee' + application_fee_amount: + description: >- + The amount of the application fee (if any) requested for the charge. + [See the Connect + documentation](https://stripe.com/docs/connect/direct-charges#collect-fees) + for details. + nullable: true + type: integer + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + ID of the balance transaction that describes the impact of this + charge on your account balance (not including refunds or disputes). + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + billing_details: + $ref: '#/components/schemas/billing_details' + calculated_statement_descriptor: + description: >- + The full statement descriptor that is passed to card networks, and + that is displayed on your customers' credit card and bank + statements. Allows you to see what the statement descriptor looks + like after the static and dynamic portions are combined. This value + only exists for card payments. + maxLength: 5000 + nullable: true + type: string + captured: + description: >- + If the charge was created without capturing, this Boolean represents + whether it is still uncaptured or has since been captured. + type: boolean + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: ID of the customer this charge is for if one exists. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 40000 + nullable: true + type: string + disputed: + description: Whether the charge has been disputed. + type: boolean + failure_balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + ID of the balance transaction that describes the reversal of the + balance on your account due to payment failure. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + failure_code: + description: >- + Error code explaining reason for charge failure if available (see + [the errors section](https://stripe.com/docs/error-codes) for a list + of codes). + maxLength: 5000 + nullable: true + type: string + failure_message: + description: >- + Message to user further explaining reason for charge failure if + available. + maxLength: 5000 + nullable: true + type: string + fraud_details: + anyOf: + - $ref: '#/components/schemas/charge_fraud_details' + description: Information on fraud assessments for the charge. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - charge + type: string + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account (if any) the charge was made on behalf of without + triggering an automatic transfer. See the [Connect + documentation](https://stripe.com/docs/connect/separate-charges-and-transfers) + for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + outcome: + anyOf: + - $ref: '#/components/schemas/charge_outcome' + description: >- + Details about whether the payment was accepted, and why. See + [understanding declines](https://stripe.com/docs/declines) for + details. + nullable: true + paid: + description: >- + `true` if the charge succeeded, or was successfully authorized for + later capture. + type: boolean + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: 'ID of the PaymentIntent associated with this charge, if one exists.' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + payment_method: + description: ID of the payment method used in this charge. + maxLength: 5000 + nullable: true + type: string + payment_method_details: + anyOf: + - $ref: '#/components/schemas/payment_method_details' + description: Details about the payment method at the time of the transaction. + nullable: true + presentment_details: + $ref: >- + #/components/schemas/payment_flows_payment_intent_presentment_details + radar_options: + $ref: '#/components/schemas/radar_radar_options' + receipt_email: + description: >- + This is the email address that the receipt for this charge was sent + to. + maxLength: 5000 + nullable: true + type: string + receipt_number: + description: >- + This is the transaction number that appears on email receipts sent + for this charge. This attribute will be `null` until a receipt has + been sent. + maxLength: 5000 + nullable: true + type: string + receipt_url: + description: >- + This is the URL to view the receipt for this charge. The receipt is + kept up-to-date to the latest state of the charge, including any + refunds. If the charge is for an Invoice, the receipt will be + stylized as an Invoice receipt. + maxLength: 5000 + nullable: true + type: string + refunded: + description: >- + Whether the charge has been fully refunded. If the charge is only + partially refunded, this attribute will still be false. + type: boolean + refunds: + description: A list of refunds that have been applied to the charge. + nullable: true + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/refund' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: RefundList + type: object + x-expandableFields: + - data + review: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/review' + description: ID of the review associated with this charge if one exists. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/review' + shipping: + anyOf: + - $ref: '#/components/schemas/shipping' + description: Shipping information for the charge. + nullable: true + source_transfer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/transfer' + description: >- + The transfer ID which created this charge. Only present if the + charge came from another Stripe account. [See the Connect + documentation](https://docs.stripe.com/connect/destination-charges) + for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/transfer' + statement_descriptor: + description: >- + For a non-card charge, text that appears on the customer's statement + as the statement descriptor. This value overrides the account's + default statement descriptor. For information about requirements, + including the 22-character limit, see [the Statement Descriptor + docs](https://docs.stripe.com/get-started/account/statement-descriptors). + + + For a card charge, this value is ignored unless you don't specify a + `statement_descriptor_suffix`, in which case this value is used as + the suffix. + maxLength: 5000 + nullable: true + type: string + statement_descriptor_suffix: + description: >- + Provides information about a card charge. Concatenated to the + account's [statement descriptor + prefix](https://docs.stripe.com/get-started/account/statement-descriptors#static) + to form the complete statement descriptor that appears on the + customer's statement. If the account has no prefix value, the suffix + is concatenated to the account's statement descriptor. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + The status of the payment is either `succeeded`, `pending`, or + `failed`. + enum: + - failed + - pending + - succeeded + type: string + transfer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/transfer' + description: >- + ID of the transfer to the `destination` account (only applicable if + the charge was created using the `destination` parameter). + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/transfer' + transfer_data: + anyOf: + - $ref: '#/components/schemas/charge_transfer_data' + description: >- + An optional dictionary including the account to automatically + transfer to as part of a destination charge. [See the Connect + documentation](https://stripe.com/docs/connect/destination-charges) + for details. + nullable: true + transfer_group: + description: >- + A string that identifies this transaction as part of a group. See + the [Connect + documentation](https://stripe.com/docs/connect/separate-charges-and-transfers#transfer-options) + for details. + maxLength: 5000 + nullable: true + type: string + required: + - amount + - amount_captured + - amount_refunded + - billing_details + - captured + - created + - currency + - disputed + - id + - livemode + - metadata + - object + - paid + - refunded + - status + title: Charge + type: object + x-expandableFields: + - application + - application_fee + - balance_transaction + - billing_details + - customer + - failure_balance_transaction + - fraud_details + - on_behalf_of + - outcome + - payment_intent + - payment_method_details + - presentment_details + - radar_options + - refunds + - review + - shipping + - source_transfer + - transfer + - transfer_data + x-resourceId: charge + charge_fraud_details: + description: '' + properties: + stripe_report: + description: 'Assessments from Stripe. If set, the value is `fraudulent`.' + maxLength: 5000 + type: string + user_report: + description: >- + Assessments reported by you. If set, possible values of are `safe` + and `fraudulent`. + maxLength: 5000 + type: string + title: ChargeFraudDetails + type: object + x-expandableFields: [] + charge_outcome: + description: '' + properties: + advice_code: + description: >- + An enumerated value providing a more detailed explanation on [how to + proceed with an + error](https://stripe.com/docs/declines#retrying-issuer-declines). + enum: + - confirm_card_data + - do_not_try_again + - try_again_later + nullable: true + type: string + network_advice_code: + description: >- + For charges declined by the network, a 2 digit code which indicates + the advice returned by the network on how to proceed with an error. + maxLength: 5000 + nullable: true + type: string + network_decline_code: + description: >- + For charges declined by the network, a brand specific 2, 3, or 4 + digit code which indicates the reason the authorization failed. + maxLength: 5000 + nullable: true + type: string + network_status: + description: >- + Possible values are `approved_by_network`, `declined_by_network`, + `not_sent_to_network`, and `reversed_after_approval`. The value + `reversed_after_approval` indicates the payment was [blocked by + Stripe](https://stripe.com/docs/declines#blocked-payments) after + bank authorization, and may temporarily appear as "pending" on a + cardholder's statement. + maxLength: 5000 + nullable: true + type: string + reason: + description: >- + An enumerated value providing a more detailed explanation of the + outcome's `type`. Charges blocked by Radar's default block rule have + the value `highest_risk_level`. Charges placed in review by Radar's + default review rule have the value `elevated_risk_level`. Charges + authorized, blocked, or placed in review by custom rules have the + value `rule`. See [understanding + declines](https://stripe.com/docs/declines) for more details. + maxLength: 5000 + nullable: true + type: string + risk_level: + description: >- + Stripe Radar's evaluation of the riskiness of the payment. Possible + values for evaluated payments are `normal`, `elevated`, `highest`. + For non-card payments, and card-based payments predating the public + assignment of risk levels, this field will have the value + `not_assessed`. In the event of an error in the evaluation, this + field will have the value `unknown`. This field is only available + with Radar. + maxLength: 5000 + type: string + risk_score: + description: >- + Stripe Radar's evaluation of the riskiness of the payment. Possible + values for evaluated payments are between 0 and 100. For non-card + payments, card-based payments predating the public assignment of + risk scores, or in the event of an error during evaluation, this + field will not be present. This field is only available with Radar + for Fraud Teams. + type: integer + rule: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/rule' + description: 'The ID of the Radar rule that matched the payment, if applicable.' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/rule' + seller_message: + description: >- + A human-readable description of the outcome type and reason, + designed for you (the recipient of the payment), not your customer. + maxLength: 5000 + nullable: true + type: string + type: + description: >- + Possible values are `authorized`, `manual_review`, + `issuer_declined`, `blocked`, and `invalid`. See [understanding + declines](https://stripe.com/docs/declines) and [Radar + reviews](https://stripe.com/docs/radar/reviews) for details. + maxLength: 5000 + type: string + required: + - type + title: ChargeOutcome + type: object + x-expandableFields: + - rule + charge_transfer_data: + description: '' + properties: + amount: + description: >- + The amount transferred to the destination account, if specified. By + default, the entire charge amount is transferred to the destination + account. + nullable: true + type: integer + destination: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + ID of an existing, connected Stripe account to transfer funds to if + `transfer_data` was specified in the charge request. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + required: + - destination + title: ChargeTransferData + type: object + x-expandableFields: + - destination + checkout.session: + description: >- + A Checkout Session represents your customer's session as they pay for + + one-time purchases or subscriptions through + [Checkout](https://stripe.com/docs/payments/checkout) + + or [Payment Links](https://stripe.com/docs/payments/payment-links). We + recommend creating a + + new Session each time your customer attempts to pay. + + + Once payment is successful, the Checkout Session will contain a + reference + + to the [Customer](https://stripe.com/docs/api/customers), and either the + successful + + [PaymentIntent](https://stripe.com/docs/api/payment_intents) or an + active + + [Subscription](https://stripe.com/docs/api/subscriptions). + + + You can create a Checkout Session on your server and redirect to its URL + + to begin Checkout. + + + Related guide: [Checkout + quickstart](https://stripe.com/docs/checkout/quickstart) + properties: + adaptive_pricing: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_adaptive_pricing + description: >- + Settings for price localization with [Adaptive + Pricing](https://docs.stripe.com/payments/checkout/adaptive-pricing). + nullable: true + after_expiration: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_after_expiration + description: >- + When set, provides configuration for actions to take if this + Checkout Session expires. + nullable: true + allow_promotion_codes: + description: Enables user redeemable promotion codes. + nullable: true + type: boolean + amount_subtotal: + description: Total of all items before discounts or taxes are applied. + nullable: true + type: integer + amount_total: + description: Total of all items after discounts and taxes are applied. + nullable: true + type: integer + automatic_tax: + $ref: '#/components/schemas/payment_pages_checkout_session_automatic_tax' + billing_address_collection: + description: >- + Describes whether Checkout should collect the customer's billing + address. Defaults to `auto`. + enum: + - auto + - required + nullable: true + type: string + cancel_url: + description: >- + If set, Checkout displays a back button and customers will be + directed to this URL if they decide to cancel payment and return to + your website. + maxLength: 5000 + nullable: true + type: string + client_reference_id: + description: |- + A unique string to reference the Checkout Session. This can be a + customer ID, a cart ID, or similar, and can be used to reconcile the + Session with your internal systems. + maxLength: 5000 + nullable: true + type: string + client_secret: + description: >- + The client secret of your Checkout Session. Applies to Checkout + Sessions with `ui_mode: embedded` or `ui_mode: custom`. For + `ui_mode: embedded`, the client secret is to be used when + initializing Stripe.js embedded checkout. + For `ui_mode: custom`, use the client secret with [initCheckout](https://stripe.com/docs/js/custom_checkout/init) on your front end. + maxLength: 5000 + nullable: true + type: string + collected_information: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_collected_information + description: >- + Information about the customer collected within the Checkout + Session. + nullable: true + consent: + anyOf: + - $ref: '#/components/schemas/payment_pages_checkout_session_consent' + description: Results of `consent_collection` for this session. + nullable: true + consent_collection: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_consent_collection + description: >- + When set, provides configuration for the Checkout Session to gather + active consent from customers. + nullable: true + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + nullable: true + type: string + currency_conversion: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_currency_conversion + description: >- + Currency conversion details for [Adaptive + Pricing](https://docs.stripe.com/payments/checkout/adaptive-pricing) + sessions created before 2025-03-31. + nullable: true + custom_fields: + description: >- + Collect additional information from your customer using custom + fields. Up to 3 fields are supported. + items: + $ref: '#/components/schemas/payment_pages_checkout_session_custom_fields' + type: array + custom_text: + $ref: '#/components/schemas/payment_pages_checkout_session_custom_text' + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: >- + The ID of the customer for this Session. + + For Checkout Sessions in `subscription` mode or Checkout Sessions + with `customer_creation` set as `always` in `payment` mode, Checkout + + will create a new customer object based on information provided + + during the payment flow unless an existing customer was provided + when + + the Session was created. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + customer_creation: + description: >- + Configure whether a Checkout Session creates a Customer when the + Checkout Session completes. + enum: + - always + - if_required + nullable: true + type: string + customer_details: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_customer_details + description: >- + The customer details including the customer's tax exempt status and + the customer's tax IDs. Customer's address details are not present + on Sessions in `setup` mode. + nullable: true + customer_email: + description: >- + If provided, this value will be used when the Customer object is + created. + + If not provided, customers will be asked to enter their email + address. + + Use this parameter to prefill customer data if you already have an + email + + on file. To access information about the customer once the payment + flow is + + complete, use the `customer` attribute. + maxLength: 5000 + nullable: true + type: string + discounts: + description: >- + List of coupons and promotion codes attached to the Checkout + Session. + items: + $ref: '#/components/schemas/payment_pages_checkout_session_discount' + nullable: true + type: array + expires_at: + description: The timestamp at which the Checkout Session will expire. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: 'ID of the invoice created by the Checkout Session, if it exists.' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + invoice_creation: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_invoice_creation + description: Details on the state of invoice creation for the Checkout Session. + nullable: true + line_items: + description: The line items purchased by the customer. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: PaymentPagesCheckoutSessionListLineItems + type: object + x-expandableFields: + - data + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + locale: + description: >- + The IETF language tag of the locale Checkout is displayed in. If + blank or `auto`, the browser's locale is used. + enum: + - auto + - bg + - cs + - da + - de + - el + - en + - en-GB + - es + - es-419 + - et + - fi + - fil + - fr + - fr-CA + - hr + - hu + - id + - it + - ja + - ko + - lt + - lv + - ms + - mt + - nb + - nl + - pl + - pt + - pt-BR + - ro + - ru + - sk + - sl + - sv + - th + - tr + - vi + - zh + - zh-HK + - zh-TW + nullable: true + type: string + x-stripeBypassValidation: true + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + mode: + description: The mode of the Checkout Session. + enum: + - payment + - setup + - subscription + type: string + x-stripeBypassValidation: true + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - checkout.session + type: string + optional_items: + description: The optional items presented to the customer at checkout. + items: + $ref: '#/components/schemas/payment_pages_checkout_session_optional_item' + nullable: true + type: array + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: >- + The ID of the PaymentIntent for Checkout Sessions in `payment` mode. + You can't confirm or cancel the PaymentIntent for a Checkout + Session. To cancel, [expire the Checkout + Session](https://stripe.com/docs/api/checkout/sessions/expire) + instead. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + payment_link: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_link' + description: The ID of the Payment Link that created this Session. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_link' + payment_method_collection: + description: >- + Configure whether a Checkout Session should collect a payment + method. Defaults to `always`. + enum: + - always + - if_required + nullable: true + type: string + payment_method_configuration_details: + anyOf: + - $ref: >- + #/components/schemas/payment_method_config_biz_payment_method_configuration_details + description: >- + Information about the payment method configuration used for this + Checkout session if using dynamic payment methods. + nullable: true + payment_method_options: + anyOf: + - $ref: '#/components/schemas/checkout_session_payment_method_options' + description: >- + Payment-method-specific configuration for the PaymentIntent or + SetupIntent of this CheckoutSession. + nullable: true + payment_method_types: + description: |- + A list of the types of payment methods (e.g. card) this Checkout + Session is allowed to accept. + items: + maxLength: 5000 + type: string + type: array + payment_status: + description: >- + The payment status of the Checkout Session, one of `paid`, `unpaid`, + or `no_payment_required`. + + You can use this value to decide when to fulfill your customer's + order. + enum: + - no_payment_required + - paid + - unpaid + type: string + permissions: + anyOf: + - $ref: '#/components/schemas/payment_pages_checkout_session_permissions' + description: >- + This property is used to set up permissions for various actions + (e.g., update) on the CheckoutSession object. + + + For specific permissions, please refer to their dedicated + subsections, such as `permissions.update_shipping_details`. + nullable: true + phone_number_collection: + $ref: >- + #/components/schemas/payment_pages_checkout_session_phone_number_collection + presentment_details: + $ref: >- + #/components/schemas/payment_flows_payment_intent_presentment_details + recovered_from: + description: >- + The ID of the original expired Checkout Session that triggered the + recovery flow. + maxLength: 5000 + nullable: true + type: string + redirect_on_completion: + description: >- + This parameter applies to `ui_mode: embedded`. Learn more about the + [redirect + behavior](https://stripe.com/docs/payments/checkout/custom-success-page?payment-ui=embedded-form) + of embedded sessions. Defaults to `always`. + enum: + - always + - if_required + - never + type: string + return_url: + description: >- + Applies to Checkout Sessions with `ui_mode: embedded` or `ui_mode: + custom`. The URL to redirect your customer back to after they + authenticate or cancel their payment on the payment method's app or + site. + maxLength: 5000 + type: string + saved_payment_method_options: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_saved_payment_method_options + description: >- + Controls saved payment method settings for the session. Only + available in `payment` and `subscription` mode. + nullable: true + setup_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/setup_intent' + description: >- + The ID of the SetupIntent for Checkout Sessions in `setup` mode. You + can't confirm or cancel the SetupIntent for a Checkout Session. To + cancel, [expire the Checkout + Session](https://stripe.com/docs/api/checkout/sessions/expire) + instead. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/setup_intent' + shipping_address_collection: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_shipping_address_collection + description: >- + When set, provides configuration for Checkout to collect a shipping + address from a customer. + nullable: true + shipping_cost: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_shipping_cost + description: >- + The details of the customer cost of shipping, including the customer + chosen ShippingRate. + nullable: true + shipping_options: + description: The shipping rate options applied to this Session. + items: + $ref: >- + #/components/schemas/payment_pages_checkout_session_shipping_option + type: array + status: + description: >- + The status of the Checkout Session, one of `open`, `complete`, or + `expired`. + enum: + - complete + - expired + - open + nullable: true + type: string + submit_type: + description: >- + Describes the type of transaction being performed by Checkout in + order to customize + + relevant text on the page, such as the submit button. `submit_type` + can only be + + specified on Checkout Sessions in `payment` mode. If blank or + `auto`, `pay` is used. + enum: + - auto + - book + - donate + - pay + - subscribe + nullable: true + type: string + subscription: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/subscription' + description: >- + The ID of the + [Subscription](https://stripe.com/docs/api/subscriptions) for + Checkout Sessions in `subscription` mode. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/subscription' + success_url: + description: |- + The URL the customer will be directed to after the payment or + subscription creation is successful. + maxLength: 5000 + nullable: true + type: string + tax_id_collection: + $ref: >- + #/components/schemas/payment_pages_checkout_session_tax_id_collection + total_details: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_total_details + description: Tax and discount details for the computed total amount. + nullable: true + ui_mode: + description: The UI mode of the Session. Defaults to `hosted`. + enum: + - custom + - embedded + - hosted + nullable: true + type: string + url: + description: >- + The URL to the Checkout Session. Applies to Checkout Sessions with + `ui_mode: hosted`. Redirect customers to this URL to take them to + Checkout. If you’re using [Custom + Domains](https://stripe.com/docs/payments/checkout/custom-domains), + the URL will use your subdomain. Otherwise, it’ll use + `checkout.stripe.com.` + + This value is only present when the session is active. + maxLength: 5000 + nullable: true + type: string + wallet_options: + anyOf: + - $ref: '#/components/schemas/checkout_session_wallet_options' + description: Wallet-specific configuration for this Checkout Session. + nullable: true + required: + - automatic_tax + - created + - custom_fields + - custom_text + - expires_at + - id + - livemode + - mode + - object + - payment_method_types + - payment_status + - shipping_options + title: Session + type: object + x-expandableFields: + - adaptive_pricing + - after_expiration + - automatic_tax + - collected_information + - consent + - consent_collection + - currency_conversion + - custom_fields + - custom_text + - customer + - customer_details + - discounts + - invoice + - invoice_creation + - line_items + - optional_items + - payment_intent + - payment_link + - payment_method_configuration_details + - payment_method_options + - permissions + - phone_number_collection + - presentment_details + - saved_payment_method_options + - setup_intent + - shipping_address_collection + - shipping_cost + - shipping_options + - subscription + - tax_id_collection + - total_details + - wallet_options + x-resourceId: checkout.session + checkout_acss_debit_mandate_options: + description: '' + properties: + custom_mandate_url: + description: A URL for custom mandate text + maxLength: 5000 + type: string + default_for: + description: >- + List of Stripe products where this mandate can be selected + automatically. Returned when the Session is in `setup` mode. + items: + enum: + - invoice + - subscription + type: string + type: array + interval_description: + description: >- + Description of the interval. Only required if the 'payment_schedule' + parameter is 'interval' or 'combined'. + maxLength: 5000 + nullable: true + type: string + payment_schedule: + description: Payment schedule for the mandate. + enum: + - combined + - interval + - sporadic + nullable: true + type: string + transaction_type: + description: Transaction type of the mandate. + enum: + - business + - personal + nullable: true + type: string + title: CheckoutAcssDebitMandateOptions + type: object + x-expandableFields: [] + checkout_acss_debit_payment_method_options: + description: '' + properties: + currency: + description: >- + Currency supported by the bank account. Returned when the Session is + in `setup` mode. + enum: + - cad + - usd + type: string + mandate_options: + $ref: '#/components/schemas/checkout_acss_debit_mandate_options' + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: CheckoutAcssDebitPaymentMethodOptions + type: object + x-expandableFields: + - mandate_options + checkout_affirm_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutAffirmPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_afterpay_clearpay_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutAfterpayClearpayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_alipay_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutAlipayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_amazon_pay_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: CheckoutAmazonPayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_au_becs_debit_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + title: CheckoutAuBecsDebitPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_bacs_debit_payment_method_options: + description: '' + properties: + mandate_options: + $ref: >- + #/components/schemas/checkout_payment_method_options_mandate_options_bacs_debit + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + title: CheckoutBacsDebitPaymentMethodOptions + type: object + x-expandableFields: + - mandate_options + checkout_bancontact_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutBancontactPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_boleto_payment_method_options: + description: '' + properties: + expires_after_days: + description: >- + The number of calendar days before a Boleto voucher expires. For + example, if you create a Boleto voucher on Monday and you set + expires_after_days to 2, the Boleto voucher will expire on Wednesday + at 23:59 America/Sao_Paulo time. + type: integer + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + required: + - expires_after_days + title: CheckoutBoletoPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_card_installments_options: + description: '' + properties: + enabled: + description: Indicates if installments are enabled + type: boolean + title: CheckoutCardInstallmentsOptions + type: object + x-expandableFields: [] + checkout_card_payment_method_options: + description: '' + properties: + installments: + $ref: '#/components/schemas/checkout_card_installments_options' + request_extended_authorization: + description: >- + Request ability to [capture beyond the standard authorization + validity window](/payments/extended-authorization) for this + CheckoutSession. + enum: + - if_available + - never + type: string + request_incremental_authorization: + description: >- + Request ability to [increment the + authorization](/payments/incremental-authorization) for this + CheckoutSession. + enum: + - if_available + - never + type: string + request_multicapture: + description: >- + Request ability to make [multiple captures](/payments/multicapture) + for this CheckoutSession. + enum: + - if_available + - never + type: string + request_overcapture: + description: >- + Request ability to [overcapture](/payments/overcapture) for this + CheckoutSession. + enum: + - if_available + - never + type: string + request_three_d_secure: + description: >- + We strongly recommend that you rely on our SCA Engine to + automatically prompt your customers for authentication based on risk + level and [other + requirements](https://stripe.com/docs/strong-customer-authentication). + However, if you wish to request 3D Secure based on logic from your + own fraud engine, provide this option. If not provided, this value + defaults to `automatic`. Read our guide on [manually requesting 3D + Secure](https://stripe.com/docs/payments/3d-secure/authentication-flow#manual-three-ds) + for more information on how this configuration interacts with Radar + and our SCA Engine. + enum: + - any + - automatic + - challenge + type: string + x-stripeBypassValidation: true + restrictions: + $ref: >- + #/components/schemas/payment_pages_private_card_payment_method_options_resource_restrictions + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + statement_descriptor_suffix_kana: + description: >- + Provides information about a card payment that customers see on + their statements. Concatenated with the Kana prefix (shortened Kana + descriptor) or Kana statement descriptor that’s set on the account + to form the complete statement descriptor. Maximum 22 characters. On + card statements, the *concatenation* of both prefix and suffix + (including separators) will appear truncated to 22 characters. + maxLength: 5000 + type: string + statement_descriptor_suffix_kanji: + description: >- + Provides information about a card payment that customers see on + their statements. Concatenated with the Kanji prefix (shortened + Kanji descriptor) or Kanji statement descriptor that’s set on the + account to form the complete statement descriptor. Maximum 17 + characters. On card statements, the *concatenation* of both prefix + and suffix (including separators) will appear truncated to 17 + characters. + maxLength: 5000 + type: string + required: + - request_three_d_secure + title: CheckoutCardPaymentMethodOptions + type: object + x-expandableFields: + - installments + - restrictions + checkout_cashapp_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutCashappPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_customer_balance_bank_transfer_payment_method_options: + description: '' + properties: + eu_bank_transfer: + $ref: >- + #/components/schemas/payment_method_options_customer_balance_eu_bank_account + requested_address_types: + description: >- + List of address types that should be returned in the + financial_addresses response. If not specified, all valid types will + be returned. + + + Permitted values include: `sort_code`, `zengin`, `iban`, or `spei`. + items: + enum: + - aba + - iban + - sepa + - sort_code + - spei + - swift + - zengin + type: string + x-stripeBypassValidation: true + type: array + type: + description: >- + The bank transfer type that this PaymentIntent is allowed to use for + funding Permitted values include: `eu_bank_transfer`, + `gb_bank_transfer`, `jp_bank_transfer`, `mx_bank_transfer`, or + `us_bank_transfer`. + enum: + - eu_bank_transfer + - gb_bank_transfer + - jp_bank_transfer + - mx_bank_transfer + - us_bank_transfer + nullable: true + type: string + x-stripeBypassValidation: true + title: CheckoutCustomerBalanceBankTransferPaymentMethodOptions + type: object + x-expandableFields: + - eu_bank_transfer + checkout_customer_balance_payment_method_options: + description: '' + properties: + bank_transfer: + $ref: >- + #/components/schemas/checkout_customer_balance_bank_transfer_payment_method_options + funding_type: + description: >- + The funding method type to be used when there are not enough funds + in the customer balance. Permitted values include: `bank_transfer`. + enum: + - bank_transfer + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutCustomerBalancePaymentMethodOptions + type: object + x-expandableFields: + - bank_transfer + checkout_eps_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutEpsPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_fpx_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutFpxPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_giropay_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutGiropayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_grab_pay_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutGrabPayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_ideal_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutIdealPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_kakao_pay_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: CheckoutKakaoPayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_klarna_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + title: CheckoutKlarnaPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_konbini_payment_method_options: + description: '' + properties: + expires_after_days: + description: >- + The number of calendar days (between 1 and 60) after which Konbini + payment instructions will expire. For example, if a PaymentIntent is + confirmed with Konbini and `expires_after_days` set to 2 on Monday + JST, the instructions will expire on Wednesday 23:59:59 JST. + nullable: true + type: integer + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutKonbiniPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_kr_card_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: CheckoutKrCardPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_link_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: CheckoutLinkPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_link_wallet_options: + description: '' + properties: + display: + description: Describes whether Checkout should display Link. Defaults to `auto`. + enum: + - auto + - never + type: string + title: CheckoutLinkWalletOptions + type: object + x-expandableFields: [] + checkout_mobilepay_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutMobilepayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_multibanco_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutMultibancoPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_naver_pay_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: CheckoutNaverPayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_oxxo_payment_method_options: + description: '' + properties: + expires_after_days: + description: >- + The number of calendar days before an OXXO invoice expires. For + example, if you create an OXXO invoice on Monday and you set + expires_after_days to 2, the OXXO invoice will expire on Wednesday + at 23:59 America/Mexico_City time. + type: integer + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + required: + - expires_after_days + title: CheckoutOxxoPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_p24_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutP24PaymentMethodOptions + type: object + x-expandableFields: [] + checkout_payco_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + title: CheckoutPaycoPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_payment_method_options_mandate_options_bacs_debit: + description: '' + properties: + reference_prefix: + description: >- + Prefix used to generate the Mandate reference. Must be at most 12 + characters long. Must consist of only uppercase letters, numbers, + spaces, or the following special characters: '/', '_', '-', '&', + '.'. Cannot begin with 'DDIC' or 'STRIPE'. + maxLength: 5000 + type: string + title: checkout_payment_method_options_mandate_options_bacs_debit + type: object + x-expandableFields: [] + checkout_payment_method_options_mandate_options_sepa_debit: + description: '' + properties: + reference_prefix: + description: >- + Prefix used to generate the Mandate reference. Must be at most 12 + characters long. Must consist of only uppercase letters, numbers, + spaces, or the following special characters: '/', '_', '-', '&', + '.'. Cannot begin with 'STRIPE'. + maxLength: 5000 + type: string + title: checkout_payment_method_options_mandate_options_sepa_debit + type: object + x-expandableFields: [] + checkout_paynow_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutPaynowPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_paypal_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + preferred_locale: + description: >- + Preferred locale of the PayPal checkout page that the customer is + redirected to. + maxLength: 5000 + nullable: true + type: string + reference: + description: >- + A reference of the PayPal transaction visible to customer which is + mapped to PayPal's invoice ID. This must be a globally unique ID if + you have configured in your PayPal settings to block multiple + payments per invoice ID. + maxLength: 5000 + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: CheckoutPaypalPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_pix_payment_method_options: + description: '' + properties: + expires_after_seconds: + description: The number of seconds after which Pix payment will expire. + nullable: true + type: integer + title: CheckoutPixPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_revolut_pay_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: CheckoutRevolutPayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_samsung_pay_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + title: CheckoutSamsungPayPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_sepa_debit_payment_method_options: + description: '' + properties: + mandate_options: + $ref: >- + #/components/schemas/checkout_payment_method_options_mandate_options_sepa_debit + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + title: CheckoutSepaDebitPaymentMethodOptions + type: object + x-expandableFields: + - mandate_options + checkout_session_payment_method_options: + description: '' + properties: + acss_debit: + $ref: '#/components/schemas/checkout_acss_debit_payment_method_options' + affirm: + $ref: '#/components/schemas/checkout_affirm_payment_method_options' + afterpay_clearpay: + $ref: >- + #/components/schemas/checkout_afterpay_clearpay_payment_method_options + alipay: + $ref: '#/components/schemas/checkout_alipay_payment_method_options' + amazon_pay: + $ref: '#/components/schemas/checkout_amazon_pay_payment_method_options' + au_becs_debit: + $ref: '#/components/schemas/checkout_au_becs_debit_payment_method_options' + bacs_debit: + $ref: '#/components/schemas/checkout_bacs_debit_payment_method_options' + bancontact: + $ref: '#/components/schemas/checkout_bancontact_payment_method_options' + boleto: + $ref: '#/components/schemas/checkout_boleto_payment_method_options' + card: + $ref: '#/components/schemas/checkout_card_payment_method_options' + cashapp: + $ref: '#/components/schemas/checkout_cashapp_payment_method_options' + customer_balance: + $ref: >- + #/components/schemas/checkout_customer_balance_payment_method_options + eps: + $ref: '#/components/schemas/checkout_eps_payment_method_options' + fpx: + $ref: '#/components/schemas/checkout_fpx_payment_method_options' + giropay: + $ref: '#/components/schemas/checkout_giropay_payment_method_options' + grabpay: + $ref: '#/components/schemas/checkout_grab_pay_payment_method_options' + ideal: + $ref: '#/components/schemas/checkout_ideal_payment_method_options' + kakao_pay: + $ref: '#/components/schemas/checkout_kakao_pay_payment_method_options' + klarna: + $ref: '#/components/schemas/checkout_klarna_payment_method_options' + konbini: + $ref: '#/components/schemas/checkout_konbini_payment_method_options' + kr_card: + $ref: '#/components/schemas/checkout_kr_card_payment_method_options' + link: + $ref: '#/components/schemas/checkout_link_payment_method_options' + mobilepay: + $ref: '#/components/schemas/checkout_mobilepay_payment_method_options' + multibanco: + $ref: '#/components/schemas/checkout_multibanco_payment_method_options' + naver_pay: + $ref: '#/components/schemas/checkout_naver_pay_payment_method_options' + oxxo: + $ref: '#/components/schemas/checkout_oxxo_payment_method_options' + p24: + $ref: '#/components/schemas/checkout_p24_payment_method_options' + payco: + $ref: '#/components/schemas/checkout_payco_payment_method_options' + paynow: + $ref: '#/components/schemas/checkout_paynow_payment_method_options' + paypal: + $ref: '#/components/schemas/checkout_paypal_payment_method_options' + pix: + $ref: '#/components/schemas/checkout_pix_payment_method_options' + revolut_pay: + $ref: '#/components/schemas/checkout_revolut_pay_payment_method_options' + samsung_pay: + $ref: '#/components/schemas/checkout_samsung_pay_payment_method_options' + sepa_debit: + $ref: '#/components/schemas/checkout_sepa_debit_payment_method_options' + sofort: + $ref: '#/components/schemas/checkout_sofort_payment_method_options' + swish: + $ref: '#/components/schemas/checkout_swish_payment_method_options' + us_bank_account: + $ref: '#/components/schemas/checkout_us_bank_account_payment_method_options' + title: CheckoutSessionPaymentMethodOptions + type: object + x-expandableFields: + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - boleto + - card + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - oxxo + - p24 + - payco + - paynow + - paypal + - pix + - revolut_pay + - samsung_pay + - sepa_debit + - sofort + - swish + - us_bank_account + checkout_session_wallet_options: + description: '' + properties: + link: + $ref: '#/components/schemas/checkout_link_wallet_options' + title: CheckoutSessionWalletOptions + type: object + x-expandableFields: + - link + checkout_sofort_payment_method_options: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: CheckoutSofortPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_swish_payment_method_options: + description: '' + properties: + reference: + description: >- + The order reference that will be displayed to customers in the Swish + application. Defaults to the `id` of the Payment Intent. + maxLength: 5000 + nullable: true + type: string + title: CheckoutSwishPaymentMethodOptions + type: object + x-expandableFields: [] + checkout_us_bank_account_payment_method_options: + description: '' + properties: + financial_connections: + $ref: '#/components/schemas/linked_account_options_common' + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + type: string + x-stripeBypassValidation: true + title: CheckoutUsBankAccountPaymentMethodOptions + type: object + x-expandableFields: + - financial_connections + climate.order: + description: >- + Orders represent your intent to purchase a particular Climate product. + When you create an order, the + + payment is deducted from your merchant balance. + properties: + amount_fees: + description: >- + Total amount of [Frontier](https://frontierclimate.com/)'s service + fees in the currency's smallest unit. + type: integer + amount_subtotal: + description: Total amount of the carbon removal in the currency's smallest unit. + type: integer + amount_total: + description: >- + Total amount of the order including fees in the currency's smallest + unit. + type: integer + beneficiary: + $ref: '#/components/schemas/climate_removals_beneficiary' + canceled_at: + description: >- + Time at which the order was canceled. Measured in seconds since the + Unix epoch. + format: unix-time + nullable: true + type: integer + cancellation_reason: + description: Reason for the cancellation of this order. + enum: + - expired + - product_unavailable + - requested + nullable: true + type: string + x-stripeBypassValidation: true + certificate: + description: 'For delivered orders, a URL to a delivery certificate for the order.' + maxLength: 5000 + nullable: true + type: string + confirmed_at: + description: >- + Time at which the order was confirmed. Measured in seconds since the + Unix epoch. + format: unix-time + nullable: true + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase, representing the currency for this order. + maxLength: 5000 + type: string + delayed_at: + description: >- + Time at which the order's expected_delivery_year was delayed. + Measured in seconds since the Unix epoch. + format: unix-time + nullable: true + type: integer + delivered_at: + description: >- + Time at which the order was delivered. Measured in seconds since the + Unix epoch. + format: unix-time + nullable: true + type: integer + delivery_details: + description: Details about the delivery of carbon removal for this order. + items: + $ref: '#/components/schemas/climate_removals_order_deliveries' + type: array + expected_delivery_year: + description: The year this order is expected to be delivered. + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + metric_tons: + description: Quantity of carbon removal that is included in this order. + format: decimal + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - climate.order + type: string + product: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/climate.product' + description: Unique ID for the Climate `Product` this order is purchasing. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/climate.product' + product_substituted_at: + description: >- + Time at which the order's product was substituted for a different + product. Measured in seconds since the Unix epoch. + format: unix-time + nullable: true + type: integer + status: + description: The current status of this order. + enum: + - awaiting_funds + - canceled + - confirmed + - delivered + - open + type: string + required: + - amount_fees + - amount_subtotal + - amount_total + - created + - currency + - delivery_details + - expected_delivery_year + - id + - livemode + - metadata + - metric_tons + - object + - product + - status + title: ClimateRemovalsOrders + type: object + x-expandableFields: + - beneficiary + - delivery_details + - product + x-resourceId: climate.order + climate.product: + description: >- + A Climate product represents a type of carbon removal unit available for + reservation. + + You can retrieve it to see the current price and availability. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + current_prices_per_metric_ton: + additionalProperties: + $ref: '#/components/schemas/climate_removals_products_price' + description: >- + Current prices for a metric ton of carbon removal in a currency's + smallest unit. + type: object + delivery_year: + description: The year in which the carbon removal is expected to be delivered. + nullable: true + type: integer + id: + description: >- + Unique identifier for the object. For convenience, Climate product + IDs are human-readable strings + + that start with `climsku_`. See [carbon removal + inventory](https://stripe.com/docs/climate/orders/carbon-removal-inventory) + + for a list of available carbon removal products. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metric_tons_available: + description: The quantity of metric tons available for reservation. + format: decimal + type: string + name: + description: The Climate product's name. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - climate.product + type: string + suppliers: + description: >- + The carbon removal suppliers that fulfill orders for this Climate + product. + items: + $ref: '#/components/schemas/climate.supplier' + type: array + required: + - created + - current_prices_per_metric_ton + - id + - livemode + - metric_tons_available + - name + - object + - suppliers + title: ClimateRemovalsProducts + type: object + x-expandableFields: + - current_prices_per_metric_ton + - suppliers + x-resourceId: climate.product + climate.supplier: + description: A supplier of carbon removal. + properties: + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + info_url: + description: Link to a webpage to learn more about the supplier. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + locations: + description: The locations in which this supplier operates. + items: + $ref: '#/components/schemas/climate_removals_location' + type: array + name: + description: Name of this carbon removal supplier. + maxLength: 5000 + type: string + object: + description: >- + String representing the object’s type. Objects of the same type + share the same value. + enum: + - climate.supplier + type: string + removal_pathway: + description: The scientific pathway used for carbon removal. + enum: + - biomass_carbon_removal_and_storage + - direct_air_capture + - enhanced_weathering + type: string + x-stripeBypassValidation: true + required: + - id + - info_url + - livemode + - locations + - name + - object + - removal_pathway + title: ClimateRemovalsSuppliers + type: object + x-expandableFields: + - locations + x-resourceId: climate.supplier + climate_removals_beneficiary: + description: '' + properties: + public_name: + description: Publicly displayable name for the end beneficiary of carbon removal. + maxLength: 5000 + type: string + required: + - public_name + title: ClimateRemovalsBeneficiary + type: object + x-expandableFields: [] + climate_removals_location: + description: '' + properties: + city: + description: The city where the supplier is located. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country where the supplier is + located. + maxLength: 5000 + type: string + latitude: + description: The geographic latitude where the supplier is located. + nullable: true + type: number + longitude: + description: The geographic longitude where the supplier is located. + nullable: true + type: number + region: + description: The state/county/province/region where the supplier is located. + maxLength: 5000 + nullable: true + type: string + required: + - country + title: ClimateRemovalsLocation + type: object + x-expandableFields: [] + climate_removals_order_deliveries: + description: The delivery of a specified quantity of carbon for an order. + properties: + delivered_at: + description: >- + Time at which the delivery occurred. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + location: + anyOf: + - $ref: '#/components/schemas/climate_removals_location' + description: Specific location of this delivery. + nullable: true + metric_tons: + description: Quantity of carbon removal supplied by this delivery. + maxLength: 5000 + type: string + registry_url: + description: >- + Once retired, a URL to the registry entry for the tons from this + delivery. + maxLength: 5000 + nullable: true + type: string + supplier: + $ref: '#/components/schemas/climate.supplier' + required: + - delivered_at + - metric_tons + - supplier + title: ClimateRemovalsOrderDeliveries + type: object + x-expandableFields: + - location + - supplier + climate_removals_products_price: + description: '' + properties: + amount_fees: + description: >- + Fees for one metric ton of carbon removal in the currency's smallest + unit. + type: integer + amount_subtotal: + description: >- + Subtotal for one metric ton of carbon removal (excluding fees) in + the currency's smallest unit. + type: integer + amount_total: + description: >- + Total for one metric ton of carbon removal (including fees) in the + currency's smallest unit. + type: integer + required: + - amount_fees + - amount_subtotal + - amount_total + title: ClimateRemovalsProductsPrice + type: object + x-expandableFields: [] + confirmation_token: + description: >- + ConfirmationTokens help transport client side data collected by Stripe + JS over + + to your server for confirming a PaymentIntent or SetupIntent. If the + confirmation + + is successful, values present on the ConfirmationToken are written onto + the Intent. + + + To learn more about how to use ConfirmationToken, visit the related + guides: + + - [Finalize payments on the + server](https://stripe.com/docs/payments/finalize-payments-on-the-server) + + - [Build two-step + confirmation](https://stripe.com/docs/payments/build-a-two-step-confirmation). + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + expires_at: + description: >- + Time at which this ConfirmationToken expires and can no longer be + used to confirm a PaymentIntent or SetupIntent. + format: unix-time + nullable: true + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + mandate_data: + anyOf: + - $ref: '#/components/schemas/confirmation_tokens_resource_mandate_data' + description: Data used for generating a Mandate. + nullable: true + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - confirmation_token + type: string + payment_intent: + description: >- + ID of the PaymentIntent that this ConfirmationToken was used to + confirm, or null if this ConfirmationToken has not yet been used. + maxLength: 5000 + nullable: true + type: string + payment_method_options: + anyOf: + - $ref: >- + #/components/schemas/confirmation_tokens_resource_payment_method_options + description: Payment-method-specific configuration for this ConfirmationToken. + nullable: true + payment_method_preview: + anyOf: + - $ref: >- + #/components/schemas/confirmation_tokens_resource_payment_method_preview + description: >- + Payment details collected by the Payment Element, used to create a + PaymentMethod when a PaymentIntent or SetupIntent is confirmed with + this ConfirmationToken. + nullable: true + return_url: + description: Return URL used to confirm the Intent. + maxLength: 5000 + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + ConfirmationToken's payment method. + + + The presence of this property will [attach the payment + method](https://stripe.com/docs/payments/save-during-payment) to the + PaymentIntent's Customer, if present, after the PaymentIntent is + confirmed and any required actions from the user are complete. + enum: + - off_session + - on_session + nullable: true + type: string + setup_intent: + description: >- + ID of the SetupIntent that this ConfirmationToken was used to + confirm, or null if this ConfirmationToken has not yet been used. + maxLength: 5000 + nullable: true + type: string + shipping: + anyOf: + - $ref: '#/components/schemas/confirmation_tokens_resource_shipping' + description: Shipping information collected on this ConfirmationToken. + nullable: true + use_stripe_sdk: + description: >- + Indicates whether the Stripe SDK is used to handle confirmation + flow. Defaults to `true` on ConfirmationToken. + type: boolean + required: + - created + - id + - livemode + - object + - use_stripe_sdk + title: ConfirmationTokensResourceConfirmationToken + type: object + x-expandableFields: + - mandate_data + - payment_method_options + - payment_method_preview + - shipping + x-resourceId: confirmation_token + confirmation_tokens_resource_mandate_data: + description: Data used for generating a Mandate. + properties: + customer_acceptance: + $ref: >- + #/components/schemas/confirmation_tokens_resource_mandate_data_resource_customer_acceptance + required: + - customer_acceptance + title: ConfirmationTokensResourceMandateData + type: object + x-expandableFields: + - customer_acceptance + confirmation_tokens_resource_mandate_data_resource_customer_acceptance: + description: This hash contains details about the customer acceptance of the Mandate. + properties: + online: + anyOf: + - $ref: >- + #/components/schemas/confirmation_tokens_resource_mandate_data_resource_customer_acceptance_resource_online + description: >- + If this is a Mandate accepted online, this hash contains details + about the online acceptance. + nullable: true + type: + description: >- + The type of customer acceptance information included with the + Mandate. + maxLength: 5000 + type: string + required: + - type + title: ConfirmationTokensResourceMandateDataResourceCustomerAcceptance + type: object + x-expandableFields: + - online + confirmation_tokens_resource_mandate_data_resource_customer_acceptance_resource_online: + description: This hash contains details about the online acceptance. + properties: + ip_address: + description: The IP address from which the Mandate was accepted by the customer. + maxLength: 5000 + nullable: true + type: string + user_agent: + description: >- + The user agent of the browser from which the Mandate was accepted by + the customer. + maxLength: 5000 + nullable: true + type: string + title: >- + ConfirmationTokensResourceMandateDataResourceCustomerAcceptanceResourceOnline + type: object + x-expandableFields: [] + confirmation_tokens_resource_payment_method_options: + description: Payment-method-specific configuration + properties: + card: + anyOf: + - $ref: >- + #/components/schemas/confirmation_tokens_resource_payment_method_options_resource_card + description: This hash contains the card payment method options. + nullable: true + title: ConfirmationTokensResourcePaymentMethodOptions + type: object + x-expandableFields: + - card + confirmation_tokens_resource_payment_method_options_resource_card: + description: This hash contains the card payment method options. + properties: + cvc_token: + description: The `cvc_update` Token collected from the Payment Element. + maxLength: 5000 + nullable: true + type: string + installments: + $ref: >- + #/components/schemas/confirmation_tokens_resource_payment_method_options_resource_card_resource_installment + title: ConfirmationTokensResourcePaymentMethodOptionsResourceCard + type: object + x-expandableFields: + - installments + confirmation_tokens_resource_payment_method_options_resource_card_resource_installment: + description: Installment configuration for payments. + properties: + plan: + $ref: '#/components/schemas/payment_method_details_card_installments_plan' + title: >- + ConfirmationTokensResourcePaymentMethodOptionsResourceCardResourceInstallment + type: object + x-expandableFields: + - plan + confirmation_tokens_resource_payment_method_preview: + description: Details of the PaymentMethod collected by Payment Element + properties: + acss_debit: + $ref: '#/components/schemas/payment_method_acss_debit' + affirm: + $ref: '#/components/schemas/payment_method_affirm' + afterpay_clearpay: + $ref: '#/components/schemas/payment_method_afterpay_clearpay' + alipay: + $ref: '#/components/schemas/payment_flows_private_payment_methods_alipay' + allow_redisplay: + description: >- + This field indicates whether this payment method can be shown again + to its customer in a checkout flow. Stripe products such as Checkout + and Elements use this field to determine whether a payment method + can be shown as a saved payment method in a checkout flow. The field + defaults to “unspecified”. + enum: + - always + - limited + - unspecified + type: string + alma: + $ref: '#/components/schemas/payment_method_alma' + amazon_pay: + $ref: '#/components/schemas/payment_method_amazon_pay' + au_becs_debit: + $ref: '#/components/schemas/payment_method_au_becs_debit' + bacs_debit: + $ref: '#/components/schemas/payment_method_bacs_debit' + bancontact: + $ref: '#/components/schemas/payment_method_bancontact' + billie: + $ref: '#/components/schemas/payment_method_billie' + billing_details: + $ref: '#/components/schemas/billing_details' + blik: + $ref: '#/components/schemas/payment_method_blik' + boleto: + $ref: '#/components/schemas/payment_method_boleto' + card: + $ref: '#/components/schemas/payment_method_card' + card_present: + $ref: '#/components/schemas/payment_method_card_present' + cashapp: + $ref: '#/components/schemas/payment_method_cashapp' + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: >- + The ID of the Customer to which this PaymentMethod is saved. This + will not be set when the PaymentMethod has not been saved to a + Customer. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + customer_balance: + $ref: '#/components/schemas/payment_method_customer_balance' + eps: + $ref: '#/components/schemas/payment_method_eps' + fpx: + $ref: '#/components/schemas/payment_method_fpx' + giropay: + $ref: '#/components/schemas/payment_method_giropay' + grabpay: + $ref: '#/components/schemas/payment_method_grabpay' + ideal: + $ref: '#/components/schemas/payment_method_ideal' + interac_present: + $ref: '#/components/schemas/payment_method_interac_present' + kakao_pay: + $ref: '#/components/schemas/payment_method_kakao_pay' + klarna: + $ref: '#/components/schemas/payment_method_klarna' + konbini: + $ref: '#/components/schemas/payment_method_konbini' + kr_card: + $ref: '#/components/schemas/payment_method_kr_card' + link: + $ref: '#/components/schemas/payment_method_link' + mobilepay: + $ref: '#/components/schemas/payment_method_mobilepay' + multibanco: + $ref: '#/components/schemas/payment_method_multibanco' + naver_pay: + $ref: '#/components/schemas/payment_method_naver_pay' + nz_bank_account: + $ref: '#/components/schemas/payment_method_nz_bank_account' + oxxo: + $ref: '#/components/schemas/payment_method_oxxo' + p24: + $ref: '#/components/schemas/payment_method_p24' + pay_by_bank: + $ref: '#/components/schemas/payment_method_pay_by_bank' + payco: + $ref: '#/components/schemas/payment_method_payco' + paynow: + $ref: '#/components/schemas/payment_method_paynow' + paypal: + $ref: '#/components/schemas/payment_method_paypal' + pix: + $ref: '#/components/schemas/payment_method_pix' + promptpay: + $ref: '#/components/schemas/payment_method_promptpay' + revolut_pay: + $ref: '#/components/schemas/payment_method_revolut_pay' + samsung_pay: + $ref: '#/components/schemas/payment_method_samsung_pay' + satispay: + $ref: '#/components/schemas/payment_method_satispay' + sepa_debit: + $ref: '#/components/schemas/payment_method_sepa_debit' + sofort: + $ref: '#/components/schemas/payment_method_sofort' + swish: + $ref: '#/components/schemas/payment_method_swish' + twint: + $ref: '#/components/schemas/payment_method_twint' + type: + description: >- + The type of the PaymentMethod. An additional hash is included on the + PaymentMethod with a name matching this value. It contains + additional information specific to the PaymentMethod type. + enum: + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - billie + - blik + - boleto + - card + - card_present + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - interac_present + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - nz_bank_account + - oxxo + - p24 + - pay_by_bank + - payco + - paynow + - paypal + - pix + - promptpay + - revolut_pay + - samsung_pay + - satispay + - sepa_debit + - sofort + - swish + - twint + - us_bank_account + - wechat_pay + - zip + type: string + x-stripeBypassValidation: true + us_bank_account: + $ref: '#/components/schemas/payment_method_us_bank_account' + wechat_pay: + $ref: '#/components/schemas/payment_method_wechat_pay' + zip: + $ref: '#/components/schemas/payment_method_zip' + required: + - billing_details + - type + title: ConfirmationTokensResourcePaymentMethodPreview + type: object + x-expandableFields: + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - billie + - billing_details + - blik + - boleto + - card + - card_present + - cashapp + - customer + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - interac_present + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - nz_bank_account + - oxxo + - p24 + - pay_by_bank + - payco + - paynow + - paypal + - pix + - promptpay + - revolut_pay + - samsung_pay + - satispay + - sepa_debit + - sofort + - swish + - twint + - us_bank_account + - wechat_pay + - zip + confirmation_tokens_resource_shipping: + description: '' + properties: + address: + $ref: '#/components/schemas/address' + name: + description: Recipient name. + maxLength: 5000 + type: string + phone: + description: Recipient phone (including extension). + maxLength: 5000 + nullable: true + type: string + required: + - address + - name + title: ConfirmationTokensResourceShipping + type: object + x-expandableFields: + - address + connect_account_reference: + description: '' + properties: + account: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: The connected account being referenced when `type` is `account`. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + type: + description: Type of the account referenced. + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: ConnectAccountReference + type: object + x-expandableFields: + - account + connect_collection_transfer: + description: '' + properties: + amount: + description: 'Amount transferred, in cents (or local equivalent).' + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + destination: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: ID of the account that funds are being collected for. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - connect_collection_transfer + type: string + required: + - amount + - currency + - destination + - id + - livemode + - object + title: ConnectCollectionTransfer + type: object + x-expandableFields: + - destination + connect_embedded_account_config_claim: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_account_features_claim' + required: + - enabled + - features + title: ConnectEmbeddedAccountConfigClaim + type: object + x-expandableFields: + - features + connect_embedded_account_features_claim: + description: '' + properties: + disable_stripe_user_authentication: + description: >- + Disables Stripe user authentication for this embedded component. + This value can only be true for accounts where + `controller.requirement_collection` is `application`. The default + value is the opposite of the `external_account_collection` value. + For example, if you don’t set `external_account_collection`, it + defaults to true and `disable_stripe_user_authentication` defaults + to false. + type: boolean + external_account_collection: + description: >- + Whether to allow platforms to control bank account collection for + their connected accounts. This feature can only be false for + accounts where you’re responsible for collecting updated information + when requirements are due or change, like custom accounts. + Otherwise, bank account collection is determined by compliance + requirements. The default value for this feature is `true`. + type: boolean + required: + - disable_stripe_user_authentication + - external_account_collection + title: ConnectEmbeddedAccountFeaturesClaim + type: object + x-expandableFields: [] + connect_embedded_account_session_create_components: + description: '' + properties: + account_management: + $ref: '#/components/schemas/connect_embedded_account_config_claim' + account_onboarding: + $ref: '#/components/schemas/connect_embedded_account_config_claim' + balances: + $ref: '#/components/schemas/connect_embedded_payouts_config' + disputes_list: + $ref: '#/components/schemas/connect_embedded_disputes_list_config' + documents: + $ref: '#/components/schemas/connect_embedded_base_config_claim' + financial_account: + $ref: '#/components/schemas/connect_embedded_financial_account_config_claim' + financial_account_transactions: + $ref: >- + #/components/schemas/connect_embedded_financial_account_transactions_config_claim + issuing_card: + $ref: '#/components/schemas/connect_embedded_issuing_card_config_claim' + issuing_cards_list: + $ref: >- + #/components/schemas/connect_embedded_issuing_cards_list_config_claim + notification_banner: + $ref: '#/components/schemas/connect_embedded_account_config_claim' + payment_details: + $ref: '#/components/schemas/connect_embedded_payments_config_claim' + payment_disputes: + $ref: '#/components/schemas/connect_embedded_payment_disputes_config' + payments: + $ref: '#/components/schemas/connect_embedded_payments_config_claim' + payouts: + $ref: '#/components/schemas/connect_embedded_payouts_config' + payouts_list: + $ref: '#/components/schemas/connect_embedded_base_config_claim' + tax_registrations: + $ref: '#/components/schemas/connect_embedded_base_config_claim' + tax_settings: + $ref: '#/components/schemas/connect_embedded_base_config_claim' + required: + - account_management + - account_onboarding + - balances + - disputes_list + - documents + - financial_account + - financial_account_transactions + - issuing_card + - issuing_cards_list + - notification_banner + - payment_details + - payment_disputes + - payments + - payouts + - payouts_list + - tax_registrations + - tax_settings + title: ConnectEmbeddedAccountSessionCreateComponents + type: object + x-expandableFields: + - account_management + - account_onboarding + - balances + - disputes_list + - documents + - financial_account + - financial_account_transactions + - issuing_card + - issuing_cards_list + - notification_banner + - payment_details + - payment_disputes + - payments + - payouts + - payouts_list + - tax_registrations + - tax_settings + connect_embedded_base_config_claim: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_base_features' + required: + - enabled + - features + title: ConnectEmbeddedBaseConfigClaim + type: object + x-expandableFields: + - features + connect_embedded_base_features: + description: '' + properties: {} + title: ConnectEmbeddedBaseFeatures + type: object + x-expandableFields: [] + connect_embedded_disputes_list_config: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_disputes_list_features' + required: + - enabled + - features + title: ConnectEmbeddedDisputesListConfig + type: object + x-expandableFields: + - features + connect_embedded_disputes_list_features: + description: '' + properties: + capture_payments: + description: >- + Whether to allow capturing and cancelling payment intents. This is + `true` by default. + type: boolean + destination_on_behalf_of_charge_management: + description: >- + Whether to allow connected accounts to manage destination charges + that are created on behalf of them. This is `false` by default. + type: boolean + dispute_management: + description: >- + Whether to allow responding to disputes, including submitting + evidence and accepting disputes. This is `true` by default. + type: boolean + refund_management: + description: Whether to allow sending refunds. This is `true` by default. + type: boolean + required: + - capture_payments + - destination_on_behalf_of_charge_management + - dispute_management + - refund_management + title: ConnectEmbeddedDisputesListFeatures + type: object + x-expandableFields: [] + connect_embedded_financial_account_config_claim: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_financial_account_features' + required: + - enabled + - features + title: ConnectEmbeddedFinancialAccountConfigClaim + type: object + x-expandableFields: + - features + connect_embedded_financial_account_features: + description: '' + properties: + disable_stripe_user_authentication: + description: >- + Disables Stripe user authentication for this embedded component. + This value can only be true for accounts where + `controller.requirement_collection` is `application`. The default + value is the opposite of the `external_account_collection` value. + For example, if you don’t set `external_account_collection`, it + defaults to true and `disable_stripe_user_authentication` defaults + to false. + type: boolean + external_account_collection: + description: Whether to allow external accounts to be linked for money transfer. + type: boolean + send_money: + description: Whether to allow sending money. + type: boolean + transfer_balance: + description: Whether to allow transferring balance. + type: boolean + required: + - disable_stripe_user_authentication + - external_account_collection + - send_money + - transfer_balance + title: ConnectEmbeddedFinancialAccountFeatures + type: object + x-expandableFields: [] + connect_embedded_financial_account_transactions_config_claim: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: >- + #/components/schemas/connect_embedded_financial_account_transactions_features + required: + - enabled + - features + title: ConnectEmbeddedFinancialAccountTransactionsConfigClaim + type: object + x-expandableFields: + - features + connect_embedded_financial_account_transactions_features: + description: '' + properties: + card_spend_dispute_management: + description: Whether to allow card spend dispute management features. + type: boolean + required: + - card_spend_dispute_management + title: ConnectEmbeddedFinancialAccountTransactionsFeatures + type: object + x-expandableFields: [] + connect_embedded_issuing_card_config_claim: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_issuing_card_features' + required: + - enabled + - features + title: ConnectEmbeddedIssuingCardConfigClaim + type: object + x-expandableFields: + - features + connect_embedded_issuing_card_features: + description: '' + properties: + card_management: + description: Whether to allow card management features. + type: boolean + card_spend_dispute_management: + description: Whether to allow card spend dispute management features. + type: boolean + cardholder_management: + description: Whether to allow cardholder management features. + type: boolean + spend_control_management: + description: Whether to allow spend control management features. + type: boolean + required: + - card_management + - card_spend_dispute_management + - cardholder_management + - spend_control_management + title: ConnectEmbeddedIssuingCardFeatures + type: object + x-expandableFields: [] + connect_embedded_issuing_cards_list_config_claim: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_issuing_cards_list_features' + required: + - enabled + - features + title: ConnectEmbeddedIssuingCardsListConfigClaim + type: object + x-expandableFields: + - features + connect_embedded_issuing_cards_list_features: + description: '' + properties: + card_management: + description: Whether to allow card management features. + type: boolean + card_spend_dispute_management: + description: Whether to allow card spend dispute management features. + type: boolean + cardholder_management: + description: Whether to allow cardholder management features. + type: boolean + disable_stripe_user_authentication: + description: >- + Disables Stripe user authentication for this embedded component. + This feature can only be false for accounts where you’re responsible + for collecting updated information when requirements are due or + change, like custom accounts. + type: boolean + spend_control_management: + description: Whether to allow spend control management features. + type: boolean + required: + - card_management + - card_spend_dispute_management + - cardholder_management + - disable_stripe_user_authentication + - spend_control_management + title: ConnectEmbeddedIssuingCardsListFeatures + type: object + x-expandableFields: [] + connect_embedded_payment_disputes_config: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_payment_disputes_features' + required: + - enabled + - features + title: ConnectEmbeddedPaymentDisputesConfig + type: object + x-expandableFields: + - features + connect_embedded_payment_disputes_features: + description: '' + properties: + destination_on_behalf_of_charge_management: + description: >- + Whether to allow connected accounts to manage destination charges + that are created on behalf of them. This is `false` by default. + type: boolean + dispute_management: + description: >- + Whether to allow responding to disputes, including submitting + evidence and accepting disputes. This is `true` by default. + type: boolean + refund_management: + description: Whether to allow sending refunds. This is `true` by default. + type: boolean + required: + - destination_on_behalf_of_charge_management + - dispute_management + - refund_management + title: ConnectEmbeddedPaymentDisputesFeatures + type: object + x-expandableFields: [] + connect_embedded_payments_config_claim: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_payments_features' + required: + - enabled + - features + title: ConnectEmbeddedPaymentsConfigClaim + type: object + x-expandableFields: + - features + connect_embedded_payments_features: + description: '' + properties: + capture_payments: + description: >- + Whether to allow capturing and cancelling payment intents. This is + `true` by default. + type: boolean + destination_on_behalf_of_charge_management: + description: >- + Whether to allow connected accounts to manage destination charges + that are created on behalf of them. This is `false` by default. + type: boolean + dispute_management: + description: >- + Whether to allow responding to disputes, including submitting + evidence and accepting disputes. This is `true` by default. + type: boolean + refund_management: + description: Whether to allow sending refunds. This is `true` by default. + type: boolean + required: + - capture_payments + - destination_on_behalf_of_charge_management + - dispute_management + - refund_management + title: ConnectEmbeddedPaymentsFeatures + type: object + x-expandableFields: [] + connect_embedded_payouts_config: + description: '' + properties: + enabled: + description: Whether the embedded component is enabled. + type: boolean + features: + $ref: '#/components/schemas/connect_embedded_payouts_features' + required: + - enabled + - features + title: ConnectEmbeddedPayoutsConfig + type: object + x-expandableFields: + - features + connect_embedded_payouts_features: + description: '' + properties: + disable_stripe_user_authentication: + description: >- + Disables Stripe user authentication for this embedded component. + This value can only be true for accounts where + `controller.requirement_collection` is `application`. The default + value is the opposite of the `external_account_collection` value. + For example, if you don’t set `external_account_collection`, it + defaults to true and `disable_stripe_user_authentication` defaults + to false. + type: boolean + edit_payout_schedule: + description: >- + Whether to allow payout schedule to be changed. Default `true` when + Stripe owns Loss Liability, default `false` otherwise. + type: boolean + external_account_collection: + description: >- + Whether to allow platforms to control bank account collection for + their connected accounts. This feature can only be false for + accounts where you’re responsible for collecting updated information + when requirements are due or change, like custom accounts. + Otherwise, bank account collection is determined by compliance + requirements. The default value for this feature is `true`. + type: boolean + instant_payouts: + description: >- + Whether to allow creation of instant payouts. Default `true` when + Stripe owns Loss Liability, default `false` otherwise. + type: boolean + standard_payouts: + description: >- + Whether to allow creation of standard payouts. Default `true` when + Stripe owns Loss Liability, default `false` otherwise. + type: boolean + required: + - disable_stripe_user_authentication + - edit_payout_schedule + - external_account_collection + - instant_payouts + - standard_payouts + title: ConnectEmbeddedPayoutsFeatures + type: object + x-expandableFields: [] + country_spec: + description: >- + Stripe needs to collect certain pieces of information about each account + + created. These requirements can differ depending on the account's + country. The + + Country Specs API makes these rules available to your integration. + + + You can also view the information from this API call as [an online + + guide](/docs/connect/required-verification-information). + properties: + default_currency: + description: >- + The default currency for this country. This applies to both payment + methods and bank accounts. + maxLength: 5000 + type: string + id: + description: >- + Unique identifier for the object. Represented as the ISO country + code for this country. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - country_spec + type: string + supported_bank_account_currencies: + additionalProperties: + items: + maxLength: 5000 + type: string + type: array + description: >- + Currencies that can be accepted in the specific country (for + transfers). + type: object + supported_payment_currencies: + description: >- + Currencies that can be accepted in the specified country (for + payments). + items: + maxLength: 5000 + type: string + type: array + supported_payment_methods: + description: >- + Payment methods available in the specified country. You may need to + enable some payment methods (e.g., + [ACH](https://stripe.com/docs/ach)) on your account before they + appear in this list. The `stripe` payment method refers to [charging + through your + platform](https://stripe.com/docs/connect/destination-charges). + items: + maxLength: 5000 + type: string + type: array + supported_transfer_countries: + description: Countries that can accept transfers from the specified country. + items: + maxLength: 5000 + type: string + type: array + verification_fields: + $ref: '#/components/schemas/country_spec_verification_fields' + required: + - default_currency + - id + - object + - supported_bank_account_currencies + - supported_payment_currencies + - supported_payment_methods + - supported_transfer_countries + - verification_fields + title: CountrySpec + type: object + x-expandableFields: + - verification_fields + x-resourceId: country_spec + country_spec_verification_field_details: + description: '' + properties: + additional: + description: Additional fields which are only required for some users. + items: + maxLength: 5000 + type: string + type: array + minimum: + description: Fields which every account must eventually provide. + items: + maxLength: 5000 + type: string + type: array + required: + - additional + - minimum + title: CountrySpecVerificationFieldDetails + type: object + x-expandableFields: [] + country_spec_verification_fields: + description: '' + properties: + company: + $ref: '#/components/schemas/country_spec_verification_field_details' + individual: + $ref: '#/components/schemas/country_spec_verification_field_details' + required: + - company + - individual + title: CountrySpecVerificationFields + type: object + x-expandableFields: + - company + - individual + coupon: + description: >- + A coupon contains information about a percent-off or amount-off discount + you + + might want to apply to a customer. Coupons may be applied to + [subscriptions](https://stripe.com/docs/api#subscriptions), + [invoices](https://stripe.com/docs/api#invoices), + + [checkout sessions](https://stripe.com/docs/api/checkout/sessions), + [quotes](https://stripe.com/docs/api#quotes), and more. Coupons do not + work with conventional one-off + [charges](https://stripe.com/docs/api#create_charge) or [payment + intents](https://stripe.com/docs/api/payment_intents). + properties: + amount_off: + description: >- + Amount (in the `currency` specified) that will be taken off the + subtotal of any invoices for this customer. + nullable: true + type: integer + applies_to: + $ref: '#/components/schemas/coupon_applies_to' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + If `amount_off` has been set, the three-letter [ISO code for the + currency](https://stripe.com/docs/currencies) of the amount to take + off. + format: currency + nullable: true + type: string + currency_options: + additionalProperties: + $ref: '#/components/schemas/coupon_currency_option' + description: >- + Coupons defined in each available currency option. Each key must be + a three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html) and a + [supported currency](https://stripe.com/docs/currencies). + type: object + duration: + description: >- + One of `forever`, `once`, or `repeating`. Describes how long a + customer who applies this coupon will get the discount. + enum: + - forever + - once + - repeating + type: string + x-stripeBypassValidation: true + duration_in_months: + description: >- + If `duration` is `repeating`, the number of months the coupon + applies. Null if coupon `duration` is `forever` or `once`. + nullable: true + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + max_redemptions: + description: >- + Maximum number of times this coupon can be redeemed, in total, + across all customers, before it is no longer valid. + nullable: true + type: integer + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + name: + description: >- + Name of the coupon displayed to customers on for instance invoices + or receipts. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - coupon + type: string + percent_off: + description: >- + Percent that will be taken off the subtotal of any invoices for this + customer for the duration of the coupon. For example, a coupon with + percent_off of 50 will make a $ (or local equivalent)100 invoice $ + (or local equivalent)50 instead. + nullable: true + type: number + redeem_by: + description: Date after which the coupon can no longer be redeemed. + format: unix-time + nullable: true + type: integer + times_redeemed: + description: Number of times this coupon has been applied to a customer. + type: integer + valid: + description: >- + Taking account of the above properties, whether this coupon can + still be applied to a customer. + type: boolean + required: + - created + - duration + - id + - livemode + - object + - times_redeemed + - valid + title: Coupon + type: object + x-expandableFields: + - applies_to + - currency_options + x-resourceId: coupon + coupon_applies_to: + description: '' + properties: + products: + description: A list of product IDs this coupon applies to + items: + maxLength: 5000 + type: string + type: array + required: + - products + title: CouponAppliesTo + type: object + x-expandableFields: [] + coupon_currency_option: + description: '' + properties: + amount_off: + description: >- + Amount (in the `currency` specified) that will be taken off the + subtotal of any invoices for this customer. + type: integer + required: + - amount_off + title: CouponCurrencyOption + type: object + x-expandableFields: [] + credit_balance: + description: '' + properties: + available_balance: + $ref: '#/components/schemas/billing_credit_grants_resource_amount' + ledger_balance: + $ref: '#/components/schemas/billing_credit_grants_resource_amount' + required: + - available_balance + - ledger_balance + title: CreditBalance + type: object + x-expandableFields: + - available_balance + - ledger_balance + credit_note: + description: >- + Issue a credit note to adjust an invoice's amount after the invoice is + finalized. + + + Related guide: [Credit + notes](https://stripe.com/docs/billing/invoices/credit-notes) + properties: + amount: + description: >- + The integer amount in cents (or local equivalent) representing the + total amount of the credit note, including tax. + type: integer + amount_shipping: + description: This is the sum of all the shipping amounts. + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: ID of the customer. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + customer_balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer_balance_transaction' + description: Customer balance transaction related to this credit note. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer_balance_transaction' + discount_amount: + description: >- + The integer amount in cents (or local equivalent) representing the + total amount of discount that was credited. + type: integer + discount_amounts: + description: The aggregate amounts calculated per discount for all line items. + items: + $ref: '#/components/schemas/discounts_resource_discount_amount' + type: array + effective_at: + description: >- + The date when this credit note is in effect. Same as `created` + unless overwritten. When defined, this value replaces the + system-generated 'Date of issue' printed on the credit note PDF. + format: unix-time + nullable: true + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: ID of the invoice. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + lines: + description: Line items that make up the credit note + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/credit_note_line_item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: CreditNoteLinesList + type: object + x-expandableFields: + - data + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + memo: + description: Customer-facing text that appears on the credit note PDF. + maxLength: 5000 + nullable: true + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + number: + description: >- + A unique number that identifies this particular credit note and + appears on the PDF of the credit note and its associated invoice. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - credit_note + type: string + out_of_band_amount: + description: Amount that was credited outside of Stripe. + nullable: true + type: integer + pdf: + description: The link to download the PDF of the credit note. + maxLength: 5000 + type: string + post_payment_amount: + description: >- + The amount of the credit note that was refunded to the customer, + credited to the customer's balance, credited outside of Stripe, or + any combination thereof. + type: integer + pre_payment_amount: + description: >- + The amount of the credit note by which the invoice's + `amount_remaining` and `amount_due` were reduced. + type: integer + pretax_credit_amounts: + description: >- + The pretax credit amounts (ex: discount, credit grants, etc) for all + line items. + items: + $ref: '#/components/schemas/credit_notes_pretax_credit_amount' + type: array + reason: + description: >- + Reason for issuing this credit note, one of `duplicate`, + `fraudulent`, `order_change`, or `product_unsatisfactory` + enum: + - duplicate + - fraudulent + - order_change + - product_unsatisfactory + nullable: true + type: string + refunds: + description: Refunds related to this credit note. + items: + $ref: '#/components/schemas/credit_note_refund' + type: array + shipping_cost: + anyOf: + - $ref: '#/components/schemas/invoices_resource_shipping_cost' + description: >- + The details of the cost of shipping, including the ShippingRate + applied to the invoice. + nullable: true + status: + description: >- + Status of this credit note, one of `issued` or `void`. Learn more + about [voiding credit + notes](https://stripe.com/docs/billing/invoices/credit-notes#voiding). + enum: + - issued + - void + type: string + x-stripeBypassValidation: true + subtotal: + description: >- + The integer amount in cents (or local equivalent) representing the + amount of the credit note, excluding exclusive tax and invoice level + discounts. + type: integer + subtotal_excluding_tax: + description: >- + The integer amount in cents (or local equivalent) representing the + amount of the credit note, excluding all tax and invoice level + discounts. + nullable: true + type: integer + total: + description: >- + The integer amount in cents (or local equivalent) representing the + total amount of the credit note, including tax and all discount. + type: integer + total_excluding_tax: + description: >- + The integer amount in cents (or local equivalent) representing the + total amount of the credit note, excluding tax, but including + discounts. + nullable: true + type: integer + total_taxes: + description: The aggregate tax information for all line items. + items: + $ref: '#/components/schemas/billing_bill_resource_invoicing_taxes_tax' + nullable: true + type: array + type: + description: >- + Type of this credit note, one of `pre_payment` or `post_payment`. A + `pre_payment` credit note means it was issued when the invoice was + open. A `post_payment` credit note means it was issued when the + invoice was paid. + enum: + - mixed + - post_payment + - pre_payment + type: string + voided_at: + description: The time that the credit note was voided. + format: unix-time + nullable: true + type: integer + required: + - amount + - amount_shipping + - created + - currency + - customer + - discount_amount + - discount_amounts + - id + - invoice + - lines + - livemode + - number + - object + - pdf + - post_payment_amount + - pre_payment_amount + - pretax_credit_amounts + - refunds + - status + - subtotal + - total + - type + title: CreditNote + type: object + x-expandableFields: + - customer + - customer_balance_transaction + - discount_amounts + - invoice + - lines + - pretax_credit_amounts + - refunds + - shipping_cost + - total_taxes + x-resourceId: credit_note + credit_note_line_item: + description: The credit note line item object + properties: + amount: + description: >- + The integer amount in cents (or local equivalent) representing the + gross amount being credited for this line item, excluding + (exclusive) tax and discounts. + type: integer + description: + description: Description of the item being credited. + maxLength: 5000 + nullable: true + type: string + discount_amount: + description: >- + The integer amount in cents (or local equivalent) representing the + discount being credited for this line item. + type: integer + discount_amounts: + description: The amount of discount calculated per discount for this line item + items: + $ref: '#/components/schemas/discounts_resource_discount_amount' + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice_line_item: + description: ID of the invoice line item being credited + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - credit_note_line_item + type: string + pretax_credit_amounts: + description: >- + The pretax credit amounts (ex: discount, credit grants, etc) for + this line item. + items: + $ref: '#/components/schemas/credit_notes_pretax_credit_amount' + type: array + quantity: + description: The number of units of product being credited. + nullable: true + type: integer + tax_rates: + description: The tax rates which apply to the line item. + items: + $ref: '#/components/schemas/tax_rate' + type: array + taxes: + description: The tax information of the line item. + items: + $ref: '#/components/schemas/billing_bill_resource_invoicing_taxes_tax' + nullable: true + type: array + type: + description: >- + The type of the credit note line item, one of `invoice_line_item` or + `custom_line_item`. When the type is `invoice_line_item` there is an + additional `invoice_line_item` property on the resource the value of + which is the id of the credited line item on the invoice. + enum: + - custom_line_item + - invoice_line_item + type: string + unit_amount: + description: The cost of each unit of product being credited. + nullable: true + type: integer + unit_amount_decimal: + description: >- + Same as `unit_amount`, but contains a decimal value with at most 12 + decimal places. + format: decimal + nullable: true + type: string + required: + - amount + - discount_amount + - discount_amounts + - id + - livemode + - object + - pretax_credit_amounts + - tax_rates + - type + title: CreditNoteLineItem + type: object + x-expandableFields: + - discount_amounts + - pretax_credit_amounts + - tax_rates + - taxes + x-resourceId: credit_note_line_item + credit_note_refund: + description: '' + properties: + amount_refunded: + description: >- + Amount of the refund that applies to this credit note, in cents (or + local equivalent). + type: integer + refund: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/refund' + description: ID of the refund. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/refund' + required: + - amount_refunded + - refund + title: CreditNoteRefund + type: object + x-expandableFields: + - refund + credit_notes_pretax_credit_amount: + description: '' + properties: + amount: + description: >- + The amount, in cents (or local equivalent), of the pretax credit + amount. + type: integer + credit_balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/billing.credit_balance_transaction' + description: >- + The credit balance transaction that was applied to get this pretax + credit amount. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/billing.credit_balance_transaction' + discount: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + - $ref: '#/components/schemas/deleted_discount' + description: The discount that was applied to get this pretax credit amount. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + - $ref: '#/components/schemas/deleted_discount' + type: + description: Type of the pretax credit amount referenced. + enum: + - credit_balance_transaction + - discount + type: string + required: + - amount + - type + title: CreditNotesPretaxCreditAmount + type: object + x-expandableFields: + - credit_balance_transaction + - discount + currency_option: + description: '' + properties: + custom_unit_amount: + anyOf: + - $ref: '#/components/schemas/custom_unit_amount' + description: >- + When set, provides configuration for the amount to be adjusted by + the customer during Checkout Sessions and Payment Links. + nullable: true + tax_behavior: + description: >- + Only required if a [default tax + behavior](https://stripe.com/docs/tax/products-prices-tax-categories-tax-behavior#setting-a-default-tax-behavior-(recommended)) + was not provided in the Stripe Tax settings. Specifies whether the + price is considered inclusive of taxes or exclusive of taxes. One of + `inclusive`, `exclusive`, or `unspecified`. Once specified as either + `inclusive` or `exclusive`, it cannot be changed. + enum: + - exclusive + - inclusive + - unspecified + nullable: true + type: string + tiers: + description: >- + Each element represents a pricing tier. This parameter requires + `billing_scheme` to be set to `tiered`. See also the documentation + for `billing_scheme`. + items: + $ref: '#/components/schemas/price_tier' + type: array + unit_amount: + description: >- + The unit amount in cents (or local equivalent) to be charged, + represented as a whole integer if possible. Only set if + `billing_scheme=per_unit`. + nullable: true + type: integer + unit_amount_decimal: + description: >- + The unit amount in cents (or local equivalent) to be charged, + represented as a decimal string with at most 12 decimal places. Only + set if `billing_scheme=per_unit`. + format: decimal + nullable: true + type: string + title: CurrencyOption + type: object + x-expandableFields: + - custom_unit_amount + - tiers + custom_unit_amount: + description: '' + properties: + maximum: + description: The maximum unit amount the customer can specify for this item. + nullable: true + type: integer + minimum: + description: >- + The minimum unit amount the customer can specify for this item. Must + be at least the minimum charge amount. + nullable: true + type: integer + preset: + description: The starting unit amount which can be updated by the customer. + nullable: true + type: integer + title: CustomUnitAmount + type: object + x-expandableFields: [] + customer: + description: >- + This object represents a customer of your business. Use it to [create + recurring charges](https://stripe.com/docs/invoicing/customer), [save + payment](https://stripe.com/docs/payments/save-during-payment) and + contact information, + + and track payments that belong to the same customer. + properties: + address: + anyOf: + - $ref: '#/components/schemas/address' + description: The customer's address. + nullable: true + balance: + description: >- + The current balance, if any, that's stored on the customer. If + negative, the customer has credit to apply to their next invoice. If + positive, the customer has an amount owed that's added to their next + invoice. The balance only considers amounts that Stripe hasn't + successfully applied to any invoice. It doesn't reflect unpaid + invoices. This balance is only taken into account after invoices + finalize. + type: integer + cash_balance: + anyOf: + - $ref: '#/components/schemas/cash_balance' + description: >- + The current funds being held by Stripe on behalf of the customer. + You can apply these funds towards payment intents when the source is + "cash_balance". The `settings[reconciliation_mode]` field describes + if these funds apply to these payment intents manually or + automatically. + nullable: true + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO code for the + currency](https://stripe.com/docs/currencies) the customer can be + charged in for recurring billing purposes. + maxLength: 5000 + nullable: true + type: string + default_source: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + description: >- + ID of the default payment source for the customer. + + + If you use payment methods created through the PaymentMethods API, + see the + [invoice_settings.default_payment_method](https://stripe.com/docs/api/customers/object#customer_object-invoice_settings-default_payment_method) + field instead. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + x-stripeBypassValidation: true + delinquent: + description: >- + Tracks the most recent state change on any invoice belonging to the + customer. Paying an invoice or marking it uncollectible via the API + will set this field to false. An automatic payment failure or + passing the `invoice.due_date` will set this field to `true`. + + + If an invoice becomes uncollectible by + [dunning](https://stripe.com/docs/billing/automatic-collection), + `delinquent` doesn't reset to `false`. + + + If you care whether the customer has paid their most recent + subscription invoice, use `subscription.status` instead. Paying or + marking uncollectible any customer invoice regardless of whether it + is the latest invoice for a subscription will always set this field + to `false`. + nullable: true + type: boolean + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + discount: + anyOf: + - $ref: '#/components/schemas/discount' + description: >- + Describes the current discount active on the customer, if there is + one. + nullable: true + email: + description: The customer's email address. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice_credit_balance: + additionalProperties: + type: integer + description: >- + The current multi-currency balances, if any, that's stored on the + customer. If positive in a currency, the customer has a credit to + apply to their next invoice denominated in that currency. If + negative, the customer has an amount owed that's added to their next + invoice denominated in that currency. These balances don't apply to + unpaid invoices. They solely track amounts that Stripe hasn't + successfully applied to any invoice. Stripe only applies a balance + in a specific currency to an invoice after that invoice (which is in + the same currency) finalizes. + type: object + invoice_prefix: + description: The prefix for the customer used to generate unique invoice numbers. + maxLength: 5000 + nullable: true + type: string + invoice_settings: + $ref: '#/components/schemas/invoice_setting_customer_setting' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + name: + description: The customer's full name or business name. + maxLength: 5000 + nullable: true + type: string + next_invoice_sequence: + description: >- + The suffix of the customer's next invoice number (for example, + 0001). When the account uses account level sequencing, this + parameter is ignored in API requests and the field omitted in API + responses. + type: integer + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - customer + type: string + phone: + description: The customer's phone number. + maxLength: 5000 + nullable: true + type: string + preferred_locales: + description: 'The customer''s preferred locales (languages), ordered by preference.' + items: + maxLength: 5000 + type: string + nullable: true + type: array + shipping: + anyOf: + - $ref: '#/components/schemas/shipping' + description: >- + Mailing and shipping address for the customer. Appears on invoices + emailed to this customer. + nullable: true + sources: + description: 'The customer''s payment sources, if any.' + properties: + data: + description: Details about each object. + items: + anyOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + title: Polymorphic + x-stripeBypassValidation: true + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: ApmsSourcesSourceList + type: object + x-expandableFields: + - data + subscriptions: + description: 'The customer''s current subscriptions, if any.' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/subscription' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: SubscriptionList + type: object + x-expandableFields: + - data + tax: + $ref: '#/components/schemas/customer_tax' + tax_exempt: + description: >- + Describes the customer's tax exemption status, which is `none`, + `exempt`, or `reverse`. When set to `reverse`, invoice and receipt + PDFs include the following text: **"Reverse charge"**. + enum: + - exempt + - none + - reverse + nullable: true + type: string + tax_ids: + description: The customer's tax IDs. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/tax_id' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: TaxIDsList + type: object + x-expandableFields: + - data + test_clock: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/test_helpers.test_clock' + description: ID of the test clock that this customer belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/test_helpers.test_clock' + required: + - created + - id + - livemode + - object + title: Customer + type: object + x-expandableFields: + - address + - cash_balance + - default_source + - discount + - invoice_settings + - shipping + - sources + - subscriptions + - tax + - tax_ids + - test_clock + x-resourceId: customer + customer_acceptance: + description: '' + properties: + accepted_at: + description: The time that the customer accepts the mandate. + format: unix-time + nullable: true + type: integer + offline: + $ref: '#/components/schemas/offline_acceptance' + online: + $ref: '#/components/schemas/online_acceptance' + type: + description: >- + The mandate includes the type of customer acceptance information, + such as: `online` or `offline`. + enum: + - offline + - online + type: string + required: + - type + title: customer_acceptance + type: object + x-expandableFields: + - offline + - online + customer_balance_customer_balance_settings: + description: '' + properties: + reconciliation_mode: + description: >- + The configuration for how funds that land in the customer cash + balance are reconciled. + enum: + - automatic + - manual + type: string + using_merchant_default: + description: >- + A flag to indicate if reconciliation mode returned is the user's + default or is specific to this customer cash balance + type: boolean + required: + - reconciliation_mode + - using_merchant_default + title: CustomerBalanceCustomerBalanceSettings + type: object + x-expandableFields: [] + customer_balance_resource_cash_balance_transaction_resource_adjusted_for_overdraft: + description: '' + properties: + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + The [Balance + Transaction](https://stripe.com/docs/api/balance_transactions/object) + that corresponds to funds taken out of your Stripe balance. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + linked_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer_cash_balance_transaction' + description: >- + The [Cash Balance + Transaction](https://stripe.com/docs/api/cash_balance_transactions/object) + that brought the customer balance negative, triggering the clawback + of funds. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer_cash_balance_transaction' + required: + - balance_transaction + - linked_transaction + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceAdjustedForOverdraft + type: object + x-expandableFields: + - balance_transaction + - linked_transaction + customer_balance_resource_cash_balance_transaction_resource_applied_to_payment_transaction: + description: '' + properties: + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: >- + The [Payment + Intent](https://stripe.com/docs/api/payment_intents/object) that + funds were applied to. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + required: + - payment_intent + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceAppliedToPaymentTransaction + type: object + x-expandableFields: + - payment_intent + customer_balance_resource_cash_balance_transaction_resource_funded_transaction: + description: '' + properties: + bank_transfer: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer + required: + - bank_transfer + title: CustomerBalanceResourceCashBalanceTransactionResourceFundedTransaction + type: object + x-expandableFields: + - bank_transfer + customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer: + description: '' + properties: + eu_bank_transfer: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer_resource_eu_bank_transfer + gb_bank_transfer: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer_resource_gb_bank_transfer + jp_bank_transfer: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer_resource_jp_bank_transfer + reference: + description: The user-supplied reference field on the bank transfer. + maxLength: 5000 + nullable: true + type: string + type: + description: >- + The funding method type used to fund the customer balance. Permitted + values include: `eu_bank_transfer`, `gb_bank_transfer`, + `jp_bank_transfer`, `mx_bank_transfer`, or `us_bank_transfer`. + enum: + - eu_bank_transfer + - gb_bank_transfer + - jp_bank_transfer + - mx_bank_transfer + - us_bank_transfer + type: string + x-stripeBypassValidation: true + us_bank_transfer: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer_resource_us_bank_transfer + required: + - type + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceFundedTransactionResourceBankTransfer + type: object + x-expandableFields: + - eu_bank_transfer + - gb_bank_transfer + - jp_bank_transfer + - us_bank_transfer + customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer_resource_eu_bank_transfer: + description: '' + properties: + bic: + description: The BIC of the bank of the sender of the funding. + maxLength: 5000 + nullable: true + type: string + iban_last4: + description: The last 4 digits of the IBAN of the sender of the funding. + maxLength: 5000 + nullable: true + type: string + sender_name: + description: 'The full name of the sender, as supplied by the sending bank.' + maxLength: 5000 + nullable: true + type: string + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceFundedTransactionResourceBankTransferResourceEuBankTransfer + type: object + x-expandableFields: [] + customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer_resource_gb_bank_transfer: + description: '' + properties: + account_number_last4: + description: >- + The last 4 digits of the account number of the sender of the + funding. + maxLength: 5000 + nullable: true + type: string + sender_name: + description: 'The full name of the sender, as supplied by the sending bank.' + maxLength: 5000 + nullable: true + type: string + sort_code: + description: The sort code of the bank of the sender of the funding + maxLength: 5000 + nullable: true + type: string + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceFundedTransactionResourceBankTransferResourceGbBankTransfer + type: object + x-expandableFields: [] + customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer_resource_jp_bank_transfer: + description: '' + properties: + sender_bank: + description: The name of the bank of the sender of the funding. + maxLength: 5000 + nullable: true + type: string + sender_branch: + description: The name of the bank branch of the sender of the funding. + maxLength: 5000 + nullable: true + type: string + sender_name: + description: 'The full name of the sender, as supplied by the sending bank.' + maxLength: 5000 + nullable: true + type: string + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceFundedTransactionResourceBankTransferResourceJpBankTransfer + type: object + x-expandableFields: [] + customer_balance_resource_cash_balance_transaction_resource_funded_transaction_resource_bank_transfer_resource_us_bank_transfer: + description: '' + properties: + network: + description: The banking network used for this funding. + enum: + - ach + - domestic_wire_us + - swift + type: string + sender_name: + description: 'The full name of the sender, as supplied by the sending bank.' + maxLength: 5000 + nullable: true + type: string + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceFundedTransactionResourceBankTransferResourceUsBankTransfer + type: object + x-expandableFields: [] + customer_balance_resource_cash_balance_transaction_resource_refunded_from_payment_transaction: + description: '' + properties: + refund: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/refund' + description: >- + The [Refund](https://stripe.com/docs/api/refunds/object) that moved + these funds into the customer's cash balance. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/refund' + required: + - refund + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceRefundedFromPaymentTransaction + type: object + x-expandableFields: + - refund + customer_balance_resource_cash_balance_transaction_resource_transferred_to_balance: + description: '' + properties: + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + The [Balance + Transaction](https://stripe.com/docs/api/balance_transactions/object) + that corresponds to funds transferred to your Stripe balance. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + required: + - balance_transaction + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceTransferredToBalance + type: object + x-expandableFields: + - balance_transaction + customer_balance_resource_cash_balance_transaction_resource_unapplied_from_payment_transaction: + description: '' + properties: + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: >- + The [Payment + Intent](https://stripe.com/docs/api/payment_intents/object) that + funds were unapplied from. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + required: + - payment_intent + title: >- + CustomerBalanceResourceCashBalanceTransactionResourceUnappliedFromPaymentTransaction + type: object + x-expandableFields: + - payment_intent + customer_balance_transaction: + description: >- + Each customer has a + [Balance](https://stripe.com/docs/api/customers/object#customer_object-balance) + value, + + which denotes a debit or credit that's automatically applied to their + next invoice upon finalization. + + You may modify the value directly by using the [update customer + API](https://stripe.com/docs/api/customers/update), + + or by creating a Customer Balance Transaction, which increments or + decrements the customer's `balance` by the specified `amount`. + + + Related guide: [Customer + balance](https://stripe.com/docs/billing/customer/balance) + properties: + amount: + description: >- + The amount of the transaction. A negative value is a credit for the + customer's balance, and a positive value is a debit to the + customer's `balance`. + type: integer + checkout_session: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/checkout.session' + description: >- + The ID of the checkout session (if any) that created the + transaction. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/checkout.session' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + credit_note: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/credit_note' + description: The ID of the credit note (if any) related to the transaction. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/credit_note' + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: The ID of the customer the transaction belongs to. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + ending_balance: + description: >- + The customer's `balance` after the transaction was applied. A + negative value decreases the amount due on the customer's next + invoice. A positive value increases the amount due on the customer's + next invoice. + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: The ID of the invoice (if any) related to the transaction. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - customer_balance_transaction + type: string + type: + description: >- + Transaction type: `adjustment`, `applied_to_invoice`, `credit_note`, + `initial`, `invoice_overpaid`, `invoice_too_large`, + `invoice_too_small`, `unspent_receiver_credit`, + `unapplied_from_invoice`, `checkout_session_subscription_payment`, + or `checkout_session_subscription_payment_canceled`. See the + [Customer Balance + page](https://stripe.com/docs/billing/customer/balance#types) to + learn more about transaction types. + enum: + - adjustment + - applied_to_invoice + - checkout_session_subscription_payment + - checkout_session_subscription_payment_canceled + - credit_note + - initial + - invoice_overpaid + - invoice_too_large + - invoice_too_small + - migration + - unapplied_from_invoice + - unspent_receiver_credit + type: string + x-stripeBypassValidation: true + required: + - amount + - created + - currency + - customer + - ending_balance + - id + - livemode + - object + - type + title: CustomerBalanceTransaction + type: object + x-expandableFields: + - checkout_session + - credit_note + - customer + - invoice + x-resourceId: customer_balance_transaction + customer_cash_balance_transaction: + description: >- + Customers with certain payments enabled have a cash balance, + representing funds that were paid + + by the customer to a merchant, but have not yet been allocated to a + payment. Cash Balance Transactions + + represent when funds are moved into or out of this balance. This + includes funding by the customer, allocation + + to payments, and refunds to the customer. + properties: + adjusted_for_overdraft: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_adjusted_for_overdraft + applied_to_payment: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_applied_to_payment_transaction + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: >- + The customer whose available cash balance changed as a result of + this transaction. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + ending_balance: + description: >- + The total available cash balance for the specified currency after + this transaction was applied. Represented in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + funded: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_funded_transaction + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + net_amount: + description: >- + The amount by which the cash balance changed, represented in the + [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). A positive + value represents funds being added to the cash balance, a negative + value represents funds being removed from the cash balance. + type: integer + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - customer_cash_balance_transaction + type: string + refunded_from_payment: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_refunded_from_payment_transaction + transferred_to_balance: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_transferred_to_balance + type: + description: >- + The type of the cash balance transaction. New types may be added in + future. See [Customer + Balance](https://stripe.com/docs/payments/customer-balance#types) to + learn more about these types. + enum: + - adjusted_for_overdraft + - applied_to_payment + - funded + - funding_reversed + - refunded_from_payment + - return_canceled + - return_initiated + - transferred_to_balance + - unapplied_from_payment + type: string + unapplied_from_payment: + $ref: >- + #/components/schemas/customer_balance_resource_cash_balance_transaction_resource_unapplied_from_payment_transaction + required: + - created + - currency + - customer + - ending_balance + - id + - livemode + - net_amount + - object + - type + title: CustomerCashBalanceTransaction + type: object + x-expandableFields: + - adjusted_for_overdraft + - applied_to_payment + - customer + - funded + - refunded_from_payment + - transferred_to_balance + - unapplied_from_payment + x-resourceId: customer_cash_balance_transaction + customer_session: + description: >- + A Customer Session allows you to grant Stripe's frontend SDKs (like + Stripe.js) client-side access + + control over a Customer. + + + Related guides: [Customer Session with the Payment + Element](/payments/accept-a-payment-deferred?platform=web&type=payment#save-payment-methods), + + [Customer Session with the Pricing + Table](/payments/checkout/pricing-table#customer-session), + + [Customer Session with the Buy + Button](/payment-links/buy-button#pass-an-existing-customer). + properties: + client_secret: + description: >- + The client secret of this Customer Session. Used on the client to + set up secure access to the given `customer`. + + + The client secret can be used to provide access to `customer` from + your frontend. It should not be stored, logged, or exposed to anyone + other than the relevant customer. Make sure that you have TLS + enabled on any page that includes the client secret. + maxLength: 5000 + type: string + components: + $ref: '#/components/schemas/customer_session_resource_components' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: The Customer the Customer Session was created for. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + expires_at: + description: The timestamp at which this Customer Session will expire. + format: unix-time + type: integer + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - customer_session + type: string + required: + - client_secret + - created + - customer + - expires_at + - livemode + - object + title: CustomerSessionResourceCustomerSession + type: object + x-expandableFields: + - components + - customer + x-resourceId: customer_session + customer_session_resource_components: + description: Configuration for the components supported by this Customer Session. + properties: + buy_button: + $ref: >- + #/components/schemas/customer_session_resource_components_resource_buy_button + payment_element: + $ref: >- + #/components/schemas/customer_session_resource_components_resource_payment_element + pricing_table: + $ref: >- + #/components/schemas/customer_session_resource_components_resource_pricing_table + required: + - buy_button + - payment_element + - pricing_table + title: CustomerSessionResourceComponents + type: object + x-expandableFields: + - buy_button + - payment_element + - pricing_table + customer_session_resource_components_resource_buy_button: + description: This hash contains whether the buy button is enabled. + properties: + enabled: + description: Whether the buy button is enabled. + type: boolean + required: + - enabled + title: CustomerSessionResourceComponentsResourceBuyButton + type: object + x-expandableFields: [] + customer_session_resource_components_resource_payment_element: + description: >- + This hash contains whether the Payment Element is enabled and the + features it supports. + properties: + enabled: + description: Whether the Payment Element is enabled. + type: boolean + features: + anyOf: + - $ref: >- + #/components/schemas/customer_session_resource_components_resource_payment_element_resource_features + description: >- + This hash defines whether the Payment Element supports certain + features. + nullable: true + required: + - enabled + title: CustomerSessionResourceComponentsResourcePaymentElement + type: object + x-expandableFields: + - features + customer_session_resource_components_resource_payment_element_resource_features: + description: This hash contains the features the Payment Element supports. + properties: + payment_method_allow_redisplay_filters: + description: >- + A list of + [`allow_redisplay`](https://docs.stripe.com/api/payment_methods/object#payment_method_object-allow_redisplay) + values that controls which saved payment methods the Payment Element + displays by filtering to only show payment methods with an + `allow_redisplay` value that is present in this list. + + + If not specified, defaults to ["always"]. In order to display all + saved payment methods, specify ["always", "limited", "unspecified"]. + items: + enum: + - always + - limited + - unspecified + type: string + type: array + payment_method_redisplay: + description: >- + Controls whether or not the Payment Element shows saved payment + methods. This parameter defaults to `disabled`. + enum: + - disabled + - enabled + type: string + x-stripeBypassValidation: true + payment_method_redisplay_limit: + description: >- + Determines the max number of saved payment methods for the Payment + Element to display. This parameter defaults to `3`. + nullable: true + type: integer + payment_method_remove: + description: >- + Controls whether the Payment Element displays the option to remove a + saved payment method. This parameter defaults to `disabled`. + + + Allowing buyers to remove their saved payment methods impacts + subscriptions that depend on that payment method. Removing the + payment method detaches the [`customer` + object](https://docs.stripe.com/api/payment_methods/object#payment_method_object-customer) + from that + [PaymentMethod](https://docs.stripe.com/api/payment_methods). + enum: + - disabled + - enabled + type: string + x-stripeBypassValidation: true + payment_method_save: + description: >- + Controls whether the Payment Element displays a checkbox offering to + save a new payment method. This parameter defaults to `disabled`. + + + If a customer checks the box, the + [`allow_redisplay`](https://docs.stripe.com/api/payment_methods/object#payment_method_object-allow_redisplay) + value on the PaymentMethod is set to `'always'` at confirmation + time. For PaymentIntents, the + [`setup_future_usage`](https://docs.stripe.com/api/payment_intents/object#payment_intent_object-setup_future_usage) + value is also set to the value defined in + `payment_method_save_usage`. + enum: + - disabled + - enabled + type: string + x-stripeBypassValidation: true + payment_method_save_usage: + description: >- + When using PaymentIntents and the customer checks the save checkbox, + this field determines the + [`setup_future_usage`](https://docs.stripe.com/api/payment_intents/object#payment_intent_object-setup_future_usage) + value used to confirm the PaymentIntent. + + + When using SetupIntents, directly configure the + [`usage`](https://docs.stripe.com/api/setup_intents/object#setup_intent_object-usage) + value on SetupIntent creation. + enum: + - off_session + - on_session + nullable: true + type: string + required: + - payment_method_allow_redisplay_filters + - payment_method_redisplay + - payment_method_remove + - payment_method_save + title: CustomerSessionResourceComponentsResourcePaymentElementResourceFeatures + type: object + x-expandableFields: [] + customer_session_resource_components_resource_pricing_table: + description: This hash contains whether the pricing table is enabled. + properties: + enabled: + description: Whether the pricing table is enabled. + type: boolean + required: + - enabled + title: CustomerSessionResourceComponentsResourcePricingTable + type: object + x-expandableFields: [] + customer_tax: + description: '' + properties: + automatic_tax: + description: >- + Surfaces if automatic tax computation is possible given the current + customer location information. + enum: + - failed + - not_collecting + - supported + - unrecognized_location + type: string + ip_address: + description: >- + A recent IP address of the customer used for tax reporting and tax + location inference. + maxLength: 5000 + nullable: true + type: string + location: + anyOf: + - $ref: '#/components/schemas/customer_tax_location' + description: The identified tax location of the customer. + nullable: true + required: + - automatic_tax + title: CustomerTax + type: object + x-expandableFields: + - location + customer_tax_location: + description: '' + properties: + country: + description: The identified tax country of the customer. + maxLength: 5000 + type: string + source: + description: The data source used to infer the customer's location. + enum: + - billing_address + - ip_address + - payment_method + - shipping_destination + type: string + state: + description: >- + The identified tax state, county, province, or region of the + customer. + maxLength: 5000 + nullable: true + type: string + required: + - country + - source + title: CustomerTaxLocation + type: object + x-expandableFields: [] + deleted_account: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - account + type: string + required: + - deleted + - id + - object + title: DeletedAccount + type: object + x-expandableFields: [] + x-resourceId: deleted_account + deleted_apple_pay_domain: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - apple_pay_domain + type: string + required: + - deleted + - id + - object + title: DeletedApplePayDomain + type: object + x-expandableFields: [] + x-resourceId: deleted_apple_pay_domain + deleted_application: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + name: + description: The name of the application. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - application + type: string + required: + - deleted + - id + - object + title: DeletedApplication + type: object + x-expandableFields: [] + deleted_bank_account: + description: '' + properties: + currency: + description: >- + Three-letter [ISO code for the + currency](https://stripe.com/docs/payouts) paid out to the bank + account. + maxLength: 5000 + nullable: true + type: string + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - bank_account + type: string + required: + - deleted + - id + - object + title: DeletedBankAccount + type: object + x-expandableFields: [] + deleted_card: + description: '' + properties: + currency: + description: >- + Three-letter [ISO code for the + currency](https://stripe.com/docs/payouts) paid out to the bank + account. + maxLength: 5000 + nullable: true + type: string + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - card + type: string + required: + - deleted + - id + - object + title: DeletedCard + type: object + x-expandableFields: [] + deleted_coupon: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - coupon + type: string + required: + - deleted + - id + - object + title: DeletedCoupon + type: object + x-expandableFields: [] + x-resourceId: deleted_coupon + deleted_customer: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - customer + type: string + required: + - deleted + - id + - object + title: DeletedCustomer + type: object + x-expandableFields: [] + x-resourceId: deleted_customer + deleted_discount: + description: '' + properties: + checkout_session: + description: >- + The Checkout session that this coupon is applied to, if it is + applied to a particular session in payment mode. Will not be present + for subscription mode. + maxLength: 5000 + nullable: true + type: string + coupon: + $ref: '#/components/schemas/coupon' + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: The ID of the customer associated with this discount. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: >- + The ID of the discount object. Discounts cannot be fetched by ID. + Use `expand[]=discounts` in API calls to expand discount IDs in an + array. + maxLength: 5000 + type: string + invoice: + description: >- + The invoice that the discount's coupon was applied to, if it was + applied directly to a particular invoice. + maxLength: 5000 + nullable: true + type: string + invoice_item: + description: >- + The invoice item `id` (or invoice line item `id` for invoice line + items of type='subscription') that the discount's coupon was applied + to, if it was applied directly to a particular invoice item or + invoice line item. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - discount + type: string + promotion_code: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/promotion_code' + description: The promotion code applied to create this discount. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/promotion_code' + start: + description: Date that the coupon was applied. + format: unix-time + type: integer + subscription: + description: >- + The subscription that this coupon is applied to, if it is applied to + a particular subscription. + maxLength: 5000 + nullable: true + type: string + subscription_item: + description: >- + The subscription item that this coupon is applied to, if it is + applied to a particular subscription item. + maxLength: 5000 + nullable: true + type: string + required: + - coupon + - deleted + - id + - object + - start + title: DeletedDiscount + type: object + x-expandableFields: + - coupon + - customer + - promotion_code + x-resourceId: deleted_discount + deleted_external_account: + anyOf: + - $ref: '#/components/schemas/deleted_bank_account' + - $ref: '#/components/schemas/deleted_card' + title: Polymorphic + x-resourceId: deleted_external_account + x-stripeBypassValidation: true + deleted_invoice: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - invoice + type: string + required: + - deleted + - id + - object + title: DeletedInvoice + type: object + x-expandableFields: [] + x-resourceId: deleted_invoice + deleted_invoiceitem: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - invoiceitem + type: string + required: + - deleted + - id + - object + title: DeletedInvoiceItem + type: object + x-expandableFields: [] + x-resourceId: deleted_invoiceitem + deleted_payment_source: + anyOf: + - $ref: '#/components/schemas/deleted_bank_account' + - $ref: '#/components/schemas/deleted_card' + title: Polymorphic + x-resourceId: deleted_payment_source + x-stripeBypassValidation: true + deleted_person: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - person + type: string + required: + - deleted + - id + - object + title: DeletedPerson + type: object + x-expandableFields: [] + x-resourceId: deleted_person + deleted_plan: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - plan + type: string + required: + - deleted + - id + - object + title: DeletedPlan + type: object + x-expandableFields: [] + x-resourceId: deleted_plan + deleted_price: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - price + type: string + required: + - deleted + - id + - object + title: DeletedPrice + type: object + x-expandableFields: [] + deleted_product: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - product + type: string + required: + - deleted + - id + - object + title: DeletedProduct + type: object + x-expandableFields: [] + x-resourceId: deleted_product + deleted_product_feature: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - product_feature + type: string + required: + - deleted + - id + - object + title: DeletedProductFeature + type: object + x-expandableFields: [] + x-resourceId: deleted_product_feature + deleted_radar.value_list: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - radar.value_list + type: string + required: + - deleted + - id + - object + title: RadarListDeletedList + type: object + x-expandableFields: [] + x-resourceId: deleted_radar.value_list + deleted_radar.value_list_item: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - radar.value_list_item + type: string + required: + - deleted + - id + - object + title: RadarListDeletedListItem + type: object + x-expandableFields: [] + x-resourceId: deleted_radar.value_list_item + deleted_subscription_item: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - subscription_item + type: string + required: + - deleted + - id + - object + title: DeletedSubscriptionItem + type: object + x-expandableFields: [] + x-resourceId: deleted_subscription_item + deleted_tax_id: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax_id + type: string + required: + - deleted + - id + - object + title: deleted_tax_id + type: object + x-expandableFields: [] + x-resourceId: deleted_tax_id + deleted_terminal.configuration: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - terminal.configuration + type: string + required: + - deleted + - id + - object + title: TerminalConfigurationDeletedConfiguration + type: object + x-expandableFields: [] + x-resourceId: deleted_terminal.configuration + deleted_terminal.location: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - terminal.location + type: string + required: + - deleted + - id + - object + title: TerminalLocationDeletedLocation + type: object + x-expandableFields: [] + x-resourceId: deleted_terminal.location + deleted_terminal.reader: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - terminal.reader + type: string + required: + - deleted + - id + - object + title: TerminalReaderDeletedReader + type: object + x-expandableFields: [] + x-resourceId: deleted_terminal.reader + deleted_test_helpers.test_clock: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - test_helpers.test_clock + type: string + required: + - deleted + - id + - object + title: DeletedTestClock + type: object + x-expandableFields: [] + x-resourceId: deleted_test_helpers.test_clock + deleted_webhook_endpoint: + description: '' + properties: + deleted: + description: Always true for a deleted object + enum: + - true + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - webhook_endpoint + type: string + required: + - deleted + - id + - object + title: NotificationWebhookEndpointDeleted + type: object + x-expandableFields: [] + x-resourceId: deleted_webhook_endpoint + destination_details_unimplemented: + description: '' + properties: {} + title: destination_details_unimplemented + type: object + x-expandableFields: [] + discount: + description: >- + A discount represents the actual application of a + [coupon](https://stripe.com/docs/api#coupons) or [promotion + code](https://stripe.com/docs/api#promotion_codes). + + It contains information about when the discount began, when it will end, + and what it is applied to. + + + Related guide: [Applying discounts to + subscriptions](https://stripe.com/docs/billing/subscriptions/discounts) + properties: + checkout_session: + description: >- + The Checkout session that this coupon is applied to, if it is + applied to a particular session in payment mode. Will not be present + for subscription mode. + maxLength: 5000 + nullable: true + type: string + coupon: + $ref: '#/components/schemas/coupon' + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: The ID of the customer associated with this discount. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + end: + description: >- + If the coupon has a duration of `repeating`, the date that this + discount will end. If the coupon has a duration of `once` or + `forever`, this attribute will be null. + format: unix-time + nullable: true + type: integer + id: + description: >- + The ID of the discount object. Discounts cannot be fetched by ID. + Use `expand[]=discounts` in API calls to expand discount IDs in an + array. + maxLength: 5000 + type: string + invoice: + description: >- + The invoice that the discount's coupon was applied to, if it was + applied directly to a particular invoice. + maxLength: 5000 + nullable: true + type: string + invoice_item: + description: >- + The invoice item `id` (or invoice line item `id` for invoice line + items of type='subscription') that the discount's coupon was applied + to, if it was applied directly to a particular invoice item or + invoice line item. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - discount + type: string + promotion_code: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/promotion_code' + description: The promotion code applied to create this discount. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/promotion_code' + start: + description: Date that the coupon was applied. + format: unix-time + type: integer + subscription: + description: >- + The subscription that this coupon is applied to, if it is applied to + a particular subscription. + maxLength: 5000 + nullable: true + type: string + subscription_item: + description: >- + The subscription item that this coupon is applied to, if it is + applied to a particular subscription item. + maxLength: 5000 + nullable: true + type: string + required: + - coupon + - id + - object + - start + title: Discount + type: object + x-expandableFields: + - coupon + - customer + - promotion_code + x-resourceId: discount + discounts_resource_discount_amount: + description: '' + properties: + amount: + description: 'The amount, in cents (or local equivalent), of the discount.' + type: integer + discount: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + - $ref: '#/components/schemas/deleted_discount' + description: The discount that was applied to get this discount amount. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + - $ref: '#/components/schemas/deleted_discount' + required: + - amount + - discount + title: DiscountsResourceDiscountAmount + type: object + x-expandableFields: + - discount + discounts_resource_stackable_discount: + description: '' + properties: + coupon: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/coupon' + description: ID of the coupon to create a new discount for. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/coupon' + discount: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + description: >- + ID of an existing discount on the object (or one of its ancestors) + to reuse. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + promotion_code: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/promotion_code' + description: ID of the promotion code to create a new discount for. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/promotion_code' + title: DiscountsResourceStackableDiscount + type: object + x-expandableFields: + - coupon + - discount + - promotion_code + dispute: + description: >- + A dispute occurs when a customer questions your charge with their card + issuer. + + When this happens, you have the opportunity to respond to the dispute + with + + evidence that shows that the charge is legitimate. + + + Related guide: [Disputes and fraud](https://stripe.com/docs/disputes) + properties: + amount: + description: >- + Disputed amount. Usually the amount of the charge, but it can differ + (usually because of currency fluctuation or because only part of the + order is disputed). + type: integer + balance_transactions: + description: >- + List of zero, one, or two balance transactions that show funds + withdrawn and reinstated to your Stripe account as a result of this + dispute. + items: + $ref: '#/components/schemas/balance_transaction' + type: array + charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: ID of the charge that's disputed. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + enhanced_eligibility_types: + description: List of eligibility types that are included in `enhanced_evidence`. + items: + enum: + - visa_compelling_evidence_3 + type: string + x-stripeBypassValidation: true + type: array + evidence: + $ref: '#/components/schemas/dispute_evidence' + evidence_details: + $ref: '#/components/schemas/dispute_evidence_details' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + is_charge_refundable: + description: >- + If true, it's still possible to refund the disputed payment. After + the payment has been fully refunded, no further funds are withdrawn + from your Stripe account as a result of this dispute. + type: boolean + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - dispute + type: string + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: ID of the PaymentIntent that's disputed. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + payment_method_details: + $ref: '#/components/schemas/dispute_payment_method_details' + reason: + description: >- + Reason given by cardholder for dispute. Possible values are + `bank_cannot_process`, `check_returned`, `credit_not_processed`, + `customer_initiated`, `debit_not_authorized`, `duplicate`, + `fraudulent`, `general`, `incorrect_account_details`, + `insufficient_funds`, `noncompliant`, `product_not_received`, + `product_unacceptable`, `subscription_canceled`, or `unrecognized`. + Learn more about [dispute + reasons](https://stripe.com/docs/disputes/categories). + maxLength: 5000 + type: string + status: + description: >- + Current status of dispute. Possible values are + `warning_needs_response`, `warning_under_review`, `warning_closed`, + `needs_response`, `under_review`, `won`, or `lost`. + enum: + - lost + - needs_response + - under_review + - warning_closed + - warning_needs_response + - warning_under_review + - won + type: string + required: + - amount + - balance_transactions + - charge + - created + - currency + - enhanced_eligibility_types + - evidence + - evidence_details + - id + - is_charge_refundable + - livemode + - metadata + - object + - reason + - status + title: Dispute + type: object + x-expandableFields: + - balance_transactions + - charge + - evidence + - evidence_details + - payment_intent + - payment_method_details + x-resourceId: dispute + dispute_enhanced_eligibility: + description: '' + properties: + visa_compelling_evidence_3: + $ref: >- + #/components/schemas/dispute_enhanced_eligibility_visa_compelling_evidence3 + visa_compliance: + $ref: '#/components/schemas/dispute_enhanced_eligibility_visa_compliance' + title: DisputeEnhancedEligibility + type: object + x-expandableFields: + - visa_compelling_evidence_3 + - visa_compliance + dispute_enhanced_eligibility_visa_compelling_evidence3: + description: '' + properties: + required_actions: + description: >- + List of actions required to qualify dispute for Visa Compelling + Evidence 3.0 evidence submission. + items: + enum: + - missing_customer_identifiers + - missing_disputed_transaction_description + - missing_merchandise_or_services + - missing_prior_undisputed_transaction_description + - missing_prior_undisputed_transactions + type: string + type: array + status: + description: Visa Compelling Evidence 3.0 eligibility status. + enum: + - not_qualified + - qualified + - requires_action + type: string + required: + - required_actions + - status + title: DisputeEnhancedEligibilityVisaCompellingEvidence3 + type: object + x-expandableFields: [] + dispute_enhanced_eligibility_visa_compliance: + description: '' + properties: + status: + description: Visa compliance eligibility status. + enum: + - fee_acknowledged + - requires_fee_acknowledgement + type: string + required: + - status + title: DisputeEnhancedEligibilityVisaCompliance + type: object + x-expandableFields: [] + dispute_enhanced_evidence: + description: '' + properties: + visa_compelling_evidence_3: + $ref: >- + #/components/schemas/dispute_enhanced_evidence_visa_compelling_evidence3 + visa_compliance: + $ref: '#/components/schemas/dispute_enhanced_evidence_visa_compliance' + title: DisputeEnhancedEvidence + type: object + x-expandableFields: + - visa_compelling_evidence_3 + - visa_compliance + dispute_enhanced_evidence_visa_compelling_evidence3: + description: '' + properties: + disputed_transaction: + anyOf: + - $ref: >- + #/components/schemas/dispute_visa_compelling_evidence3_disputed_transaction + description: >- + Disputed transaction details for Visa Compelling Evidence 3.0 + evidence submission. + nullable: true + prior_undisputed_transactions: + description: >- + List of exactly two prior undisputed transaction objects for Visa + Compelling Evidence 3.0 evidence submission. + items: + $ref: >- + #/components/schemas/dispute_visa_compelling_evidence3_prior_undisputed_transaction + type: array + required: + - prior_undisputed_transactions + title: DisputeEnhancedEvidenceVisaCompellingEvidence3 + type: object + x-expandableFields: + - disputed_transaction + - prior_undisputed_transactions + dispute_enhanced_evidence_visa_compliance: + description: '' + properties: + fee_acknowledged: + description: >- + A field acknowledging the fee incurred when countering a Visa + compliance dispute. If this field is set to true, evidence can be + submitted for the compliance dispute. Stripe collects a 500 USD (or + local equivalent) amount to cover the network costs associated with + resolving compliance disputes. Stripe refunds the 500 USD network + fee if you win the dispute. + type: boolean + required: + - fee_acknowledged + title: DisputeEnhancedEvidenceVisaCompliance + type: object + x-expandableFields: [] + dispute_evidence: + description: '' + properties: + access_activity_log: + description: >- + Any server or activity logs showing proof that the customer accessed + or downloaded the purchased digital product. This information should + include IP addresses, corresponding timestamps, and any detailed + recorded activity. + maxLength: 150000 + nullable: true + type: string + billing_address: + description: The billing address provided by the customer. + maxLength: 5000 + nullable: true + type: string + cancellation_policy: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Your subscription cancellation policy, as shown to the customer. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + cancellation_policy_disclosure: + description: >- + An explanation of how and when the customer was shown your refund + policy prior to purchase. + maxLength: 150000 + nullable: true + type: string + cancellation_rebuttal: + description: >- + A justification for why the customer's subscription was not + canceled. + maxLength: 150000 + nullable: true + type: string + customer_communication: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Any communication with the customer that you feel is relevant to + your case. Examples include emails proving that the customer + received the product or service, or demonstrating their use of or + satisfaction with the product or service. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + customer_email_address: + description: The email address of the customer. + maxLength: 5000 + nullable: true + type: string + customer_name: + description: The name of the customer. + maxLength: 5000 + nullable: true + type: string + customer_purchase_ip: + description: The IP address that the customer used when making the purchase. + maxLength: 5000 + nullable: true + type: string + customer_signature: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + A relevant document or contract showing the customer's signature. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + duplicate_charge_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Documentation for the prior charge that can uniquely identify the + charge, such as a receipt, shipping label, work order, etc. This + document should be paired with a similar document from the disputed + payment that proves the two payments are separate. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + duplicate_charge_explanation: + description: >- + An explanation of the difference between the disputed charge versus + the prior charge that appears to be a duplicate. + maxLength: 150000 + nullable: true + type: string + duplicate_charge_id: + description: >- + The Stripe ID for the prior charge which appears to be a duplicate + of the disputed charge. + maxLength: 5000 + nullable: true + type: string + enhanced_evidence: + $ref: '#/components/schemas/dispute_enhanced_evidence' + product_description: + description: A description of the product or service that was sold. + maxLength: 150000 + nullable: true + type: string + receipt: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Any receipt or message sent to the customer notifying them of the + charge. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + refund_policy: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Your refund policy, as shown to the customer. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + refund_policy_disclosure: + description: >- + Documentation demonstrating that the customer was shown your refund + policy prior to purchase. + maxLength: 150000 + nullable: true + type: string + refund_refusal_explanation: + description: A justification for why the customer is not entitled to a refund. + maxLength: 150000 + nullable: true + type: string + service_date: + description: >- + The date on which the customer received or began receiving the + purchased service, in a clear human-readable format. + maxLength: 5000 + nullable: true + type: string + service_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Documentation showing proof that a service was provided to the + customer. This could include a copy of a signed contract, work + order, or other form of written agreement. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + shipping_address: + description: >- + The address to which a physical product was shipped. You should try + to include as complete address information as possible. + maxLength: 5000 + nullable: true + type: string + shipping_carrier: + description: >- + The delivery service that shipped a physical product, such as Fedex, + UPS, USPS, etc. If multiple carriers were used for this purchase, + please separate them with commas. + maxLength: 5000 + nullable: true + type: string + shipping_date: + description: >- + The date on which a physical product began its route to the shipping + address, in a clear human-readable format. + maxLength: 5000 + nullable: true + type: string + shipping_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Documentation showing proof that a product was shipped to the + customer at the same address the customer provided to you. This + could include a copy of the shipment receipt, shipping label, etc. + It should show the customer's full shipping address, if possible. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + shipping_tracking_number: + description: >- + The tracking number for a physical product, obtained from the + delivery service. If multiple tracking numbers were generated for + this purchase, please separate them with commas. + maxLength: 5000 + nullable: true + type: string + uncategorized_file: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Any additional evidence or statements. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + uncategorized_text: + description: Any additional evidence or statements. + maxLength: 150000 + nullable: true + type: string + required: + - enhanced_evidence + title: DisputeEvidence + type: object + x-expandableFields: + - cancellation_policy + - customer_communication + - customer_signature + - duplicate_charge_documentation + - enhanced_evidence + - receipt + - refund_policy + - service_documentation + - shipping_documentation + - uncategorized_file + dispute_evidence_details: + description: '' + properties: + due_by: + description: >- + Date by which evidence must be submitted in order to successfully + challenge dispute. Will be 0 if the customer's bank or credit card + company doesn't allow a response for this particular dispute. + format: unix-time + nullable: true + type: integer + enhanced_eligibility: + $ref: '#/components/schemas/dispute_enhanced_eligibility' + has_evidence: + description: Whether evidence has been staged for this dispute. + type: boolean + past_due: + description: >- + Whether the last evidence submission was submitted past the due + date. Defaults to `false` if no evidence submissions have occurred. + If `true`, then delivery of the latest evidence is *not* guaranteed. + type: boolean + submission_count: + description: >- + The number of times evidence has been submitted. Typically, you may + only submit evidence once. + type: integer + required: + - enhanced_eligibility + - has_evidence + - past_due + - submission_count + title: DisputeEvidenceDetails + type: object + x-expandableFields: + - enhanced_eligibility + dispute_payment_method_details: + description: '' + properties: + amazon_pay: + $ref: '#/components/schemas/dispute_payment_method_details_amazon_pay' + card: + $ref: '#/components/schemas/dispute_payment_method_details_card' + klarna: + $ref: '#/components/schemas/dispute_payment_method_details_klarna' + paypal: + $ref: '#/components/schemas/dispute_payment_method_details_paypal' + type: + description: Payment method type. + enum: + - amazon_pay + - card + - klarna + - paypal + type: string + required: + - type + title: DisputePaymentMethodDetails + type: object + x-expandableFields: + - amazon_pay + - card + - klarna + - paypal + dispute_payment_method_details_amazon_pay: + description: '' + properties: + dispute_type: + description: 'The AmazonPay dispute type, chargeback or claim' + enum: + - chargeback + - claim + nullable: true + type: string + title: DisputePaymentMethodDetailsAmazonPay + type: object + x-expandableFields: [] + dispute_payment_method_details_card: + description: '' + properties: + brand: + description: >- + Card brand. Can be `amex`, `diners`, `discover`, `eftpos_au`, `jcb`, + `link`, `mastercard`, `unionpay`, `visa`, or `unknown`. + maxLength: 5000 + type: string + case_type: + description: >- + The type of dispute opened. Different case types may have varying + fees and financial impact. + enum: + - chargeback + - inquiry + type: string + x-stripeBypassValidation: true + network_reason_code: + description: >- + The card network's specific dispute reason code, which maps to one + of Stripe's primary dispute categories to simplify response + guidance. The [Network code + map](https://stripe.com/docs/disputes/categories#network-code-map) + lists all available dispute reason codes by network. + maxLength: 5000 + nullable: true + type: string + required: + - brand + - case_type + title: DisputePaymentMethodDetailsCard + type: object + x-expandableFields: [] + dispute_payment_method_details_klarna: + description: '' + properties: + reason_code: + description: The reason for the dispute as defined by Klarna + maxLength: 5000 + nullable: true + type: string + title: DisputePaymentMethodDetailsKlarna + type: object + x-expandableFields: [] + dispute_payment_method_details_paypal: + description: '' + properties: + case_id: + description: The ID of the dispute in PayPal. + maxLength: 5000 + nullable: true + type: string + reason_code: + description: The reason for the dispute as defined by PayPal + maxLength: 5000 + nullable: true + type: string + title: DisputePaymentMethodDetailsPaypal + type: object + x-expandableFields: [] + dispute_transaction_shipping_address: + description: '' + properties: + city: + description: 'City, district, suburb, town, or village.' + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + nullable: true + type: string + line1: + description: 'Address line 1 (e.g., street, PO Box, or company name).' + maxLength: 5000 + nullable: true + type: string + line2: + description: 'Address line 2 (e.g., apartment, suite, unit, or building).' + maxLength: 5000 + nullable: true + type: string + postal_code: + description: ZIP or postal code. + maxLength: 5000 + nullable: true + type: string + state: + description: 'State, county, province, or region.' + maxLength: 5000 + nullable: true + type: string + title: DisputeTransactionShippingAddress + type: object + x-expandableFields: [] + dispute_visa_compelling_evidence3_disputed_transaction: + description: '' + properties: + customer_account_id: + description: >- + User Account ID used to log into business platform. Must be + recognizable by the user. + maxLength: 5000 + nullable: true + type: string + customer_device_fingerprint: + description: >- + Unique identifier of the cardholder’s device derived from a + combination of at least two hardware and software attributes. Must + be at least 20 characters. + maxLength: 5000 + nullable: true + type: string + customer_device_id: + description: >- + Unique identifier of the cardholder’s device such as a device serial + number (e.g., International Mobile Equipment Identity [IMEI]). Must + be at least 15 characters. + maxLength: 5000 + nullable: true + type: string + customer_email_address: + description: The email address of the customer. + maxLength: 5000 + nullable: true + type: string + customer_purchase_ip: + description: The IP address that the customer used when making the purchase. + maxLength: 5000 + nullable: true + type: string + merchandise_or_services: + description: Categorization of disputed payment. + enum: + - merchandise + - services + nullable: true + type: string + product_description: + description: A description of the product or service that was sold. + maxLength: 150000 + nullable: true + type: string + shipping_address: + anyOf: + - $ref: '#/components/schemas/dispute_transaction_shipping_address' + description: >- + The address to which a physical product was shipped. All fields are + required for Visa Compelling Evidence 3.0 evidence submission. + nullable: true + title: DisputeVisaCompellingEvidence3DisputedTransaction + type: object + x-expandableFields: + - shipping_address + dispute_visa_compelling_evidence3_prior_undisputed_transaction: + description: '' + properties: + charge: + description: >- + Stripe charge ID for the Visa Compelling Evidence 3.0 eligible prior + charge. + maxLength: 5000 + type: string + customer_account_id: + description: >- + User Account ID used to log into business platform. Must be + recognizable by the user. + maxLength: 5000 + nullable: true + type: string + customer_device_fingerprint: + description: >- + Unique identifier of the cardholder’s device derived from a + combination of at least two hardware and software attributes. Must + be at least 20 characters. + maxLength: 5000 + nullable: true + type: string + customer_device_id: + description: >- + Unique identifier of the cardholder’s device such as a device serial + number (e.g., International Mobile Equipment Identity [IMEI]). Must + be at least 15 characters. + maxLength: 5000 + nullable: true + type: string + customer_email_address: + description: The email address of the customer. + maxLength: 5000 + nullable: true + type: string + customer_purchase_ip: + description: The IP address that the customer used when making the purchase. + maxLength: 5000 + nullable: true + type: string + product_description: + description: A description of the product or service that was sold. + maxLength: 150000 + nullable: true + type: string + shipping_address: + anyOf: + - $ref: '#/components/schemas/dispute_transaction_shipping_address' + description: >- + The address to which a physical product was shipped. All fields are + required for Visa Compelling Evidence 3.0 evidence submission. + nullable: true + required: + - charge + title: DisputeVisaCompellingEvidence3PriorUndisputedTransaction + type: object + x-expandableFields: + - shipping_address + email_sent: + description: '' + properties: + email_sent_at: + description: The timestamp when the email was sent. + format: unix-time + type: integer + email_sent_to: + description: The recipient's email address. + maxLength: 5000 + type: string + required: + - email_sent_at + - email_sent_to + title: EmailSent + type: object + x-expandableFields: [] + entitlements.active_entitlement: + description: An active entitlement describes access to a feature for a customer. + properties: + feature: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/entitlements.feature' + description: >- + The [Feature](https://stripe.com/docs/api/entitlements/feature) that + the customer is entitled to. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/entitlements.feature' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + lookup_key: + description: >- + A unique key you provide as your own system identifier. This may be + up to 80 characters. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - entitlements.active_entitlement + type: string + required: + - feature + - id + - livemode + - lookup_key + - object + title: ActiveEntitlement + type: object + x-expandableFields: + - feature + x-resourceId: entitlements.active_entitlement + entitlements.feature: + description: >- + A feature represents a monetizable ability or functionality in your + system. + + Features can be assigned to products, and when those products are + purchased, Stripe will create an entitlement to the feature for the + purchasing customer. + properties: + active: + description: >- + Inactive features cannot be attached to new products and will not be + returned from the features list endpoint. + type: boolean + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + lookup_key: + description: >- + A unique key you provide as your own system identifier. This may be + up to 80 characters. + maxLength: 5000 + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of key-value pairs that you can attach to an object. This can be + useful for storing additional information about the object in a + structured format. + type: object + name: + description: >- + The feature's name, for your own purpose, not meant to be + displayable to the customer. + maxLength: 80 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - entitlements.feature + type: string + required: + - active + - id + - livemode + - lookup_key + - metadata + - name + - object + title: Feature + type: object + x-expandableFields: [] + x-resourceId: entitlements.feature + ephemeral_key: + description: '' + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + expires: + description: >- + Time at which the key will expire. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - ephemeral_key + type: string + secret: + description: >- + The key's secret. You can use this value to make authorized requests + to the Stripe API. + maxLength: 5000 + type: string + required: + - created + - expires + - id + - livemode + - object + title: EphemeralKey + type: object + x-expandableFields: [] + x-resourceId: ephemeral_key + error: + description: An error response from the Stripe API + properties: + error: + $ref: '#/components/schemas/api_errors' + required: + - error + type: object + event: + description: >- + Events are our way of letting you know when something interesting + happens in + + your account. When an interesting event occurs, we create a new `Event` + + object. For example, when a charge succeeds, we create a + `charge.succeeded` + + event, and when an invoice payment attempt fails, we create an + + `invoice.payment_failed` event. Certain API requests might create + multiple + + events. For example, if you create a new subscription for a + + customer, you receive both a `customer.subscription.created` event and a + + `charge.succeeded` event. + + + Events occur when the state of another API resource changes. The event's + data + + field embeds the resource's state at the time of the change. For + + example, a `charge.succeeded` event contains a charge, and an + + `invoice.payment_failed` event contains an invoice. + + + As with other API resources, you can use endpoints to retrieve an + + [individual event](https://stripe.com/docs/api#retrieve_event) or a + [list of events](https://stripe.com/docs/api#list_events) + + from the API. We also have a separate + + [webhooks](http://en.wikipedia.org/wiki/Webhook) system for sending the + + `Event` objects directly to an endpoint on your server. You can manage + + webhooks in your + + [account settings](https://dashboard.stripe.com/account/webhooks). Learn + how + + to [listen for events](https://docs.stripe.com/webhooks) + + so that your integration can automatically trigger reactions. + + + When using [Connect](https://docs.stripe.com/connect), you can also + receive event notifications + + that occur in connected accounts. For these events, there's an + + additional `account` attribute in the received `Event` object. + + + We only guarantee access to events through the [Retrieve Event + API](https://stripe.com/docs/api#retrieve_event) + + for 30 days. + properties: + account: + description: The connected account that originates the event. + maxLength: 5000 + type: string + api_version: + description: >- + The Stripe API version used to render `data`. This property is + populated only for events on or after October 31, 2014. + maxLength: 5000 + nullable: true + type: string + context: + description: Authentication context needed to fetch the event or related object. + maxLength: 5000 + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + data: + $ref: '#/components/schemas/notification_event_data' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - event + type: string + pending_webhooks: + description: >- + Number of webhooks that haven't been successfully delivered (for + example, to return a 20x response) to the URLs you specify. + type: integer + request: + anyOf: + - $ref: '#/components/schemas/notification_event_request' + description: Information on the API request that triggers the event. + nullable: true + type: + description: >- + Description of the event (for example, `invoice.created` or + `charge.refunded`). + maxLength: 5000 + type: string + required: + - created + - data + - id + - livemode + - object + - pending_webhooks + - type + title: NotificationEvent + type: object + x-expandableFields: + - data + - request + x-resourceId: event + exchange_rate: + description: >- + `ExchangeRate` objects allow you to determine the rates that Stripe is + currently + + using to convert from one currency to another. Since this number is + variable + + throughout the day, there are various reasons why you might want to know + the current + + rate (for example, to dynamically price an item for a user with a + default + + payment in a foreign currency). + + + Please refer to our [Exchange Rates + API](https://stripe.com/docs/fx-rates) guide for more details. + + + *[Note: this integration path is supported but no longer recommended]* + Additionally, + + you can guarantee that a charge is made with an exchange rate that you + expect is + + current. To do so, you must pass in the exchange_rate to charges + endpoints. If the + + value is no longer up to date, the charge won't go through. Please refer + to our + + [Using with charges](https://stripe.com/docs/exchange-rates) guide for + more details. + + + ----- + + +   + + + *This Exchange Rates API is a Beta Service and is subject to Stripe's + terms of service. You may use the API solely for the purpose of + transacting on Stripe. For example, the API may be queried in order to:* + + + - *localize prices for processing payments on Stripe* + + - *reconcile Stripe transactions* + + - *determine how much money to send to a connected account* + + - *determine app fees to charge a connected account* + + + *Using this Exchange Rates API beta for any purpose other than to + transact on Stripe is strictly prohibited and constitutes a violation of + Stripe's terms of service.* + properties: + id: + description: >- + Unique identifier for the object. Represented as the three-letter + [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html) in + lowercase. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - exchange_rate + type: string + rates: + additionalProperties: + type: number + description: >- + Hash where the keys are supported currencies and the values are the + exchange rate at which the base id currency converts to the key + currency. + type: object + required: + - id + - object + - rates + title: ExchangeRate + type: object + x-expandableFields: [] + x-resourceId: exchange_rate + external_account: + anyOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + title: Polymorphic + x-resourceId: external_account + x-stripeBypassValidation: true + external_account_requirements: + description: '' + properties: + currently_due: + description: >- + Fields that need to be collected to keep the external account + enabled. If not collected by `current_deadline`, these fields appear + in `past_due` as well, and the account is disabled. + items: + maxLength: 5000 + type: string + nullable: true + type: array + errors: + description: >- + Fields that are `currently_due` and need to be collected again + because validation or verification failed. + items: + $ref: '#/components/schemas/account_requirements_error' + nullable: true + type: array + past_due: + description: >- + Fields that weren't collected by `current_deadline`. These fields + need to be collected to enable the external account. + items: + maxLength: 5000 + type: string + nullable: true + type: array + pending_verification: + description: >- + Fields that might become required depending on the results of + verification or review. It's an empty array unless an asynchronous + verification is pending. If verification fails, these fields move to + `eventually_due`, `currently_due`, or `past_due`. Fields might + appear in `eventually_due`, `currently_due`, or `past_due` and in + `pending_verification` if verification fails but another + verification is still pending. + items: + maxLength: 5000 + type: string + nullable: true + type: array + title: ExternalAccountRequirements + type: object + x-expandableFields: + - errors + fee: + description: '' + properties: + amount: + description: 'Amount of the fee, in cents.' + type: integer + application: + description: ID of the Connect application that earned the fee. + maxLength: 5000 + nullable: true + type: string + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + type: + description: >- + Type of the fee, one of: `application_fee`, + `payment_method_passthrough_fee`, `stripe_fee` or `tax`. + maxLength: 5000 + type: string + required: + - amount + - currency + - type + title: Fee + type: object + x-expandableFields: [] + fee_refund: + description: >- + `Application Fee Refund` objects allow you to refund an application fee + that + + has previously been created but not yet refunded. Funds will be refunded + to + + the Stripe account from which the fee was originally collected. + + + Related guide: [Refunding application + fees](https://stripe.com/docs/connect/destination-charges#refunding-app-fee) + properties: + amount: + description: 'Amount, in cents (or local equivalent).' + type: integer + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + Balance transaction that describes the impact on your account + balance. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + fee: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application_fee' + description: ID of the application fee that was refunded. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application_fee' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - fee_refund + type: string + required: + - amount + - created + - currency + - fee + - id + - object + title: FeeRefund + type: object + x-expandableFields: + - balance_transaction + - fee + x-resourceId: fee_refund + file: + description: >- + This object represents files hosted on Stripe's servers. You can upload + + files with the [create file](https://stripe.com/docs/api#create_file) + request + + (for example, when uploading dispute evidence). Stripe also + + creates files independently (for example, the results of a [Sigma + scheduled + + query](#scheduled_queries)). + + + Related guide: [File upload guide](https://stripe.com/docs/file-upload) + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + expires_at: + description: The file expires and isn't available at this time in epoch seconds. + format: unix-time + nullable: true + type: integer + filename: + description: The suitable name for saving the file to a filesystem. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + links: + description: >- + A list of [file links](https://stripe.com/docs/api#file_links) that + point at this file. + nullable: true + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/file_link' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/file_links + type: string + required: + - data + - has_more + - object + - url + title: FileResourceFileLinkList + type: object + x-expandableFields: + - data + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - file + type: string + purpose: + description: >- + The [purpose](https://stripe.com/docs/file-upload#uploading-a-file) + of the uploaded file. + enum: + - account_requirement + - additional_verification + - business_icon + - business_logo + - customer_signature + - dispute_evidence + - document_provider_identity_document + - finance_report_run + - financial_account_statement + - identity_document + - identity_document_downloadable + - issuing_regulatory_reporting + - pci_document + - selfie + - sigma_scheduled_query + - tax_document_user_upload + - terminal_reader_splashscreen + type: string + x-stripeBypassValidation: true + size: + description: The size of the file object in bytes. + type: integer + title: + description: A suitable title for the document. + maxLength: 5000 + nullable: true + type: string + type: + description: 'The returned file type (for example, `csv`, `pdf`, `jpg`, or `png`).' + maxLength: 5000 + nullable: true + type: string + url: + description: Use your live secret API key to download the file from this URL. + maxLength: 5000 + nullable: true + type: string + required: + - created + - id + - object + - purpose + - size + title: File + type: object + x-expandableFields: + - links + x-resourceId: file + file_link: + description: |- + To share the contents of a `File` object with non-Stripe users, you can + create a `FileLink`. `FileLink`s contain a URL that you can use to + retrieve the contents of the file without authentication. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + expired: + description: Returns if the link is already expired. + type: boolean + expires_at: + description: Time that the link expires. + format: unix-time + nullable: true + type: integer + file: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: The file object this link points to. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - file_link + type: string + url: + description: The publicly accessible URL to download the file. + maxLength: 5000 + nullable: true + type: string + required: + - created + - expired + - file + - id + - livemode + - metadata + - object + title: FileLink + type: object + x-expandableFields: + - file + x-resourceId: file_link + financial_connections.account: + description: >- + A Financial Connections Account represents an account that exists + outside of Stripe, to which you have been granted some degree of access. + properties: + account_holder: + anyOf: + - $ref: '#/components/schemas/bank_connections_resource_accountholder' + description: The account holder that this account belongs to. + nullable: true + balance: + anyOf: + - $ref: '#/components/schemas/bank_connections_resource_balance' + description: The most recent information about the account's balance. + nullable: true + balance_refresh: + anyOf: + - $ref: '#/components/schemas/bank_connections_resource_balance_refresh' + description: The state of the most recent attempt to refresh the account balance. + nullable: true + category: + description: >- + The type of the account. Account category is further divided in + `subcategory`. + enum: + - cash + - credit + - investment + - other + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + display_name: + description: >- + A human-readable name that has been assigned to this account, either + by the account holder or by the institution. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + institution_name: + description: The name of the institution that holds this account. + maxLength: 5000 + type: string + last4: + description: >- + The last 4 digits of the account number. If present, this will be 4 + numeric characters. + maxLength: 5000 + nullable: true + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - financial_connections.account + type: string + ownership: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/financial_connections.account_ownership' + description: The most recent information about the account's owners. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/financial_connections.account_ownership' + ownership_refresh: + anyOf: + - $ref: '#/components/schemas/bank_connections_resource_ownership_refresh' + description: The state of the most recent attempt to refresh the account owners. + nullable: true + permissions: + description: The list of permissions granted by this account. + items: + enum: + - balances + - ownership + - payment_method + - transactions + type: string + nullable: true + type: array + status: + description: The status of the link to the account. + enum: + - active + - disconnected + - inactive + type: string + subcategory: + description: |- + If `category` is `cash`, one of: + + - `checking` + - `savings` + - `other` + + If `category` is `credit`, one of: + + - `mortgage` + - `line_of_credit` + - `credit_card` + - `other` + + If `category` is `investment` or `other`, this will be `other`. + enum: + - checking + - credit_card + - line_of_credit + - mortgage + - other + - savings + type: string + subscriptions: + description: The list of data refresh subscriptions requested on this account. + items: + enum: + - transactions + type: string + x-stripeBypassValidation: true + nullable: true + type: array + supported_payment_method_types: + description: >- + The [PaymentMethod + type](https://stripe.com/docs/api/payment_methods/object#payment_method_object-type)(s) + that can be created from this account. + items: + enum: + - link + - us_bank_account + type: string + x-stripeBypassValidation: true + type: array + transaction_refresh: + anyOf: + - $ref: >- + #/components/schemas/bank_connections_resource_transaction_refresh + description: >- + The state of the most recent attempt to refresh the account + transactions. + nullable: true + required: + - category + - created + - id + - institution_name + - livemode + - object + - status + - subcategory + - supported_payment_method_types + title: BankConnectionsResourceLinkedAccount + type: object + x-expandableFields: + - account_holder + - balance + - balance_refresh + - ownership + - ownership_refresh + - transaction_refresh + x-resourceId: financial_connections.account + financial_connections.account_owner: + description: Describes an owner of an account. + properties: + email: + description: The email address of the owner. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + name: + description: The full name of the owner. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - financial_connections.account_owner + type: string + ownership: + description: The ownership object that this owner belongs to. + maxLength: 5000 + type: string + phone: + description: The raw phone number of the owner. + maxLength: 5000 + nullable: true + type: string + raw_address: + description: The raw physical address of the owner. + maxLength: 5000 + nullable: true + type: string + refreshed_at: + description: The timestamp of the refresh that updated this owner. + format: unix-time + nullable: true + type: integer + required: + - id + - name + - object + - ownership + title: BankConnectionsResourceOwner + type: object + x-expandableFields: [] + x-resourceId: financial_connections.account_owner + financial_connections.account_ownership: + description: >- + Describes a snapshot of the owners of an account at a particular point + in time. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - financial_connections.account_ownership + type: string + owners: + description: A paginated list of owners for this account. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/financial_connections.account_owner' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: BankConnectionsResourceOwnerList + type: object + x-expandableFields: + - data + required: + - created + - id + - object + - owners + title: BankConnectionsResourceOwnership + type: object + x-expandableFields: + - owners + financial_connections.session: + description: >- + A Financial Connections Session is the secure way to programmatically + launch the client-side Stripe.js modal that lets your users link their + accounts. + properties: + account_holder: + anyOf: + - $ref: '#/components/schemas/bank_connections_resource_accountholder' + description: The account holder for whom accounts are collected in this session. + nullable: true + accounts: + description: The accounts that were collected as part of this Session. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/financial_connections.account' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/financial_connections/accounts + type: string + required: + - data + - has_more + - object + - url + title: BankConnectionsResourceLinkedAccountList + type: object + x-expandableFields: + - data + client_secret: + description: >- + A value that will be passed to the client to launch the + authentication flow. + maxLength: 5000 + type: string + filters: + $ref: >- + #/components/schemas/bank_connections_resource_link_account_session_filters + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - financial_connections.session + type: string + permissions: + description: Permissions requested for accounts collected during this session. + items: + enum: + - balances + - ownership + - payment_method + - transactions + type: string + x-stripeBypassValidation: true + type: array + prefetch: + description: Data features requested to be retrieved upon account creation. + items: + enum: + - balances + - ownership + - transactions + type: string + x-stripeBypassValidation: true + nullable: true + type: array + return_url: + description: >- + For webview integrations only. Upon completing OAuth login in the + native browser, the user will be redirected to this URL to return to + your app. + maxLength: 5000 + type: string + required: + - accounts + - client_secret + - id + - livemode + - object + - permissions + title: BankConnectionsResourceLinkAccountSession + type: object + x-expandableFields: + - account_holder + - accounts + - filters + x-resourceId: financial_connections.session + financial_connections.transaction: + description: >- + A Transaction represents a real transaction that affects a Financial + Connections Account balance. + properties: + account: + description: >- + The ID of the Financial Connections Account this transaction belongs + to. + maxLength: 5000 + type: string + amount: + description: 'The amount of this transaction, in cents (or local equivalent).' + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + description: + description: The description of this transaction. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - financial_connections.transaction + type: string + status: + description: The status of the transaction. + enum: + - pending + - posted + - void + type: string + status_transitions: + $ref: >- + #/components/schemas/bank_connections_resource_transaction_resource_status_transitions + transacted_at: + description: >- + Time at which the transaction was transacted. Measured in seconds + since the Unix epoch. + format: unix-time + type: integer + transaction_refresh: + description: >- + The token of the transaction refresh that last updated or created + this transaction. + maxLength: 5000 + type: string + updated: + description: >- + Time at which the object was last updated. Measured in seconds since + the Unix epoch. + format: unix-time + type: integer + required: + - account + - amount + - currency + - description + - id + - livemode + - object + - status + - status_transitions + - transacted_at + - transaction_refresh + - updated + title: BankConnectionsResourceTransaction + type: object + x-expandableFields: + - status_transitions + x-resourceId: financial_connections.transaction + financial_reporting_finance_report_run_run_parameters: + description: '' + properties: + columns: + description: The set of output columns requested for inclusion in the report run. + items: + maxLength: 5000 + type: string + type: array + connected_account: + description: Connected account ID by which to filter the report run. + maxLength: 5000 + type: string + currency: + description: Currency of objects to be included in the report run. + format: currency + type: string + interval_end: + description: >- + Ending timestamp of data to be included in the report run. Can be + any UTC timestamp between 1 second after the user specified + `interval_start` and 1 second before this report's last + `data_available_end` value. + format: unix-time + type: integer + interval_start: + description: >- + Starting timestamp of data to be included in the report run. Can be + any UTC timestamp between 1 second after this report's + `data_available_start` and 1 second before the user specified + `interval_end` value. + format: unix-time + type: integer + payout: + description: Payout ID by which to filter the report run. + maxLength: 5000 + type: string + reporting_category: + description: Category of balance transactions to be included in the report run. + maxLength: 5000 + type: string + timezone: + description: >- + Defaults to `Etc/UTC`. The output timezone for all timestamps in the + report. A list of possible time zone values is maintained at the + [IANA Time Zone Database](http://www.iana.org/time-zones). Has no + effect on `interval_start` or `interval_end`. + maxLength: 5000 + type: string + title: FinancialReportingFinanceReportRunRunParameters + type: object + x-expandableFields: [] + forwarded_request_context: + description: Metadata about the forwarded request. + properties: + destination_duration: + description: >- + The time it took in milliseconds for the destination endpoint to + respond. + type: integer + destination_ip_address: + description: The IP address of the destination. + maxLength: 5000 + type: string + required: + - destination_duration + - destination_ip_address + title: ForwardedRequestContext + type: object + x-expandableFields: [] + forwarded_request_details: + description: Details about the request forwarded to the destination endpoint. + properties: + body: + description: The body payload to send to the destination endpoint. + maxLength: 5000 + type: string + headers: + description: >- + The headers to include in the forwarded request. Can be omitted if + no additional headers (excluding Stripe-generated ones such as the + Content-Type header) should be included. + items: + $ref: '#/components/schemas/forwarded_request_header' + type: array + http_method: + description: The HTTP method used to call the destination endpoint. + enum: + - POST + type: string + required: + - body + - headers + - http_method + title: ForwardedRequestDetails + type: object + x-expandableFields: + - headers + forwarded_request_header: + description: Header data. + properties: + name: + description: The header name. + maxLength: 5000 + type: string + value: + description: The header value. + maxLength: 5000 + type: string + required: + - name + - value + title: ForwardedRequestHeader + type: object + x-expandableFields: [] + forwarded_response_details: + description: Details about the response from the destination endpoint. + properties: + body: + description: The response body from the destination endpoint to Stripe. + maxLength: 5000 + type: string + headers: + description: HTTP headers that the destination endpoint returned. + items: + $ref: '#/components/schemas/forwarded_request_header' + type: array + status: + description: The HTTP status code that the destination endpoint returned. + type: integer + required: + - body + - headers + - status + title: ForwardedResponseDetails + type: object + x-expandableFields: + - headers + forwarding.request: + description: >- + Instructs Stripe to make a request on your behalf using the destination + URL. The destination URL + + is activated by Stripe at the time of onboarding. Stripe verifies + requests with your credentials + + provided during onboarding, and injects card details from the + payment_method into the request. + + + Stripe redacts all sensitive fields and headers, including + authentication credentials and card numbers, + + before storing the request and response data in the forwarding Request + object, which are subject to a + + 30-day retention period. + + + You can provide a Stripe idempotency key to make sure that requests with + the same key result in only one + + outbound request. The Stripe idempotency key provided should be unique + and different from any idempotency + + keys provided on the underlying third-party request. + + + Forwarding Requests are synchronous requests that return a response or + time out according to + + Stripe’s limits. + + + Related guide: [Forward card details to third-party API + endpoints](https://docs.stripe.com/payments/forwarding). + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - forwarding.request + type: string + payment_method: + description: >- + The PaymentMethod to insert into the forwarded request. Forwarding + previously consumed PaymentMethods is allowed. + maxLength: 5000 + type: string + replacements: + description: The field kinds to be replaced in the forwarded request. + items: + enum: + - card_cvc + - card_expiry + - card_number + - cardholder_name + - request_signature + type: string + x-stripeBypassValidation: true + type: array + request_context: + anyOf: + - $ref: '#/components/schemas/forwarded_request_context' + description: >- + Context about the request from Stripe's servers to the destination + endpoint. + nullable: true + request_details: + anyOf: + - $ref: '#/components/schemas/forwarded_request_details' + description: >- + The request that was sent to the destination endpoint. We redact any + sensitive fields. + nullable: true + response_details: + anyOf: + - $ref: '#/components/schemas/forwarded_response_details' + description: >- + The response that the destination endpoint returned to us. We redact + any sensitive fields. + nullable: true + url: + description: >- + The destination URL for the forwarded request. Must be supported by + the config. + maxLength: 5000 + nullable: true + type: string + required: + - created + - id + - livemode + - object + - payment_method + - replacements + title: ForwardingRequest + type: object + x-expandableFields: + - request_context + - request_details + - response_details + x-resourceId: forwarding.request + funding_instructions: + description: >- + Each customer has a + [`balance`](https://stripe.com/docs/api/customers/object#customer_object-balance) + that is + + automatically applied to future invoices and payments using the + `customer_balance` payment method. + + Customers can fund this balance by initiating a bank transfer to any + account in the + + `financial_addresses` field. + + Related guide: [Customer balance funding + instructions](https://stripe.com/docs/payments/customer-balance/funding-instructions) + properties: + bank_transfer: + $ref: '#/components/schemas/funding_instructions_bank_transfer' + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + funding_type: + description: The `funding_type` of the returned instructions + enum: + - bank_transfer + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - funding_instructions + type: string + required: + - bank_transfer + - currency + - funding_type + - livemode + - object + title: CustomerBalanceFundingInstructionsCustomerBalanceFundingInstructions + type: object + x-expandableFields: + - bank_transfer + x-resourceId: funding_instructions + funding_instructions_bank_transfer: + description: '' + properties: + country: + description: The country of the bank account to fund + maxLength: 5000 + type: string + financial_addresses: + description: >- + A list of financial addresses that can be used to fund a particular + balance + items: + $ref: >- + #/components/schemas/funding_instructions_bank_transfer_financial_address + type: array + type: + description: The bank_transfer type + enum: + - eu_bank_transfer + - jp_bank_transfer + type: string + x-stripeBypassValidation: true + required: + - country + - financial_addresses + - type + title: FundingInstructionsBankTransfer + type: object + x-expandableFields: + - financial_addresses + funding_instructions_bank_transfer_aba_record: + description: ABA Records contain U.S. bank account details per the ABA format. + properties: + account_holder_address: + $ref: '#/components/schemas/address' + account_holder_name: + description: The account holder name + maxLength: 5000 + type: string + account_number: + description: The ABA account number + maxLength: 5000 + type: string + account_type: + description: The account type + maxLength: 5000 + type: string + bank_address: + $ref: '#/components/schemas/address' + bank_name: + description: The bank name + maxLength: 5000 + type: string + routing_number: + description: The ABA routing number + maxLength: 5000 + type: string + required: + - account_holder_address + - account_holder_name + - account_number + - account_type + - bank_address + - bank_name + - routing_number + title: FundingInstructionsBankTransferABARecord + type: object + x-expandableFields: + - account_holder_address + - bank_address + funding_instructions_bank_transfer_financial_address: + description: >- + FinancialAddresses contain identifying information that resolves to a + FinancialAccount. + properties: + aba: + $ref: '#/components/schemas/funding_instructions_bank_transfer_aba_record' + iban: + $ref: '#/components/schemas/funding_instructions_bank_transfer_iban_record' + sort_code: + $ref: >- + #/components/schemas/funding_instructions_bank_transfer_sort_code_record + spei: + $ref: '#/components/schemas/funding_instructions_bank_transfer_spei_record' + supported_networks: + description: The payment networks supported by this FinancialAddress + items: + enum: + - ach + - bacs + - domestic_wire_us + - fps + - sepa + - spei + - swift + - zengin + type: string + x-stripeBypassValidation: true + type: array + swift: + $ref: '#/components/schemas/funding_instructions_bank_transfer_swift_record' + type: + description: The type of financial address + enum: + - aba + - iban + - sort_code + - spei + - swift + - zengin + type: string + x-stripeBypassValidation: true + zengin: + $ref: >- + #/components/schemas/funding_instructions_bank_transfer_zengin_record + required: + - type + title: FundingInstructionsBankTransferFinancialAddress + type: object + x-expandableFields: + - aba + - iban + - sort_code + - spei + - swift + - zengin + funding_instructions_bank_transfer_iban_record: + description: Iban Records contain E.U. bank account details per the SEPA format. + properties: + account_holder_address: + $ref: '#/components/schemas/address' + account_holder_name: + description: The name of the person or business that owns the bank account + maxLength: 5000 + type: string + bank_address: + $ref: '#/components/schemas/address' + bic: + description: The BIC/SWIFT code of the account. + maxLength: 5000 + type: string + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + type: string + iban: + description: The IBAN of the account. + maxLength: 5000 + type: string + required: + - account_holder_address + - account_holder_name + - bank_address + - bic + - country + - iban + title: FundingInstructionsBankTransferIbanRecord + type: object + x-expandableFields: + - account_holder_address + - bank_address + funding_instructions_bank_transfer_sort_code_record: + description: >- + Sort Code Records contain U.K. bank account details per the sort code + format. + properties: + account_holder_address: + $ref: '#/components/schemas/address' + account_holder_name: + description: The name of the person or business that owns the bank account + maxLength: 5000 + type: string + account_number: + description: The account number + maxLength: 5000 + type: string + bank_address: + $ref: '#/components/schemas/address' + sort_code: + description: The six-digit sort code + maxLength: 5000 + type: string + required: + - account_holder_address + - account_holder_name + - account_number + - bank_address + - sort_code + title: FundingInstructionsBankTransferSortCodeRecord + type: object + x-expandableFields: + - account_holder_address + - bank_address + funding_instructions_bank_transfer_spei_record: + description: SPEI Records contain Mexico bank account details per the SPEI format. + properties: + account_holder_address: + $ref: '#/components/schemas/address' + account_holder_name: + description: The account holder name + maxLength: 5000 + type: string + bank_address: + $ref: '#/components/schemas/address' + bank_code: + description: The three-digit bank code + maxLength: 5000 + type: string + bank_name: + description: The short banking institution name + maxLength: 5000 + type: string + clabe: + description: The CLABE number + maxLength: 5000 + type: string + required: + - account_holder_address + - account_holder_name + - bank_address + - bank_code + - bank_name + - clabe + title: FundingInstructionsBankTransferSpeiRecord + type: object + x-expandableFields: + - account_holder_address + - bank_address + funding_instructions_bank_transfer_swift_record: + description: SWIFT Records contain U.S. bank account details per the SWIFT format. + properties: + account_holder_address: + $ref: '#/components/schemas/address' + account_holder_name: + description: The account holder name + maxLength: 5000 + type: string + account_number: + description: The account number + maxLength: 5000 + type: string + account_type: + description: The account type + maxLength: 5000 + type: string + bank_address: + $ref: '#/components/schemas/address' + bank_name: + description: The bank name + maxLength: 5000 + type: string + swift_code: + description: The SWIFT code + maxLength: 5000 + type: string + required: + - account_holder_address + - account_holder_name + - account_number + - account_type + - bank_address + - bank_name + - swift_code + title: FundingInstructionsBankTransferSwiftRecord + type: object + x-expandableFields: + - account_holder_address + - bank_address + funding_instructions_bank_transfer_zengin_record: + description: Zengin Records contain Japan bank account details per the Zengin format. + properties: + account_holder_address: + $ref: '#/components/schemas/address' + account_holder_name: + description: The account holder name + maxLength: 5000 + nullable: true + type: string + account_number: + description: The account number + maxLength: 5000 + nullable: true + type: string + account_type: + description: 'The bank account type. In Japan, this can only be `futsu` or `toza`.' + maxLength: 5000 + nullable: true + type: string + bank_address: + $ref: '#/components/schemas/address' + bank_code: + description: The bank code of the account + maxLength: 5000 + nullable: true + type: string + bank_name: + description: The bank name of the account + maxLength: 5000 + nullable: true + type: string + branch_code: + description: The branch code of the account + maxLength: 5000 + nullable: true + type: string + branch_name: + description: The branch name of the account + maxLength: 5000 + nullable: true + type: string + required: + - account_holder_address + - bank_address + title: FundingInstructionsBankTransferZenginRecord + type: object + x-expandableFields: + - account_holder_address + - bank_address + gelato_data_document_report_date_of_birth: + description: Point in Time + properties: + day: + description: Numerical day between 1 and 31. + nullable: true + type: integer + month: + description: Numerical month between 1 and 12. + nullable: true + type: integer + year: + description: The four-digit year. + nullable: true + type: integer + title: GelatoDataDocumentReportDateOfBirth + type: object + x-expandableFields: [] + gelato_data_document_report_expiration_date: + description: Point in Time + properties: + day: + description: Numerical day between 1 and 31. + nullable: true + type: integer + month: + description: Numerical month between 1 and 12. + nullable: true + type: integer + year: + description: The four-digit year. + nullable: true + type: integer + title: GelatoDataDocumentReportExpirationDate + type: object + x-expandableFields: [] + gelato_data_document_report_issued_date: + description: Point in Time + properties: + day: + description: Numerical day between 1 and 31. + nullable: true + type: integer + month: + description: Numerical month between 1 and 12. + nullable: true + type: integer + year: + description: The four-digit year. + nullable: true + type: integer + title: GelatoDataDocumentReportIssuedDate + type: object + x-expandableFields: [] + gelato_data_id_number_report_date: + description: Point in Time + properties: + day: + description: Numerical day between 1 and 31. + nullable: true + type: integer + month: + description: Numerical month between 1 and 12. + nullable: true + type: integer + year: + description: The four-digit year. + nullable: true + type: integer + title: GelatoDataIdNumberReportDate + type: object + x-expandableFields: [] + gelato_data_verified_outputs_date: + description: Point in Time + properties: + day: + description: Numerical day between 1 and 31. + nullable: true + type: integer + month: + description: Numerical month between 1 and 12. + nullable: true + type: integer + year: + description: The four-digit year. + nullable: true + type: integer + title: GelatoDataVerifiedOutputsDate + type: object + x-expandableFields: [] + gelato_document_report: + description: Result from a document check + properties: + address: + anyOf: + - $ref: '#/components/schemas/address' + description: Address as it appears in the document. + nullable: true + dob: + anyOf: + - $ref: '#/components/schemas/gelato_data_document_report_date_of_birth' + description: Date of birth as it appears in the document. + nullable: true + error: + anyOf: + - $ref: '#/components/schemas/gelato_document_report_error' + description: >- + Details on the verification error. Present when status is + `unverified`. + nullable: true + expiration_date: + anyOf: + - $ref: '#/components/schemas/gelato_data_document_report_expiration_date' + description: Expiration date of the document. + nullable: true + files: + description: >- + Array of [File](https://stripe.com/docs/api/files) ids containing + images for this document. + items: + maxLength: 5000 + type: string + nullable: true + type: array + first_name: + description: First name as it appears in the document. + maxLength: 5000 + nullable: true + type: string + issued_date: + anyOf: + - $ref: '#/components/schemas/gelato_data_document_report_issued_date' + description: Issued date of the document. + nullable: true + issuing_country: + description: Issuing country of the document. + maxLength: 5000 + nullable: true + type: string + last_name: + description: Last name as it appears in the document. + maxLength: 5000 + nullable: true + type: string + number: + description: Document ID number. + maxLength: 5000 + nullable: true + type: string + sex: + description: Sex of the person in the document. + enum: + - '[redacted]' + - female + - male + - unknown + nullable: true + type: string + status: + description: Status of this `document` check. + enum: + - unverified + - verified + type: string + x-stripeBypassValidation: true + type: + description: Type of the document. + enum: + - driving_license + - id_card + - passport + nullable: true + type: string + unparsed_place_of_birth: + description: Place of birth as it appears in the document. + maxLength: 5000 + nullable: true + type: string + unparsed_sex: + description: Sex as it appears in the document. + maxLength: 5000 + nullable: true + type: string + required: + - status + title: GelatoDocumentReport + type: object + x-expandableFields: + - address + - dob + - error + - expiration_date + - issued_date + gelato_document_report_error: + description: '' + properties: + code: + description: >- + A short machine-readable string giving the reason for the + verification failure. + enum: + - document_expired + - document_type_not_supported + - document_unverified_other + nullable: true + type: string + x-stripeBypassValidation: true + reason: + description: >- + A human-readable message giving the reason for the failure. These + messages can be shown to your users. + maxLength: 5000 + nullable: true + type: string + title: GelatoDocumentReportError + type: object + x-expandableFields: [] + gelato_email_report: + description: Result from a email check + properties: + email: + description: Email to be verified. + maxLength: 5000 + nullable: true + type: string + error: + anyOf: + - $ref: '#/components/schemas/gelato_email_report_error' + description: >- + Details on the verification error. Present when status is + `unverified`. + nullable: true + status: + description: Status of this `email` check. + enum: + - unverified + - verified + type: string + x-stripeBypassValidation: true + required: + - status + title: GelatoEmailReport + type: object + x-expandableFields: + - error + gelato_email_report_error: + description: '' + properties: + code: + description: >- + A short machine-readable string giving the reason for the + verification failure. + enum: + - email_unverified_other + - email_verification_declined + nullable: true + type: string + reason: + description: >- + A human-readable message giving the reason for the failure. These + messages can be shown to your users. + maxLength: 5000 + nullable: true + type: string + title: GelatoEmailReportError + type: object + x-expandableFields: [] + gelato_id_number_report: + description: Result from an id_number check + properties: + dob: + anyOf: + - $ref: '#/components/schemas/gelato_data_id_number_report_date' + description: Date of birth. + nullable: true + error: + anyOf: + - $ref: '#/components/schemas/gelato_id_number_report_error' + description: >- + Details on the verification error. Present when status is + `unverified`. + nullable: true + first_name: + description: First name. + maxLength: 5000 + nullable: true + type: string + id_number: + description: >- + ID number. When `id_number_type` is `us_ssn`, only the last 4 digits + are present. + maxLength: 5000 + nullable: true + type: string + id_number_type: + description: Type of ID number. + enum: + - br_cpf + - sg_nric + - us_ssn + nullable: true + type: string + last_name: + description: Last name. + maxLength: 5000 + nullable: true + type: string + status: + description: Status of this `id_number` check. + enum: + - unverified + - verified + type: string + x-stripeBypassValidation: true + required: + - status + title: GelatoIdNumberReport + type: object + x-expandableFields: + - dob + - error + gelato_id_number_report_error: + description: '' + properties: + code: + description: >- + A short machine-readable string giving the reason for the + verification failure. + enum: + - id_number_insufficient_document_data + - id_number_mismatch + - id_number_unverified_other + nullable: true + type: string + reason: + description: >- + A human-readable message giving the reason for the failure. These + messages can be shown to your users. + maxLength: 5000 + nullable: true + type: string + title: GelatoIdNumberReportError + type: object + x-expandableFields: [] + gelato_phone_report: + description: Result from a phone check + properties: + error: + anyOf: + - $ref: '#/components/schemas/gelato_phone_report_error' + description: >- + Details on the verification error. Present when status is + `unverified`. + nullable: true + phone: + description: Phone to be verified. + maxLength: 5000 + nullable: true + type: string + status: + description: Status of this `phone` check. + enum: + - unverified + - verified + type: string + x-stripeBypassValidation: true + required: + - status + title: GelatoPhoneReport + type: object + x-expandableFields: + - error + gelato_phone_report_error: + description: '' + properties: + code: + description: >- + A short machine-readable string giving the reason for the + verification failure. + enum: + - phone_unverified_other + - phone_verification_declined + nullable: true + type: string + reason: + description: >- + A human-readable message giving the reason for the failure. These + messages can be shown to your users. + maxLength: 5000 + nullable: true + type: string + title: GelatoPhoneReportError + type: object + x-expandableFields: [] + gelato_provided_details: + description: '' + properties: + email: + description: Email of user being verified + maxLength: 5000 + type: string + phone: + description: Phone number of user being verified + maxLength: 5000 + type: string + title: GelatoProvidedDetails + type: object + x-expandableFields: [] + gelato_report_document_options: + description: '' + properties: + allowed_types: + description: >- + Array of strings of allowed identity document types. If the provided + identity document isn’t one of the allowed types, the verification + check will fail with a document_type_not_allowed error code. + items: + enum: + - driving_license + - id_card + - passport + type: string + type: array + require_id_number: + description: >- + Collect an ID number and perform an [ID number + check](https://stripe.com/docs/identity/verification-checks?type=id-number) + with the document’s extracted name and date of birth. + type: boolean + require_live_capture: + description: >- + Disable image uploads, identity document images have to be captured + using the device’s camera. + type: boolean + require_matching_selfie: + description: >- + Capture a face image and perform a [selfie + check](https://stripe.com/docs/identity/verification-checks?type=selfie) + comparing a photo ID and a picture of your user’s face. [Learn + more](https://stripe.com/docs/identity/selfie). + type: boolean + title: GelatoReportDocumentOptions + type: object + x-expandableFields: [] + gelato_report_id_number_options: + description: '' + properties: {} + title: GelatoReportIdNumberOptions + type: object + x-expandableFields: [] + gelato_selfie_report: + description: Result from a selfie check + properties: + document: + description: >- + ID of the [File](https://stripe.com/docs/api/files) holding the + image of the identity document used in this check. + maxLength: 5000 + nullable: true + type: string + error: + anyOf: + - $ref: '#/components/schemas/gelato_selfie_report_error' + description: >- + Details on the verification error. Present when status is + `unverified`. + nullable: true + selfie: + description: >- + ID of the [File](https://stripe.com/docs/api/files) holding the + image of the selfie used in this check. + maxLength: 5000 + nullable: true + type: string + status: + description: Status of this `selfie` check. + enum: + - unverified + - verified + type: string + x-stripeBypassValidation: true + required: + - status + title: GelatoSelfieReport + type: object + x-expandableFields: + - error + gelato_selfie_report_error: + description: '' + properties: + code: + description: >- + A short machine-readable string giving the reason for the + verification failure. + enum: + - selfie_document_missing_photo + - selfie_face_mismatch + - selfie_manipulated + - selfie_unverified_other + nullable: true + type: string + reason: + description: >- + A human-readable message giving the reason for the failure. These + messages can be shown to your users. + maxLength: 5000 + nullable: true + type: string + title: GelatoSelfieReportError + type: object + x-expandableFields: [] + gelato_session_document_options: + description: '' + properties: + allowed_types: + description: >- + Array of strings of allowed identity document types. If the provided + identity document isn’t one of the allowed types, the verification + check will fail with a document_type_not_allowed error code. + items: + enum: + - driving_license + - id_card + - passport + type: string + type: array + require_id_number: + description: >- + Collect an ID number and perform an [ID number + check](https://stripe.com/docs/identity/verification-checks?type=id-number) + with the document’s extracted name and date of birth. + type: boolean + require_live_capture: + description: >- + Disable image uploads, identity document images have to be captured + using the device’s camera. + type: boolean + require_matching_selfie: + description: >- + Capture a face image and perform a [selfie + check](https://stripe.com/docs/identity/verification-checks?type=selfie) + comparing a photo ID and a picture of your user’s face. [Learn + more](https://stripe.com/docs/identity/selfie). + type: boolean + title: GelatoSessionDocumentOptions + type: object + x-expandableFields: [] + gelato_session_email_options: + description: '' + properties: + require_verification: + description: Request one time password verification of `provided_details.email`. + type: boolean + title: GelatoSessionEmailOptions + type: object + x-expandableFields: [] + gelato_session_id_number_options: + description: '' + properties: {} + title: GelatoSessionIdNumberOptions + type: object + x-expandableFields: [] + gelato_session_last_error: + description: Shows last VerificationSession error + properties: + code: + description: >- + A short machine-readable string giving the reason for the + verification or user-session failure. + enum: + - abandoned + - consent_declined + - country_not_supported + - device_not_supported + - document_expired + - document_type_not_supported + - document_unverified_other + - email_unverified_other + - email_verification_declined + - id_number_insufficient_document_data + - id_number_mismatch + - id_number_unverified_other + - phone_unverified_other + - phone_verification_declined + - selfie_document_missing_photo + - selfie_face_mismatch + - selfie_manipulated + - selfie_unverified_other + - under_supported_age + nullable: true + type: string + x-stripeBypassValidation: true + reason: + description: >- + A message that explains the reason for verification or user-session + failure. + maxLength: 5000 + nullable: true + type: string + title: GelatoSessionLastError + type: object + x-expandableFields: [] + gelato_session_phone_options: + description: '' + properties: + require_verification: + description: Request one time password verification of `provided_details.phone`. + type: boolean + title: GelatoSessionPhoneOptions + type: object + x-expandableFields: [] + gelato_verification_report_options: + description: '' + properties: + document: + $ref: '#/components/schemas/gelato_report_document_options' + id_number: + $ref: '#/components/schemas/gelato_report_id_number_options' + title: GelatoVerificationReportOptions + type: object + x-expandableFields: + - document + - id_number + gelato_verification_session_options: + description: '' + properties: + document: + $ref: '#/components/schemas/gelato_session_document_options' + email: + $ref: '#/components/schemas/gelato_session_email_options' + id_number: + $ref: '#/components/schemas/gelato_session_id_number_options' + phone: + $ref: '#/components/schemas/gelato_session_phone_options' + title: GelatoVerificationSessionOptions + type: object + x-expandableFields: + - document + - email + - id_number + - phone + gelato_verified_outputs: + description: '' + properties: + address: + anyOf: + - $ref: '#/components/schemas/address' + description: The user's verified address. + nullable: true + dob: + anyOf: + - $ref: '#/components/schemas/gelato_data_verified_outputs_date' + description: The user’s verified date of birth. + nullable: true + email: + description: The user's verified email address + maxLength: 5000 + nullable: true + type: string + first_name: + description: The user's verified first name. + maxLength: 5000 + nullable: true + type: string + id_number: + description: The user's verified id number. + maxLength: 5000 + nullable: true + type: string + id_number_type: + description: The user's verified id number type. + enum: + - br_cpf + - sg_nric + - us_ssn + nullable: true + type: string + last_name: + description: The user's verified last name. + maxLength: 5000 + nullable: true + type: string + phone: + description: The user's verified phone number + maxLength: 5000 + nullable: true + type: string + sex: + description: The user's verified sex. + enum: + - '[redacted]' + - female + - male + - unknown + nullable: true + type: string + unparsed_place_of_birth: + description: The user's verified place of birth as it appears in the document. + maxLength: 5000 + nullable: true + type: string + unparsed_sex: + description: The user's verified sex as it appears in the document. + maxLength: 5000 + nullable: true + type: string + title: GelatoVerifiedOutputs + type: object + x-expandableFields: + - address + - dob + identity.verification_report: + description: >- + A VerificationReport is the result of an attempt to collect and verify + data from a user. + + The collection of verification checks performed is determined from the + `type` and `options` + + parameters used. You can find the result of each verification check + performed in the + + appropriate sub-resource: `document`, `id_number`, `selfie`. + + + Each VerificationReport contains a copy of any data collected by the + user as well as + + reference IDs which can be used to access collected images through the + [FileUpload](https://stripe.com/docs/api/files) + + API. To configure and create VerificationReports, use the + + [VerificationSession](https://stripe.com/docs/api/identity/verification_sessions) + API. + + + Related guide: [Accessing verification + results](https://stripe.com/docs/identity/verification-sessions#results). + properties: + client_reference_id: + description: >- + A string to reference this user. This can be a customer ID, a + session ID, or similar, and can be used to reconcile this + verification with your internal systems. + maxLength: 5000 + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + document: + $ref: '#/components/schemas/gelato_document_report' + email: + $ref: '#/components/schemas/gelato_email_report' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + id_number: + $ref: '#/components/schemas/gelato_id_number_report' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - identity.verification_report + type: string + options: + $ref: '#/components/schemas/gelato_verification_report_options' + phone: + $ref: '#/components/schemas/gelato_phone_report' + selfie: + $ref: '#/components/schemas/gelato_selfie_report' + type: + description: Type of report. + enum: + - document + - id_number + - verification_flow + type: string + x-stripeBypassValidation: true + verification_flow: + description: The configuration token of a verification flow from the dashboard. + maxLength: 5000 + type: string + verification_session: + description: ID of the VerificationSession that created this report. + maxLength: 5000 + nullable: true + type: string + required: + - created + - id + - livemode + - object + - type + title: GelatoVerificationReport + type: object + x-expandableFields: + - document + - email + - id_number + - options + - phone + - selfie + x-resourceId: identity.verification_report + identity.verification_session: + description: >- + A VerificationSession guides you through the process of collecting and + verifying the identities + + of your users. It contains details about the type of verification, such + as what [verification + + check](/docs/identity/verification-checks) to perform. Only create one + VerificationSession for + + each verification in your system. + + + A VerificationSession transitions through [multiple + + statuses](/docs/identity/how-sessions-work) throughout its lifetime as + it progresses through + + the verification flow. The VerificationSession contains the user's + verified data after + + verification checks are complete. + + + Related guide: [The Verification Sessions + API](https://stripe.com/docs/identity/verification-sessions) + properties: + client_reference_id: + description: >- + A string to reference this user. This can be a customer ID, a + session ID, or similar, and can be used to reconcile this + verification with your internal systems. + maxLength: 5000 + nullable: true + type: string + client_secret: + description: >- + The short-lived client secret used by Stripe.js to [show a + verification modal](https://stripe.com/docs/js/identity/modal) + inside your app. This client secret expires after 24 hours and can + only be used once. Don’t store it, log it, embed it in a URL, or + expose it to anyone other than the user. Make sure that you have TLS + enabled on any page that includes the client secret. Refer to our + docs on [passing the client secret to the + frontend](https://stripe.com/docs/identity/verification-sessions#client-secret) + to learn more. + maxLength: 5000 + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + last_error: + anyOf: + - $ref: '#/components/schemas/gelato_session_last_error' + description: >- + If present, this property tells you the last error encountered when + processing the verification. + nullable: true + last_verification_report: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/identity.verification_report' + description: >- + ID of the most recent VerificationReport. [Learn more about + accessing detailed verification + results.](https://stripe.com/docs/identity/verification-sessions#results) + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/identity.verification_report' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - identity.verification_session + type: string + options: + anyOf: + - $ref: '#/components/schemas/gelato_verification_session_options' + description: A set of options for the session’s verification checks. + nullable: true + provided_details: + anyOf: + - $ref: '#/components/schemas/gelato_provided_details' + description: >- + Details provided about the user being verified. These details may be + shown to the user. + nullable: true + redaction: + anyOf: + - $ref: '#/components/schemas/verification_session_redaction' + description: >- + Redaction status of this VerificationSession. If the + VerificationSession is not redacted, this field will be null. + nullable: true + related_customer: + description: Customer ID + maxLength: 5000 + nullable: true + type: string + status: + description: >- + Status of this VerificationSession. [Learn more about the lifecycle + of sessions](https://stripe.com/docs/identity/how-sessions-work). + enum: + - canceled + - processing + - requires_input + - verified + type: string + type: + description: >- + The type of [verification + check](https://stripe.com/docs/identity/verification-checks) to be + performed. + enum: + - document + - id_number + - verification_flow + type: string + x-stripeBypassValidation: true + url: + description: >- + The short-lived URL that you use to redirect a user to Stripe to + submit their identity information. This URL expires after 48 hours + and can only be used once. Don’t store it, log it, send it in emails + or expose it to anyone other than the user. Refer to our docs on + [verifying identity + documents](https://stripe.com/docs/identity/verify-identity-documents?platform=web&type=redirect) + to learn how to redirect users to Stripe. + maxLength: 5000 + nullable: true + type: string + verification_flow: + description: The configuration token of a verification flow from the dashboard. + maxLength: 5000 + type: string + verified_outputs: + anyOf: + - $ref: '#/components/schemas/gelato_verified_outputs' + description: The user’s verified data. + nullable: true + required: + - created + - id + - livemode + - metadata + - object + - status + - type + title: GelatoVerificationSession + type: object + x-expandableFields: + - last_error + - last_verification_report + - options + - provided_details + - redaction + - verified_outputs + x-resourceId: identity.verification_session + inbound_transfers: + description: '' + properties: + billing_details: + $ref: '#/components/schemas/treasury_shared_resource_billing_details' + type: + description: The type of the payment method used in the InboundTransfer. + enum: + - us_bank_account + type: string + x-stripeBypassValidation: true + us_bank_account: + $ref: >- + #/components/schemas/inbound_transfers_payment_method_details_us_bank_account + required: + - billing_details + - type + title: InboundTransfers + type: object + x-expandableFields: + - billing_details + - us_bank_account + inbound_transfers_payment_method_details_us_bank_account: + description: '' + properties: + account_holder_type: + description: 'Account holder type: individual or company.' + enum: + - company + - individual + nullable: true + type: string + account_type: + description: 'Account type: checkings or savings. Defaults to checking if omitted.' + enum: + - checking + - savings + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: ID of the mandate used to make this payment. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + network: + description: >- + The network rails used. See the + [docs](https://stripe.com/docs/treasury/money-movement/timelines) to + learn more about money movement timelines for each network type. + enum: + - ach + type: string + routing_number: + description: Routing number of the bank account. + maxLength: 5000 + nullable: true + type: string + required: + - network + title: inbound_transfers_payment_method_details_us_bank_account + type: object + x-expandableFields: + - mandate + internal_card: + description: '' + properties: + brand: + description: Brand of the card used in the transaction + maxLength: 5000 + nullable: true + type: string + country: + description: Two-letter ISO code representing the country of the card + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two digit number representing the card's expiration month + nullable: true + type: integer + exp_year: + description: Two digit number representing the card's expiration year + nullable: true + type: integer + last4: + description: The last 4 digits of the card + maxLength: 5000 + nullable: true + type: string + title: internal_card + type: object + x-expandableFields: [] + invoice: + description: >- + Invoices are statements of amounts owed by a customer, and are either + + generated one-off, or generated periodically from a subscription. + + + They contain [invoice items](https://stripe.com/docs/api#invoiceitems), + and proration adjustments + + that may be caused by subscription upgrades/downgrades (if necessary). + + + If your invoice is configured to be billed through automatic charges, + + Stripe automatically finalizes your invoice and attempts payment. Note + + that finalizing the invoice, + + [when + automatic](https://stripe.com/docs/invoicing/integration/automatic-advancement-collection), + does + + not happen immediately as the invoice is created. Stripe waits + + until one hour after the last webhook was successfully sent (or the last + + webhook timed out after failing). If you (and the platforms you may have + + connected to) have no webhooks configured, Stripe waits one hour after + + creation to finalize the invoice. + + + If your invoice is configured to be billed by sending an email, then + based on your + + [email + settings](https://dashboard.stripe.com/account/billing/automatic), + + Stripe will email the invoice to your customer and await payment. These + + emails can contain a link to a hosted page to pay the invoice. + + + Stripe applies any customer credit on the account before determining the + + amount due for the invoice (i.e., the amount that will be actually + + charged). If the amount due for the invoice is less than Stripe's + [minimum allowed charge + + per currency](/docs/currencies#minimum-and-maximum-charge-amounts), the + + invoice is automatically marked paid, and we add the amount due to the + + customer's credit balance which is applied to the next invoice. + + + More details on the customer's credit balance are + + [here](https://stripe.com/docs/billing/customer/balance). + + + Related guide: [Send invoices to + customers](https://stripe.com/docs/billing/invoices/sending) + properties: + account_country: + description: >- + The country of the business associated with this invoice, most often + the business creating the invoice. + maxLength: 5000 + nullable: true + type: string + account_name: + description: >- + The public name of the business associated with this invoice, most + often the business creating the invoice. + maxLength: 5000 + nullable: true + type: string + account_tax_ids: + description: >- + The account tax IDs associated with the invoice. Only editable when + the invoice is a draft. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + nullable: true + type: array + amount_due: + description: >- + Final amount due at this time for this invoice. If the invoice's + total is smaller than the minimum charge amount, for example, or if + there is account credit that can be applied to the invoice, the + `amount_due` may be 0. If there is a positive `starting_balance` for + the invoice (the customer owes money), the `amount_due` will also + take that into account. The charge that gets generated for the + invoice will be for the amount specified in `amount_due`. + type: integer + amount_overpaid: + description: >- + Amount that was overpaid on the invoice. The amount overpaid is + credited to the customer's credit balance. + type: integer + amount_paid: + description: 'The amount, in cents (or local equivalent), that was paid.' + type: integer + amount_remaining: + description: >- + The difference between amount_due and amount_paid, in cents (or + local equivalent). + type: integer + amount_shipping: + description: This is the sum of all the shipping amounts. + type: integer + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + description: ID of the Connect Application that created the invoice. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + attempt_count: + description: >- + Number of payment attempts made for this invoice, from the + perspective of the payment retry schedule. Any payment attempt + counts as the first attempt, and subsequently only automatic retries + increment the attempt count. In other words, manual payment attempts + after the first attempt do not affect the retry schedule. If a + failure is returned with a non-retryable return code, the invoice + can no longer be retried unless a new payment method is obtained. + Retries will continue to be scheduled, and attempt_count will + continue to increment, but retries will only be executed if a new + payment method is obtained. + type: integer + attempted: + description: >- + Whether an attempt has been made to pay the invoice. An invoice is + not attempted until 1 hour after the `invoice.created` webhook, for + example, so you might not want to display that invoice as unpaid to + your users. + type: boolean + auto_advance: + description: >- + Controls whether Stripe performs [automatic + collection](https://stripe.com/docs/invoicing/integration/automatic-advancement-collection) + of the invoice. If `false`, the invoice's state doesn't + automatically advance without an explicit action. + type: boolean + automatic_tax: + $ref: '#/components/schemas/automatic_tax' + automatically_finalizes_at: + description: >- + The time when this invoice is currently scheduled to be + automatically finalized. The field will be `null` if the invoice is + not scheduled to finalize in the future. If the invoice is not in + the draft state, this field will always be `null` - see + `finalized_at` for the time when an already-finalized invoice was + finalized. + format: unix-time + nullable: true + type: integer + billing_reason: + description: >- + Indicates the reason why the invoice was created. + + + * `manual`: Unrelated to a subscription, for example, created via + the invoice editor. + + * `subscription`: No longer in use. Applies to subscriptions from + before May 2018 where no distinction was made between updates, + cycles, and thresholds. + + * `subscription_create`: A new subscription was created. + + * `subscription_cycle`: A subscription advanced into a new period. + + * `subscription_threshold`: A subscription reached a billing + threshold. + + * `subscription_update`: A subscription was updated. + + * `upcoming`: Reserved for simulated invoices, per the upcoming + invoice endpoint. + enum: + - automatic_pending_invoice_item_invoice + - manual + - quote_accept + - subscription + - subscription_create + - subscription_cycle + - subscription_threshold + - subscription_update + - upcoming + nullable: true + type: string + collection_method: + description: >- + Either `charge_automatically`, or `send_invoice`. When charging + automatically, Stripe will attempt to pay this invoice using the + default source attached to the customer. When sending an invoice, + Stripe will email this invoice to the customer with payment + instructions. + enum: + - charge_automatically + - send_invoice + type: string + confirmation_secret: + anyOf: + - $ref: '#/components/schemas/invoices_resource_confirmation_secret' + description: >- + The confirmation secret associated with this invoice. Currently, + this contains the client_secret of the PaymentIntent that Stripe + creates during invoice finalization. + nullable: true + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + custom_fields: + description: Custom fields displayed on the invoice. + items: + $ref: '#/components/schemas/invoice_setting_custom_field' + nullable: true + type: array + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: The ID of the customer who will be billed. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + customer_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + The customer's address. Until the invoice is finalized, this field + will equal `customer.address`. Once the invoice is finalized, this + field will no longer be updated. + nullable: true + customer_email: + description: >- + The customer's email. Until the invoice is finalized, this field + will equal `customer.email`. Once the invoice is finalized, this + field will no longer be updated. + maxLength: 5000 + nullable: true + type: string + customer_name: + description: >- + The customer's name. Until the invoice is finalized, this field will + equal `customer.name`. Once the invoice is finalized, this field + will no longer be updated. + maxLength: 5000 + nullable: true + type: string + customer_phone: + description: >- + The customer's phone number. Until the invoice is finalized, this + field will equal `customer.phone`. Once the invoice is finalized, + this field will no longer be updated. + maxLength: 5000 + nullable: true + type: string + customer_shipping: + anyOf: + - $ref: '#/components/schemas/shipping' + description: >- + The customer's shipping information. Until the invoice is finalized, + this field will equal `customer.shipping`. Once the invoice is + finalized, this field will no longer be updated. + nullable: true + customer_tax_exempt: + description: >- + The customer's tax exempt status. Until the invoice is finalized, + this field will equal `customer.tax_exempt`. Once the invoice is + finalized, this field will no longer be updated. + enum: + - exempt + - none + - reverse + nullable: true + type: string + customer_tax_ids: + description: >- + The customer's tax IDs. Until the invoice is finalized, this field + will contain the same tax IDs as `customer.tax_ids`. Once the + invoice is finalized, this field will no longer be updated. + items: + $ref: '#/components/schemas/invoices_resource_invoice_tax_id' + nullable: true + type: array + default_payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + ID of the default payment method for the invoice. It must belong to + the customer associated with the invoice. If not set, defaults to + the subscription's default payment method, if any, or to the default + payment method in the customer's invoice settings. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + default_source: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + description: >- + ID of the default payment source for the invoice. It must belong to + the customer associated with the invoice and be in a chargeable + state. If not set, defaults to the subscription's default source, if + any, or to the customer's default source. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + x-stripeBypassValidation: true + default_tax_rates: + description: 'The tax rates applied to this invoice, if any.' + items: + $ref: '#/components/schemas/tax_rate' + type: array + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. Referenced as 'memo' in the Dashboard. + maxLength: 5000 + nullable: true + type: string + discounts: + description: >- + The discounts applied to the invoice. Line item discounts are + applied before invoice discounts. Use `expand[]=discounts` to expand + each discount. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + - $ref: '#/components/schemas/deleted_discount' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + - $ref: '#/components/schemas/deleted_discount' + type: array + due_date: + description: >- + The date on which payment for this invoice is due. This value will + be `null` for invoices where + `collection_method=charge_automatically`. + format: unix-time + nullable: true + type: integer + effective_at: + description: >- + The date when this invoice is in effect. Same as `finalized_at` + unless overwritten. When defined, this value replaces the + system-generated 'Date of issue' printed on the invoice PDF and + receipt. + format: unix-time + nullable: true + type: integer + ending_balance: + description: >- + Ending customer balance after the invoice is finalized. Invoices are + finalized approximately an hour after successful webhook delivery or + when payment collection is attempted for the invoice. If the invoice + has not been finalized yet, this will be null. + nullable: true + type: integer + footer: + description: Footer displayed on the invoice. + maxLength: 5000 + nullable: true + type: string + from_invoice: + anyOf: + - $ref: '#/components/schemas/invoices_resource_from_invoice' + description: >- + Details of the invoice that was cloned. See the [revision + documentation](https://stripe.com/docs/invoicing/invoice-revisions) + for more details. + nullable: true + hosted_invoice_url: + description: >- + The URL for the hosted invoice page, which allows customers to view + and pay an invoice. If the invoice has not been finalized yet, this + will be null. + maxLength: 5000 + nullable: true + type: string + id: + description: >- + Unique identifier for the object. For preview invoices created using + the [create + preview](https://stripe.com/docs/api/invoices/create_preview) + endpoint, this id will be prefixed with `upcoming_in`. + maxLength: 5000 + type: string + invoice_pdf: + description: >- + The link to download the PDF for the invoice. If the invoice has not + been finalized yet, this will be null. + maxLength: 5000 + nullable: true + type: string + issuer: + $ref: '#/components/schemas/connect_account_reference' + last_finalization_error: + anyOf: + - $ref: '#/components/schemas/api_errors' + description: >- + The error encountered during the previous attempt to finalize the + invoice. This field is cleared when the invoice is successfully + finalized. + nullable: true + latest_revision: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: The ID of the most recent non-draft revision of this invoice + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + lines: + description: >- + The individual line items that make up the invoice. `lines` is + sorted as follows: (1) pending invoice items (including prorations) + in reverse chronological order, (2) subscription items in reverse + chronological order, and (3) invoice items added after invoice + creation in chronological order. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/line_item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: InvoiceLinesList + type: object + x-expandableFields: + - data + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + next_payment_attempt: + description: >- + The time at which payment will next be attempted. This value will be + `null` for invoices where `collection_method=send_invoice`. + format: unix-time + nullable: true + type: integer + number: + description: >- + A unique, identifying string that appears on emails sent to the + customer for this invoice. This starts with the customer's unique + invoice_prefix if it is specified. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - invoice + type: string + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account (if any) for which the funds of the invoice payment are + intended. If set, the invoice will be presented with the branding + and support information of the specified account. See the [Invoices + with Connect](https://stripe.com/docs/billing/invoices/connect) + documentation for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + parent: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_parents_invoice_parent + description: The parent that generated this invoice + nullable: true + payment_settings: + $ref: '#/components/schemas/invoices_payment_settings' + payments: + description: Payments for this invoice + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/invoice_payment' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: InvoicesPaymentsListInvoicePayments + type: object + x-expandableFields: + - data + period_end: + description: >- + End of the usage period during which invoice items were added to + this invoice. This looks back one period for a subscription invoice. + Use the [line item + period](/api/invoices/line_item#invoice_line_item_object-period) to + get the service period for each price. + format: unix-time + type: integer + period_start: + description: >- + Start of the usage period during which invoice items were added to + this invoice. This looks back one period for a subscription invoice. + Use the [line item + period](/api/invoices/line_item#invoice_line_item_object-period) to + get the service period for each price. + format: unix-time + type: integer + post_payment_credit_notes_amount: + description: >- + Total amount of all post-payment credit notes issued for this + invoice. + type: integer + pre_payment_credit_notes_amount: + description: >- + Total amount of all pre-payment credit notes issued for this + invoice. + type: integer + receipt_number: + description: >- + This is the transaction number that appears on email receipts sent + for this invoice. + maxLength: 5000 + nullable: true + type: string + rendering: + anyOf: + - $ref: '#/components/schemas/invoices_resource_invoice_rendering' + description: >- + The rendering-related settings that control how the invoice is + displayed on customer-facing surfaces such as PDF and Hosted Invoice + Page. + nullable: true + shipping_cost: + anyOf: + - $ref: '#/components/schemas/invoices_resource_shipping_cost' + description: >- + The details of the cost of shipping, including the ShippingRate + applied on the invoice. + nullable: true + shipping_details: + anyOf: + - $ref: '#/components/schemas/shipping' + description: >- + Shipping details for the invoice. The Invoice PDF will use the + `shipping_details` value if it is set, otherwise the PDF will render + the shipping address from the customer. + nullable: true + starting_balance: + description: >- + Starting customer balance before the invoice is finalized. If the + invoice has not been finalized yet, this will be the current + customer balance. For revision invoices, this also includes any + customer balance that was applied to the original invoice. + type: integer + statement_descriptor: + description: >- + Extra information about an invoice for the customer's credit card + statement. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + The status of the invoice, one of `draft`, `open`, `paid`, + `uncollectible`, or `void`. [Learn + more](https://stripe.com/docs/billing/invoices/workflow#workflow-overview) + enum: + - draft + - open + - paid + - uncollectible + - void + nullable: true + type: string + x-stripeBypassValidation: true + status_transitions: + $ref: '#/components/schemas/invoices_resource_status_transitions' + subtotal: + description: >- + Total of all subscriptions, invoice items, and prorations on the + invoice before any invoice level discount or exclusive tax is + applied. Item discounts are already incorporated + type: integer + subtotal_excluding_tax: + description: >- + The integer amount in cents (or local equivalent) representing the + subtotal of the invoice before any invoice level discount or tax is + applied. Item discounts are already incorporated + nullable: true + type: integer + test_clock: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/test_helpers.test_clock' + description: ID of the test clock this invoice belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/test_helpers.test_clock' + threshold_reason: + $ref: '#/components/schemas/invoice_threshold_reason' + total: + description: Total after discounts and taxes. + type: integer + total_discount_amounts: + description: The aggregate amounts calculated per discount across all line items. + items: + $ref: '#/components/schemas/discounts_resource_discount_amount' + nullable: true + type: array + total_excluding_tax: + description: >- + The integer amount in cents (or local equivalent) representing the + total amount of the invoice including all discounts but excluding + all tax. + nullable: true + type: integer + total_pretax_credit_amounts: + description: >- + Contains pretax credit amounts (ex: discount, credit grants, etc) + that apply to this invoice. This is a combined list of + total_pretax_credit_amounts across all invoice line items. + items: + $ref: '#/components/schemas/invoices_resource_pretax_credit_amount' + nullable: true + type: array + total_taxes: + description: The aggregate tax information of all line items. + items: + $ref: '#/components/schemas/billing_bill_resource_invoicing_taxes_tax' + nullable: true + type: array + webhooks_delivered_at: + description: >- + Invoices are automatically paid or sent 1 hour after webhooks are + delivered, or until all webhook delivery attempts have [been + exhausted](https://stripe.com/docs/billing/webhooks#understand). + This field tracks the time when webhooks for this invoice were + successfully delivered. If the invoice had no webhooks to deliver, + this will be set while the invoice is being created. + format: unix-time + nullable: true + type: integer + required: + - amount_due + - amount_overpaid + - amount_paid + - amount_remaining + - amount_shipping + - attempt_count + - attempted + - auto_advance + - automatic_tax + - collection_method + - created + - currency + - customer + - default_tax_rates + - discounts + - id + - issuer + - lines + - livemode + - object + - payment_settings + - period_end + - period_start + - post_payment_credit_notes_amount + - pre_payment_credit_notes_amount + - starting_balance + - status_transitions + - subtotal + - total + title: Invoice + type: object + x-expandableFields: + - account_tax_ids + - application + - automatic_tax + - confirmation_secret + - custom_fields + - customer + - customer_address + - customer_shipping + - customer_tax_ids + - default_payment_method + - default_source + - default_tax_rates + - discounts + - from_invoice + - issuer + - last_finalization_error + - latest_revision + - lines + - on_behalf_of + - parent + - payment_settings + - payments + - rendering + - shipping_cost + - shipping_details + - status_transitions + - test_clock + - threshold_reason + - total_discount_amounts + - total_pretax_credit_amounts + - total_taxes + x-resourceId: invoice + invoice_installments_card: + description: '' + properties: + enabled: + description: Whether Installments are enabled for this Invoice. + nullable: true + type: boolean + title: invoice_installments_card + type: object + x-expandableFields: [] + invoice_item_threshold_reason: + description: '' + properties: + line_item_ids: + description: The IDs of the line items that triggered the threshold invoice. + items: + maxLength: 5000 + type: string + type: array + usage_gte: + description: The quantity threshold boundary that applied to the given line item. + type: integer + required: + - line_item_ids + - usage_gte + title: InvoiceItemThresholdReason + type: object + x-expandableFields: [] + invoice_line_item_period: + description: '' + properties: + end: + description: >- + The end of the period, which must be greater than or equal to the + start. This value is inclusive. + format: unix-time + type: integer + start: + description: The start of the period. This value is inclusive. + format: unix-time + type: integer + required: + - end + - start + title: InvoiceLineItemPeriod + type: object + x-expandableFields: [] + invoice_mandate_options_card: + description: '' + properties: + amount: + description: Amount to be charged for future payments. + nullable: true + type: integer + amount_type: + description: >- + One of `fixed` or `maximum`. If `fixed`, the `amount` param refers + to the exact amount to be charged in future payments. If `maximum`, + the amount charged can be up to the value passed for the `amount` + param. + enum: + - fixed + - maximum + nullable: true + type: string + description: + description: >- + A description of the mandate or subscription that is meant to be + displayed to the customer. + maxLength: 200 + nullable: true + type: string + title: invoice_mandate_options_card + type: object + x-expandableFields: [] + invoice_payment: + description: >- + Invoice Payments represent payments made against invoices. Invoice + Payments can + + be accessed in two ways: + + 1. By expanding the `payments` field on the + [Invoice](https://stripe.com/docs/api#invoice) resource. + + 2. By using the Invoice Payment retrieve and list endpoints. + + + Invoice Payments include the mapping between payment objects, such as + Payment Intent, and Invoices. + + This resource and its endpoints allows you to easily track if a payment + is associated with a specific invoice and + + monitor the allocation details of the payments. + properties: + amount_paid: + description: >- + Amount that was actually paid for this invoice, in cents (or local + equivalent). This field is null until the payment is `paid`. This + amount can be less than the `amount_requested` if the + PaymentIntent’s `amount_received` is not sufficient to pay all of + the invoices that it is attached to. + nullable: true + type: integer + amount_requested: + description: >- + Amount intended to be paid toward this invoice, in cents (or local + equivalent) + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + - $ref: '#/components/schemas/deleted_invoice' + description: The invoice that was paid. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + - $ref: '#/components/schemas/deleted_invoice' + is_default: + description: >- + Stripe automatically creates a default InvoicePayment when the + invoice is finalized, and keeps it synchronized with the invoice’s + `amount_remaining`. The PaymentIntent associated with the default + payment can’t be edited or canceled directly. + type: boolean + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - invoice_payment + type: string + payment: + $ref: >- + #/components/schemas/invoices_payments_invoice_payment_associated_payment + status: + description: 'The status of the payment, one of `open`, `paid`, or `canceled`.' + maxLength: 5000 + type: string + status_transitions: + $ref: >- + #/components/schemas/invoices_payments_invoice_payment_status_transitions + required: + - amount_requested + - created + - currency + - id + - invoice + - is_default + - livemode + - object + - payment + - status + - status_transitions + title: InvoicesInvoicePayment + type: object + x-expandableFields: + - invoice + - payment + - status_transitions + x-resourceId: invoice_payment + invoice_payment_method_options_acss_debit: + description: '' + properties: + mandate_options: + $ref: >- + #/components/schemas/invoice_payment_method_options_acss_debit_mandate_options + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_acss_debit + type: object + x-expandableFields: + - mandate_options + invoice_payment_method_options_acss_debit_mandate_options: + description: '' + properties: + transaction_type: + description: Transaction type of the mandate. + enum: + - business + - personal + nullable: true + type: string + title: invoice_payment_method_options_acss_debit_mandate_options + type: object + x-expandableFields: [] + invoice_payment_method_options_bancontact: + description: '' + properties: + preferred_language: + description: >- + Preferred language of the Bancontact authorization page that the + customer is redirected to. + enum: + - de + - en + - fr + - nl + type: string + required: + - preferred_language + title: invoice_payment_method_options_bancontact + type: object + x-expandableFields: [] + invoice_payment_method_options_card: + description: '' + properties: + installments: + $ref: '#/components/schemas/invoice_installments_card' + request_three_d_secure: + description: >- + We strongly recommend that you rely on our SCA Engine to + automatically prompt your customers for authentication based on risk + level and [other + requirements](https://stripe.com/docs/strong-customer-authentication). + However, if you wish to request 3D Secure based on logic from your + own fraud engine, provide this option. Read our guide on [manually + requesting 3D + Secure](https://stripe.com/docs/payments/3d-secure/authentication-flow#manual-three-ds) + for more information on how this configuration interacts with Radar + and our SCA Engine. + enum: + - any + - automatic + - challenge + nullable: true + type: string + title: invoice_payment_method_options_card + type: object + x-expandableFields: + - installments + invoice_payment_method_options_customer_balance: + description: '' + properties: + bank_transfer: + $ref: >- + #/components/schemas/invoice_payment_method_options_customer_balance_bank_transfer + funding_type: + description: >- + The funding method type to be used when there are not enough funds + in the customer balance. Permitted values include: `bank_transfer`. + enum: + - bank_transfer + nullable: true + type: string + title: invoice_payment_method_options_customer_balance + type: object + x-expandableFields: + - bank_transfer + invoice_payment_method_options_customer_balance_bank_transfer: + description: '' + properties: + eu_bank_transfer: + $ref: >- + #/components/schemas/invoice_payment_method_options_customer_balance_bank_transfer_eu_bank_transfer + type: + description: >- + The bank transfer type that can be used for funding. Permitted + values include: `eu_bank_transfer`, `gb_bank_transfer`, + `jp_bank_transfer`, `mx_bank_transfer`, or `us_bank_transfer`. + nullable: true + type: string + title: invoice_payment_method_options_customer_balance_bank_transfer + type: object + x-expandableFields: + - eu_bank_transfer + invoice_payment_method_options_customer_balance_bank_transfer_eu_bank_transfer: + description: '' + properties: + country: + description: >- + The desired country code of the bank account information. Permitted + values include: `BE`, `DE`, `ES`, `FR`, `IE`, or `NL`. + enum: + - BE + - DE + - ES + - FR + - IE + - NL + type: string + required: + - country + title: >- + invoice_payment_method_options_customer_balance_bank_transfer_eu_bank_transfer + type: object + x-expandableFields: [] + invoice_payment_method_options_konbini: + description: '' + properties: {} + title: invoice_payment_method_options_konbini + type: object + x-expandableFields: [] + invoice_payment_method_options_sepa_debit: + description: '' + properties: {} + title: invoice_payment_method_options_sepa_debit + type: object + x-expandableFields: [] + invoice_payment_method_options_us_bank_account: + description: '' + properties: + financial_connections: + $ref: >- + #/components/schemas/invoice_payment_method_options_us_bank_account_linked_account_options + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_us_bank_account + type: object + x-expandableFields: + - financial_connections + invoice_payment_method_options_us_bank_account_linked_account_options: + description: '' + properties: + filters: + $ref: >- + #/components/schemas/invoice_payment_method_options_us_bank_account_linked_account_options_filters + permissions: + description: >- + The list of permissions to request. The `payment_method` permission + must be included. + items: + enum: + - balances + - ownership + - payment_method + - transactions + type: string + type: array + prefetch: + description: Data features requested to be retrieved upon account creation. + items: + enum: + - balances + - ownership + - transactions + type: string + x-stripeBypassValidation: true + nullable: true + type: array + title: invoice_payment_method_options_us_bank_account_linked_account_options + type: object + x-expandableFields: + - filters + invoice_payment_method_options_us_bank_account_linked_account_options_filters: + description: '' + properties: + account_subcategories: + description: >- + The account subcategories to use to filter for possible accounts to + link. Valid subcategories are `checking` and `savings`. + items: + enum: + - checking + - savings + type: string + type: array + title: >- + invoice_payment_method_options_us_bank_account_linked_account_options_filters + type: object + x-expandableFields: [] + invoice_rendering_pdf: + description: '' + properties: + page_size: + description: >- + Page size of invoice pdf. Options include a4, letter, and auto. If + set to auto, page size will be switched to a4 or letter based on + customer locale. + enum: + - a4 + - auto + - letter + nullable: true + type: string + title: InvoiceRenderingPdf + type: object + x-expandableFields: [] + invoice_rendering_template: + description: >- + Invoice Rendering Templates are used to configure how invoices are + rendered on surfaces like the PDF. Invoice Rendering Templates + + can be created from within the Dashboard, and they can be used over the + API when creating invoices. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + nickname: + description: 'A brief description of the template, hidden from customers' + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - invoice_rendering_template + type: string + status: + description: 'The status of the template, one of `active` or `archived`.' + enum: + - active + - archived + type: string + version: + description: >- + Version of this template; version increases by one when an update on + the template changes any field that controls invoice rendering + type: integer + required: + - created + - id + - livemode + - object + - status + - version + title: InvoiceRenderingTemplate + type: object + x-expandableFields: [] + x-resourceId: invoice_rendering_template + invoice_setting_checkout_rendering_options: + description: '' + properties: + amount_tax_display: + description: >- + How line-item prices and amounts will be displayed with respect to + tax on invoice PDFs. + maxLength: 5000 + nullable: true + type: string + title: invoice_setting_checkout_rendering_options + type: object + x-expandableFields: [] + invoice_setting_custom_field: + description: '' + properties: + name: + description: The name of the custom field. + maxLength: 5000 + type: string + value: + description: The value of the custom field. + maxLength: 5000 + type: string + required: + - name + - value + title: InvoiceSettingCustomField + type: object + x-expandableFields: [] + invoice_setting_customer_rendering_options: + description: '' + properties: + amount_tax_display: + description: >- + How line-item prices and amounts will be displayed with respect to + tax on invoice PDFs. + maxLength: 5000 + nullable: true + type: string + template: + description: >- + ID of the invoice rendering template to be used for this customer's + invoices. If set, the template will be used on all invoices for this + customer unless a template is set directly on the invoice. + maxLength: 5000 + nullable: true + type: string + title: InvoiceSettingCustomerRenderingOptions + type: object + x-expandableFields: [] + invoice_setting_customer_setting: + description: '' + properties: + custom_fields: + description: Default custom fields to be displayed on invoices for this customer. + items: + $ref: '#/components/schemas/invoice_setting_custom_field' + nullable: true + type: array + default_payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + ID of a payment method that's attached to the customer, to be used + as the customer's default payment method for subscriptions and + invoices. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + footer: + description: Default footer to be displayed on invoices for this customer. + maxLength: 5000 + nullable: true + type: string + rendering_options: + anyOf: + - $ref: '#/components/schemas/invoice_setting_customer_rendering_options' + description: Default options for invoice PDF rendering for this customer. + nullable: true + title: InvoiceSettingCustomerSetting + type: object + x-expandableFields: + - custom_fields + - default_payment_method + - rendering_options + invoice_setting_quote_setting: + description: '' + properties: + days_until_due: + description: >- + Number of days within which a customer must pay invoices generated + by this quote. This value will be `null` for quotes where + `collection_method=charge_automatically`. + nullable: true + type: integer + issuer: + $ref: '#/components/schemas/connect_account_reference' + required: + - issuer + title: InvoiceSettingQuoteSetting + type: object + x-expandableFields: + - issuer + invoice_setting_subscription_schedule_phase_setting: + description: '' + properties: + account_tax_ids: + description: >- + The account tax IDs associated with this phase of the subscription + schedule. Will be set on invoices generated by this phase of the + subscription schedule. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + nullable: true + type: array + days_until_due: + description: >- + Number of days within which a customer must pay invoices generated + by this subscription schedule. This value will be `null` for + subscription schedules where `billing=charge_automatically`. + nullable: true + type: integer + issuer: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The connected account that issues the invoice. The invoice is + presented with the branding and support information of the specified + account. + nullable: true + title: InvoiceSettingSubscriptionSchedulePhaseSetting + type: object + x-expandableFields: + - account_tax_ids + - issuer + invoice_setting_subscription_schedule_setting: + description: '' + properties: + account_tax_ids: + description: >- + The account tax IDs associated with the subscription schedule. Will + be set on invoices generated by the subscription schedule. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + nullable: true + type: array + days_until_due: + description: >- + Number of days within which a customer must pay invoices generated + by this subscription schedule. This value will be `null` for + subscription schedules where `billing=charge_automatically`. + nullable: true + type: integer + issuer: + $ref: '#/components/schemas/connect_account_reference' + required: + - issuer + title: InvoiceSettingSubscriptionScheduleSetting + type: object + x-expandableFields: + - account_tax_ids + - issuer + invoice_threshold_reason: + description: '' + properties: + amount_gte: + description: >- + The total invoice amount threshold boundary if it triggered the + threshold invoice. + nullable: true + type: integer + item_reasons: + description: Indicates which line items triggered a threshold invoice. + items: + $ref: '#/components/schemas/invoice_item_threshold_reason' + type: array + required: + - item_reasons + title: InvoiceThresholdReason + type: object + x-expandableFields: + - item_reasons + invoiceitem: + description: >- + Invoice Items represent the component lines of an + [invoice](https://stripe.com/docs/api/invoices). An invoice item is + added to an + + invoice by creating or updating it with an `invoice` field, at which + point it will be included as + + [an invoice line item](https://stripe.com/docs/api/invoices/line_item) + within + + [invoice.lines](https://stripe.com/docs/api/invoices/object#invoice_object-lines). + + + Invoice Items can be created before you are ready to actually send the + invoice. This can be particularly useful when combined + + with a [subscription](https://stripe.com/docs/api/subscriptions). + Sometimes you want to add a charge or credit to a customer, but actually + charge + + or credit the customer’s card only at the end of a regular billing + cycle. This is useful for combining several charges + + (to minimize per-transaction fees), or for having Stripe tabulate your + usage-based billing totals. + + + Related guides: [Integrate with the Invoicing + API](https://stripe.com/docs/invoicing/integration), [Subscription + Invoices](https://stripe.com/docs/billing/invoices/subscription#adding-upcoming-invoice-items). + properties: + amount: + description: >- + Amount (in the `currency` specified) of the invoice item. This + should always be equal to `unit_amount * quantity`. + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: >- + The ID of the customer who will be billed when this invoice item is + billed. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + date: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + discountable: + description: >- + If true, discounts will apply to this invoice item. Always false for + prorations. + type: boolean + discounts: + description: >- + The discounts which apply to the invoice item. Item discounts are + applied before invoice discounts. Use `expand[]=discounts` to expand + each discount. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + nullable: true + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: The ID of the invoice this invoice item belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - invoiceitem + type: string + parent: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoice_item_parents_invoice_item_parent + description: The parent that generated this invoice item. + nullable: true + period: + $ref: '#/components/schemas/invoice_line_item_period' + pricing: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_pricing_pricing + description: The pricing information of the invoice item. + nullable: true + proration: + description: >- + Whether the invoice item was created automatically as a proration + adjustment when the customer switched plans. + type: boolean + quantity: + description: >- + Quantity of units for the invoice item. If the invoice item is a + proration, the quantity of the subscription that the proration was + computed for. + type: integer + tax_rates: + description: >- + The tax rates which apply to the invoice item. When set, the + `default_tax_rates` on the invoice do not apply to this invoice + item. + items: + $ref: '#/components/schemas/tax_rate' + nullable: true + type: array + test_clock: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/test_helpers.test_clock' + description: ID of the test clock this invoice item belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/test_helpers.test_clock' + required: + - amount + - currency + - customer + - date + - discountable + - id + - livemode + - object + - period + - proration + - quantity + title: InvoiceItem + type: object + x-expandableFields: + - customer + - discounts + - invoice + - parent + - period + - pricing + - tax_rates + - test_clock + x-resourceId: invoiceitem + invoices_payment_method_options: + description: '' + properties: + acss_debit: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_acss_debit' + description: >- + If paying by `acss_debit`, this sub-hash contains details about the + Canadian pre-authorized debit payment method options to pass to the + invoice’s PaymentIntent. + nullable: true + bancontact: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_bancontact' + description: >- + If paying by `bancontact`, this sub-hash contains details about the + Bancontact payment method options to pass to the invoice’s + PaymentIntent. + nullable: true + card: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_card' + description: >- + If paying by `card`, this sub-hash contains details about the Card + payment method options to pass to the invoice’s PaymentIntent. + nullable: true + customer_balance: + anyOf: + - $ref: >- + #/components/schemas/invoice_payment_method_options_customer_balance + description: >- + If paying by `customer_balance`, this sub-hash contains details + about the Bank transfer payment method options to pass to the + invoice’s PaymentIntent. + nullable: true + konbini: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_konbini' + description: >- + If paying by `konbini`, this sub-hash contains details about the + Konbini payment method options to pass to the invoice’s + PaymentIntent. + nullable: true + sepa_debit: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_sepa_debit' + description: >- + If paying by `sepa_debit`, this sub-hash contains details about the + SEPA Direct Debit payment method options to pass to the invoice’s + PaymentIntent. + nullable: true + us_bank_account: + anyOf: + - $ref: >- + #/components/schemas/invoice_payment_method_options_us_bank_account + description: >- + If paying by `us_bank_account`, this sub-hash contains details about + the ACH direct debit payment method options to pass to the invoice’s + PaymentIntent. + nullable: true + title: InvoicesPaymentMethodOptions + type: object + x-expandableFields: + - acss_debit + - bancontact + - card + - customer_balance + - konbini + - sepa_debit + - us_bank_account + invoices_payment_settings: + description: '' + properties: + default_mandate: + description: >- + ID of the mandate to be used for this invoice. It must correspond to + the payment method used to pay the invoice, including the invoice's + default_payment_method or default_source, if set. + maxLength: 5000 + nullable: true + type: string + payment_method_options: + anyOf: + - $ref: '#/components/schemas/invoices_payment_method_options' + description: >- + Payment-method-specific configuration to provide to the invoice’s + PaymentIntent. + nullable: true + payment_method_types: + description: >- + The list of payment method types (e.g. card) to provide to the + invoice’s PaymentIntent. If not set, Stripe attempts to + automatically determine the types to use by looking at the invoice’s + default payment method, the subscription’s default payment method, + the customer’s default payment method, and your [invoice template + settings](https://dashboard.stripe.com/settings/billing/invoice). + items: + enum: + - ach_credit_transfer + - ach_debit + - acss_debit + - affirm + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - boleto + - card + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - jp_credit_transfer + - kakao_pay + - klarna + - konbini + - kr_card + - link + - multibanco + - naver_pay + - nz_bank_account + - p24 + - payco + - paynow + - paypal + - promptpay + - revolut_pay + - sepa_credit_transfer + - sepa_debit + - sofort + - swish + - us_bank_account + - wechat_pay + type: string + x-stripeBypassValidation: true + nullable: true + type: array + title: InvoicesPaymentSettings + type: object + x-expandableFields: + - payment_method_options + invoices_payments_invoice_payment_associated_payment: + description: '' + properties: + charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: >- + ID of the successful charge for this payment when `type` is + `charge`.Note: charge is only surfaced if the charge object is not + associated with a payment intent. If the charge object does have a + payment intent, the Invoice Payment surfaces the payment intent + instead. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: >- + ID of the PaymentIntent associated with this payment when `type` is + `payment_intent`. Note: This property is only populated for invoices + finalized on or after March 15th, 2019. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + type: + description: Type of payment object associated with this invoice payment. + enum: + - charge + - payment_intent + type: string + required: + - type + title: InvoicesPaymentsInvoicePaymentAssociatedPayment + type: object + x-expandableFields: + - charge + - payment_intent + invoices_payments_invoice_payment_status_transitions: + description: '' + properties: + canceled_at: + description: The time that the payment was canceled. + format: unix-time + nullable: true + type: integer + paid_at: + description: The time that the payment succeeded. + format: unix-time + nullable: true + type: integer + title: InvoicesPaymentsInvoicePaymentStatusTransitions + type: object + x-expandableFields: [] + invoices_resource_confirmation_secret: + description: '' + properties: + client_secret: + description: >- + The client_secret of the payment that Stripe creates for the invoice + after finalization. + maxLength: 5000 + type: string + type: + description: >- + The type of client_secret. Currently this is always payment_intent, + referencing the default payment_intent that Stripe creates during + invoice finalization + maxLength: 5000 + type: string + required: + - client_secret + - type + title: InvoicesResourceConfirmationSecret + type: object + x-expandableFields: [] + invoices_resource_from_invoice: + description: '' + properties: + action: + description: The relation between this invoice and the cloned invoice + maxLength: 5000 + type: string + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: The invoice that was cloned. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + required: + - action + - invoice + title: InvoicesResourceFromInvoice + type: object + x-expandableFields: + - invoice + invoices_resource_invoice_rendering: + description: '' + properties: + amount_tax_display: + description: >- + How line-item prices and amounts will be displayed with respect to + tax on invoice PDFs. + maxLength: 5000 + nullable: true + type: string + pdf: + anyOf: + - $ref: '#/components/schemas/invoice_rendering_pdf' + description: Invoice pdf rendering options + nullable: true + template: + description: ID of the rendering template that the invoice is formatted by. + maxLength: 5000 + nullable: true + type: string + template_version: + description: Version of the rendering template that the invoice is using. + nullable: true + type: integer + title: InvoicesResourceInvoiceRendering + type: object + x-expandableFields: + - pdf + invoices_resource_invoice_tax_id: + description: '' + properties: + type: + description: >- + The type of the tax ID, one of `ad_nrt`, `ar_cuit`, `eu_vat`, + `bo_tin`, `br_cnpj`, `br_cpf`, `cn_tin`, `co_nit`, `cr_tin`, + `do_rcn`, `ec_ruc`, `eu_oss_vat`, `hr_oib`, `pe_ruc`, `ro_tin`, + `rs_pib`, `sv_nit`, `uy_ruc`, `ve_rif`, `vn_tin`, `gb_vat`, + `nz_gst`, `au_abn`, `au_arn`, `in_gst`, `no_vat`, `no_voec`, + `za_vat`, `ch_vat`, `mx_rfc`, `sg_uen`, `ru_inn`, `ru_kpp`, `ca_bn`, + `hk_br`, `es_cif`, `tw_vat`, `th_vat`, `jp_cn`, `jp_rn`, `jp_trn`, + `li_uid`, `li_vat`, `my_itn`, `us_ein`, `kr_brn`, `ca_qst`, + `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `my_sst`, + `sg_gst`, `ae_trn`, `cl_tin`, `sa_vat`, `id_npwp`, `my_frp`, + `il_vat`, `ge_vat`, `ua_vat`, `is_vat`, `bg_uic`, `hu_tin`, + `si_tin`, `ke_pin`, `tr_tin`, `eg_tin`, `ph_tin`, `al_tin`, + `bh_vat`, `kz_bin`, `ng_tin`, `om_vat`, `de_stn`, `ch_uid`, + `tz_vat`, `uz_vat`, `uz_tin`, `md_vat`, `ma_vat`, `by_tin`, + `ao_tin`, `bs_tin`, `bb_tin`, `cd_nif`, `mr_nif`, `me_pib`, + `zw_tin`, `ba_tin`, `gn_nif`, `mk_vat`, `sr_fin`, `sn_ninea`, + `am_tin`, `np_pan`, `tj_tin`, `ug_tin`, `zm_tin`, `kh_tin`, + `aw_tin`, `az_tin`, `bd_bin`, `bj_ifu`, `et_tin`, `kg_tin`, + `la_tin`, `cm_niu`, `cv_nif`, `bf_ifu`, or `unknown` + enum: + - ad_nrt + - ae_trn + - al_tin + - am_tin + - ao_tin + - ar_cuit + - au_abn + - au_arn + - aw_tin + - az_tin + - ba_tin + - bb_tin + - bd_bin + - bf_ifu + - bg_uic + - bh_vat + - bj_ifu + - bo_tin + - br_cnpj + - br_cpf + - bs_tin + - by_tin + - ca_bn + - ca_gst_hst + - ca_pst_bc + - ca_pst_mb + - ca_pst_sk + - ca_qst + - cd_nif + - ch_uid + - ch_vat + - cl_tin + - cm_niu + - cn_tin + - co_nit + - cr_tin + - cv_nif + - de_stn + - do_rcn + - ec_ruc + - eg_tin + - es_cif + - et_tin + - eu_oss_vat + - eu_vat + - gb_vat + - ge_vat + - gn_nif + - hk_br + - hr_oib + - hu_tin + - id_npwp + - il_vat + - in_gst + - is_vat + - jp_cn + - jp_rn + - jp_trn + - ke_pin + - kg_tin + - kh_tin + - kr_brn + - kz_bin + - la_tin + - li_uid + - li_vat + - ma_vat + - md_vat + - me_pib + - mk_vat + - mr_nif + - mx_rfc + - my_frp + - my_itn + - my_sst + - ng_tin + - no_vat + - no_voec + - np_pan + - nz_gst + - om_vat + - pe_ruc + - ph_tin + - ro_tin + - rs_pib + - ru_inn + - ru_kpp + - sa_vat + - sg_gst + - sg_uen + - si_tin + - sn_ninea + - sr_fin + - sv_nit + - th_vat + - tj_tin + - tr_tin + - tw_vat + - tz_vat + - ua_vat + - ug_tin + - unknown + - us_ein + - uy_ruc + - uz_tin + - uz_vat + - ve_rif + - vn_tin + - za_vat + - zm_tin + - zw_tin + type: string + value: + description: The value of the tax ID. + maxLength: 5000 + nullable: true + type: string + required: + - type + title: InvoicesResourceInvoiceTaxID + type: object + x-expandableFields: [] + invoices_resource_pretax_credit_amount: + description: '' + properties: + amount: + description: >- + The amount, in cents (or local equivalent), of the pretax credit + amount. + type: integer + credit_balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/billing.credit_balance_transaction' + description: >- + The credit balance transaction that was applied to get this pretax + credit amount. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/billing.credit_balance_transaction' + discount: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + - $ref: '#/components/schemas/deleted_discount' + description: The discount that was applied to get this pretax credit amount. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + - $ref: '#/components/schemas/deleted_discount' + type: + description: Type of the pretax credit amount referenced. + enum: + - credit_balance_transaction + - discount + type: string + x-stripeBypassValidation: true + required: + - amount + - type + title: InvoicesResourcePretaxCreditAmount + type: object + x-expandableFields: + - credit_balance_transaction + - discount + invoices_resource_shipping_cost: + description: '' + properties: + amount_subtotal: + description: Total shipping cost before any taxes are applied. + type: integer + amount_tax: + description: >- + Total tax amount applied due to shipping costs. If no tax was + applied, defaults to 0. + type: integer + amount_total: + description: Total shipping cost after taxes are applied. + type: integer + shipping_rate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/shipping_rate' + description: The ID of the ShippingRate for this invoice. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/shipping_rate' + taxes: + description: The taxes applied to the shipping rate. + items: + $ref: '#/components/schemas/line_items_tax_amount' + type: array + required: + - amount_subtotal + - amount_tax + - amount_total + title: InvoicesResourceShippingCost + type: object + x-expandableFields: + - shipping_rate + - taxes + invoices_resource_status_transitions: + description: '' + properties: + finalized_at: + description: The time that the invoice draft was finalized. + format: unix-time + nullable: true + type: integer + marked_uncollectible_at: + description: The time that the invoice was marked uncollectible. + format: unix-time + nullable: true + type: integer + paid_at: + description: The time that the invoice was paid. + format: unix-time + nullable: true + type: integer + voided_at: + description: The time that the invoice was voided. + format: unix-time + nullable: true + type: integer + title: InvoicesResourceStatusTransitions + type: object + x-expandableFields: [] + issuing.authorization: + description: >- + When an [issued card](https://stripe.com/docs/issuing) is used to make a + purchase, an Issuing `Authorization` + + object is created. + [Authorizations](https://stripe.com/docs/issuing/purchases/authorizations) + must be approved for the + + purchase to be completed successfully. + + + Related guide: [Issued card + authorizations](https://stripe.com/docs/issuing/purchases/authorizations) + properties: + amount: + description: >- + The total amount that was authorized or rejected. This amount is in + `currency` and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). `amount` + should be the same as `merchant_amount`, unless `currency` and + `merchant_currency` are different. + type: integer + amount_details: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_amount_details' + description: >- + Detailed breakdown of amount components. These amounts are + denominated in `currency` and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + nullable: true + approved: + description: Whether the authorization has been approved. + type: boolean + authorization_method: + description: How the card details were provided. + enum: + - chip + - contactless + - keyed_in + - online + - swipe + type: string + balance_transactions: + description: List of balance transactions associated with this authorization. + items: + $ref: '#/components/schemas/balance_transaction' + type: array + card: + $ref: '#/components/schemas/issuing.card' + cardholder: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.cardholder' + description: The cardholder to whom this authorization belongs. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.cardholder' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + The currency of the cardholder. This currency can be different from + the currency presented at authorization and the `merchant_currency` + field on this authorization. Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + fleet: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_fleet_data' + description: Fleet-specific information for authorizations using Fleet cards. + nullable: true + fraud_challenges: + description: >- + Fraud challenges sent to the cardholder, if this authorization was + declined for fraud risk reasons. + items: + $ref: '#/components/schemas/issuing_authorization_fraud_challenge' + nullable: true + type: array + fuel: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_fuel_data' + description: >- + Information about fuel that was purchased with this transaction. + Typically this information is received from the merchant after the + authorization has been approved and the fuel dispensed. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + merchant_amount: + description: >- + The total amount that was authorized or rejected. This amount is in + the `merchant_currency` and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + `merchant_amount` should be the same as `amount`, unless + `merchant_currency` and `currency` are different. + type: integer + merchant_currency: + description: >- + The local currency that was presented to the cardholder for the + authorization. This currency can be different from the cardholder + currency and the `currency` field on this authorization. + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + merchant_data: + $ref: '#/components/schemas/issuing_authorization_merchant_data' + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + network_data: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_network_data' + description: >- + Details about the authorization, such as identifiers, set by the + card network. + nullable: true + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.authorization + type: string + pending_request: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_pending_request' + description: >- + The pending authorization request. This field will only be non-null + during an `issuing_authorization.request` webhook. + nullable: true + request_history: + description: >- + History of every time a `pending_request` authorization was + approved/declined, either by you directly or by Stripe (e.g. based + on your spending_controls). If the merchant changes the + authorization by performing an incremental authorization, you can + look at this field to see the previous requests for the + authorization. This field can be helpful in determining why a given + authorization was approved/declined. + items: + $ref: '#/components/schemas/issuing_authorization_request' + type: array + status: + description: The current status of the authorization in its lifecycle. + enum: + - closed + - expired + - pending + - reversed + type: string + token: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.token' + description: >- + [Token](https://stripe.com/docs/api/issuing/tokens/object) object + used for this authorization. If a network token was not used for + this authorization, this field will be null. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.token' + transactions: + description: >- + List of + [transactions](https://stripe.com/docs/api/issuing/transactions) + associated with this authorization. + items: + $ref: '#/components/schemas/issuing.transaction' + type: array + treasury: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_treasury' + description: >- + [Treasury](https://stripe.com/docs/api/treasury) details related to + this authorization if it was created on a + [FinancialAccount](https://stripe.com/docs/api/treasury/financial_accounts). + nullable: true + verification_data: + $ref: '#/components/schemas/issuing_authorization_verification_data' + verified_by_fraud_challenge: + description: >- + Whether the authorization bypassed fraud risk checks because the + cardholder has previously completed a fraud challenge on a similar + high-risk authorization from the same merchant. + nullable: true + type: boolean + wallet: + description: >- + The digital wallet used for this transaction. One of `apple_pay`, + `google_pay`, or `samsung_pay`. Will populate as `null` when no + digital wallet was utilized. + maxLength: 5000 + nullable: true + type: string + required: + - amount + - approved + - authorization_method + - balance_transactions + - card + - created + - currency + - id + - livemode + - merchant_amount + - merchant_currency + - merchant_data + - metadata + - object + - request_history + - status + - transactions + - verification_data + title: IssuingAuthorization + type: object + x-expandableFields: + - amount_details + - balance_transactions + - card + - cardholder + - fleet + - fraud_challenges + - fuel + - merchant_data + - network_data + - pending_request + - request_history + - token + - transactions + - treasury + - verification_data + x-resourceId: issuing.authorization + issuing.card: + description: >- + You can [create physical or virtual + cards](https://stripe.com/docs/issuing) that are issued to cardholders. + properties: + brand: + description: The brand of the card. + maxLength: 5000 + type: string + cancellation_reason: + description: The reason why the card was canceled. + enum: + - design_rejected + - lost + - stolen + nullable: true + type: string + x-stripeBypassValidation: true + cardholder: + $ref: '#/components/schemas/issuing.cardholder' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Supported currencies are `usd` in the US, `eur` in the + EU, and `gbp` in the UK. + format: currency + type: string + cvc: + description: >- + The card's CVC. For security reasons, this is only available for + virtual cards, and will be omitted unless you explicitly request it + with [the `expand` + parameter](https://stripe.com/docs/api/expanding_objects). + Additionally, it's only available via the ["Retrieve a card" + endpoint](https://stripe.com/docs/api/issuing/cards/retrieve), not + via "List all cards" or any other endpoint. + maxLength: 5000 + type: string + exp_month: + description: The expiration month of the card. + type: integer + exp_year: + description: The expiration year of the card. + type: integer + financial_account: + description: The financial account this card is attached to. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + last4: + description: The last 4 digits of the card number. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + number: + description: >- + The full unredacted card number. For security reasons, this is only + available for virtual cards, and will be omitted unless you + explicitly request it with [the `expand` + parameter](https://stripe.com/docs/api/expanding_objects). + Additionally, it's only available via the ["Retrieve a card" + endpoint](https://stripe.com/docs/api/issuing/cards/retrieve), not + via "List all cards" or any other endpoint. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.card + type: string + personalization_design: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.personalization_design' + description: The personalization design object belonging to this card. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.personalization_design' + replaced_by: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.card' + description: 'The latest card that replaces this card, if any.' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.card' + replacement_for: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.card' + description: 'The card this card replaces, if any.' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.card' + replacement_reason: + description: The reason why the previous card needed to be replaced. + enum: + - damaged + - expired + - lost + - stolen + nullable: true + type: string + x-stripeBypassValidation: true + shipping: + anyOf: + - $ref: '#/components/schemas/issuing_card_shipping' + description: Where and how the card will be shipped. + nullable: true + spending_controls: + $ref: '#/components/schemas/issuing_card_authorization_controls' + status: + description: >- + Whether authorizations can be approved on this card. May be blocked + from activating cards depending on past-due Cardholder requirements. + Defaults to `inactive`. + enum: + - active + - canceled + - inactive + type: string + x-stripeBypassValidation: true + type: + description: The type of the card. + enum: + - physical + - virtual + type: string + wallets: + anyOf: + - $ref: '#/components/schemas/issuing_card_wallets' + description: >- + Information relating to digital wallets (like Apple Pay and Google + Pay). + nullable: true + required: + - brand + - cardholder + - created + - currency + - exp_month + - exp_year + - id + - last4 + - livemode + - metadata + - object + - spending_controls + - status + - type + title: IssuingCard + type: object + x-expandableFields: + - cardholder + - personalization_design + - replaced_by + - replacement_for + - shipping + - spending_controls + - wallets + x-resourceId: issuing.card + issuing.cardholder: + description: >- + An Issuing `Cardholder` object represents an individual or business + entity who is [issued](https://stripe.com/docs/issuing) cards. + + + Related guide: [How to create a + cardholder](https://stripe.com/docs/issuing/cards/virtual/issue-cards#create-cardholder) + properties: + billing: + $ref: '#/components/schemas/issuing_cardholder_address' + company: + anyOf: + - $ref: '#/components/schemas/issuing_cardholder_company' + description: Additional information about a `company` cardholder. + nullable: true + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + email: + description: The cardholder's email address. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + individual: + anyOf: + - $ref: '#/components/schemas/issuing_cardholder_individual' + description: Additional information about an `individual` cardholder. + nullable: true + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + name: + description: The cardholder's name. This will be printed on cards issued to them. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.cardholder + type: string + phone_number: + description: >- + The cardholder's phone number. This is required for all cardholders + who will be creating EU cards. See the [3D Secure + documentation](https://stripe.com/docs/issuing/3d-secure#when-is-3d-secure-applied) + for more details. + maxLength: 5000 + nullable: true + type: string + preferred_locales: + description: >- + The cardholder’s preferred locales (languages), ordered by + preference. Locales can be `de`, `en`, `es`, `fr`, or `it`. + This changes the language of the [3D Secure flow](https://stripe.com/docs/issuing/3d-secure) and one-time password messages sent to the cardholder. + items: + enum: + - de + - en + - es + - fr + - it + type: string + nullable: true + type: array + requirements: + $ref: '#/components/schemas/issuing_cardholder_requirements' + spending_controls: + anyOf: + - $ref: '#/components/schemas/issuing_cardholder_authorization_controls' + description: >- + Rules that control spending across this cardholder's cards. Refer to + our + [documentation](https://stripe.com/docs/issuing/controls/spending-controls) + for more details. + nullable: true + status: + description: >- + Specifies whether to permit authorizations on this cardholder's + cards. + enum: + - active + - blocked + - inactive + type: string + type: + description: >- + One of `individual` or `company`. See [Choose a cardholder + type](https://stripe.com/docs/issuing/other/choose-cardholder) for + more details. + enum: + - company + - individual + type: string + x-stripeBypassValidation: true + required: + - billing + - created + - id + - livemode + - metadata + - name + - object + - requirements + - status + - type + title: IssuingCardholder + type: object + x-expandableFields: + - billing + - company + - individual + - requirements + - spending_controls + x-resourceId: issuing.cardholder + issuing.dispute: + description: >- + As a [card issuer](https://stripe.com/docs/issuing), you can dispute + transactions that the cardholder does not recognize, suspects to be + fraudulent, or has other issues with. + + + Related guide: [Issuing + disputes](https://stripe.com/docs/issuing/purchases/disputes) + properties: + amount: + description: >- + Disputed amount in the card's currency and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). Usually the + amount of the `transaction`, but can differ (usually because of + currency fluctuation). + type: integer + balance_transactions: + description: List of balance transactions associated with the dispute. + items: + $ref: '#/components/schemas/balance_transaction' + nullable: true + type: array + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: The currency the `transaction` was made in. + format: currency + type: string + evidence: + $ref: '#/components/schemas/issuing_dispute_evidence' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + loss_reason: + description: >- + The enum that describes the dispute loss outcome. If the dispute is + not lost, this field will be absent. New enum values may be added in + the future, so be sure to handle unknown values. + enum: + - cardholder_authentication_issuer_liability + - eci5_token_transaction_with_tavv + - excess_disputes_in_timeframe + - has_not_met_the_minimum_dispute_amount_requirements + - invalid_duplicate_dispute + - invalid_incorrect_amount_dispute + - invalid_no_authorization + - invalid_use_of_disputes + - merchandise_delivered_or_shipped + - merchandise_or_service_as_described + - not_cancelled + - other + - refund_issued + - submitted_beyond_allowable_time_limit + - transaction_3ds_required + - transaction_approved_after_prior_fraud_dispute + - transaction_authorized + - transaction_electronically_read + - transaction_qualifies_for_visa_easy_payment_service + - transaction_unattended + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.dispute + type: string + status: + description: Current status of the dispute. + enum: + - expired + - lost + - submitted + - unsubmitted + - won + type: string + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.transaction' + description: The transaction being disputed. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.transaction' + treasury: + anyOf: + - $ref: '#/components/schemas/issuing_dispute_treasury' + description: >- + [Treasury](https://stripe.com/docs/api/treasury) details related to + this dispute if it was created on a + [FinancialAccount](/docs/api/treasury/financial_accounts + nullable: true + required: + - amount + - created + - currency + - evidence + - id + - livemode + - metadata + - object + - status + - transaction + title: IssuingDispute + type: object + x-expandableFields: + - balance_transactions + - evidence + - transaction + - treasury + x-resourceId: issuing.dispute + issuing.personalization_design: + description: >- + A Personalization Design is a logical grouping of a Physical Bundle, + card logo, and carrier text that represents a product line. + properties: + card_logo: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + The file for the card logo to use with physical bundles that support + card logos. Must have a `purpose` value of `issuing_logo`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + carrier_text: + anyOf: + - $ref: '#/components/schemas/issuing_personalization_design_carrier_text' + description: >- + Hash containing carrier text, for use with physical bundles that + support carrier text. + nullable: true + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + lookup_key: + description: >- + A lookup key used to retrieve personalization designs dynamically + from a static string. This may be up to 200 characters. + maxLength: 5000 + nullable: true + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + name: + description: Friendly display name. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.personalization_design + type: string + physical_bundle: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.physical_bundle' + description: The physical bundle object belonging to this personalization design. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.physical_bundle' + preferences: + $ref: '#/components/schemas/issuing_personalization_design_preferences' + rejection_reasons: + $ref: >- + #/components/schemas/issuing_personalization_design_rejection_reasons + status: + description: Whether this personalization design can be used to create cards. + enum: + - active + - inactive + - rejected + - review + type: string + required: + - created + - id + - livemode + - metadata + - object + - physical_bundle + - preferences + - rejection_reasons + - status + title: IssuingPersonalizationDesign + type: object + x-expandableFields: + - card_logo + - carrier_text + - physical_bundle + - preferences + - rejection_reasons + x-resourceId: issuing.personalization_design + issuing.physical_bundle: + description: >- + A Physical Bundle represents the bundle of physical items - card stock, + carrier letter, and envelope - that is shipped to a cardholder when you + create a physical card. + properties: + features: + $ref: '#/components/schemas/issuing_physical_bundle_features' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + name: + description: Friendly display name. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.physical_bundle + type: string + status: + description: Whether this physical bundle can be used to create cards. + enum: + - active + - inactive + - review + type: string + type: + description: >- + Whether this physical bundle is a standard Stripe offering or + custom-made for you. + enum: + - custom + - standard + type: string + required: + - features + - id + - livemode + - name + - object + - status + - type + title: IssuingPhysicalBundle + type: object + x-expandableFields: + - features + x-resourceId: issuing.physical_bundle + issuing.settlement: + description: >- + When a non-stripe BIN is used, any use of an [issued + card](https://stripe.com/docs/issuing) must be settled directly with the + card network. The net amount owed is represented by an Issuing + `Settlement` object. + properties: + bin: + description: The Bank Identification Number reflecting this settlement record. + maxLength: 5000 + type: string + clearing_date: + description: >- + The date that the transactions are cleared and posted to user's + accounts. + format: unix-time + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + interchange_fees_amount: + description: >- + The total interchange received as reimbursement for the + transactions. + type: integer + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + net_total_amount: + description: The total net amount required to settle with the network. + type: integer + network: + description: >- + The card network for this settlement report. One of ["visa", + "maestro"] + enum: + - maestro + - visa + type: string + network_fees_amount: + description: The total amount of fees owed to the network. + type: integer + network_settlement_identifier: + description: The Settlement Identification Number assigned by the network. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.settlement + type: string + settlement_service: + description: One of `international` or `uk_national_net`. + maxLength: 5000 + type: string + status: + description: The current processing status of this settlement. + enum: + - complete + - pending + type: string + transaction_amount: + description: The total transaction amount reflected in this settlement. + type: integer + transaction_count: + description: The total number of transactions reflected in this settlement. + type: integer + required: + - bin + - clearing_date + - created + - currency + - id + - interchange_fees_amount + - livemode + - metadata + - net_total_amount + - network + - network_fees_amount + - network_settlement_identifier + - object + - settlement_service + - status + - transaction_amount + - transaction_count + title: IssuingSettlement + type: object + x-expandableFields: [] + x-resourceId: issuing.settlement + issuing.token: + description: >- + An issuing token object is created when an issued card is added to a + digital wallet. As a [card issuer](https://stripe.com/docs/issuing), you + can [view and manage these + tokens](https://stripe.com/docs/issuing/controls/token-management) + through Stripe. + properties: + card: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.card' + description: Card associated with this token. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.card' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + device_fingerprint: + description: >- + The hashed ID derived from the device ID from the card network + associated with the token. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + last4: + description: The last four digits of the token. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + network: + description: The token service provider / card network associated with the token. + enum: + - mastercard + - visa + type: string + network_data: + $ref: '#/components/schemas/issuing_network_token_network_data' + network_updated_at: + description: >- + Time at which the token was last updated by the card network. + Measured in seconds since the Unix epoch. + format: unix-time + type: integer + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.token + type: string + status: + description: The usage state of the token. + enum: + - active + - deleted + - requested + - suspended + type: string + wallet_provider: + description: 'The digital wallet for this token, if one was used.' + enum: + - apple_pay + - google_pay + - samsung_pay + type: string + required: + - card + - created + - id + - livemode + - network + - network_updated_at + - object + - status + title: IssuingNetworkToken + type: object + x-expandableFields: + - card + - network_data + x-resourceId: issuing.token + issuing.transaction: + description: >- + Any use of an [issued card](https://stripe.com/docs/issuing) that + results in funds entering or leaving + + your Stripe account, such as a completed purchase or refund, is + represented by an Issuing + + `Transaction` object. + + + Related guide: [Issued card + transactions](https://stripe.com/docs/issuing/purchases/transactions) + properties: + amount: + description: >- + The transaction amount, which will be reflected in your balance. + This amount is in your currency and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + amount_details: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_amount_details' + description: >- + Detailed breakdown of amount components. These amounts are + denominated in `currency` and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + nullable: true + authorization: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.authorization' + description: The `Authorization` object that led to this transaction. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.authorization' + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + ID of the [balance + transaction](https://stripe.com/docs/api/balance_transactions) + associated with this transaction. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + card: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.card' + description: The card used to make this transaction. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.card' + cardholder: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.cardholder' + description: The cardholder to whom this transaction belongs. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.cardholder' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + dispute: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.dispute' + description: 'If you''ve disputed the transaction, the ID of the dispute.' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.dispute' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + merchant_amount: + description: >- + The amount that the merchant will receive, denominated in + `merchant_currency` and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). It will be + different from `amount` if the merchant is taking payment in a + different currency. + type: integer + merchant_currency: + description: The currency with which the merchant is taking payment. + format: currency + type: string + merchant_data: + $ref: '#/components/schemas/issuing_authorization_merchant_data' + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + network_data: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_network_data' + description: >- + Details about the transaction, such as processing dates, set by the + card network. + nullable: true + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - issuing.transaction + type: string + purchase_details: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_purchase_details' + description: >- + Additional purchase information that is optionally provided by the + merchant. + nullable: true + token: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/issuing.token' + description: >- + [Token](https://stripe.com/docs/api/issuing/tokens/object) object + used for this transaction. If a network token was not used for this + transaction, this field will be null. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/issuing.token' + treasury: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_treasury' + description: >- + [Treasury](https://stripe.com/docs/api/treasury) details related to + this transaction if it was created on a + [FinancialAccount](/docs/api/treasury/financial_accounts + nullable: true + type: + description: The nature of the transaction. + enum: + - capture + - refund + type: string + x-stripeBypassValidation: true + wallet: + description: >- + The digital wallet used for this transaction. One of `apple_pay`, + `google_pay`, or `samsung_pay`. + enum: + - apple_pay + - google_pay + - samsung_pay + nullable: true + type: string + required: + - amount + - card + - created + - currency + - id + - livemode + - merchant_amount + - merchant_currency + - merchant_data + - metadata + - object + - type + title: IssuingTransaction + type: object + x-expandableFields: + - amount_details + - authorization + - balance_transaction + - card + - cardholder + - dispute + - merchant_data + - network_data + - purchase_details + - token + - treasury + x-resourceId: issuing.transaction + issuing_authorization_amount_details: + description: '' + properties: + atm_fee: + description: The fee charged by the ATM for the cash withdrawal. + nullable: true + type: integer + cashback_amount: + description: The amount of cash requested by the cardholder. + nullable: true + type: integer + title: IssuingAuthorizationAmountDetails + type: object + x-expandableFields: [] + issuing_authorization_authentication_exemption: + description: '' + properties: + claimed_by: + description: >- + The entity that requested the exemption, either the acquiring + merchant or the Issuing user. + enum: + - acquirer + - issuer + type: string + type: + description: The specific exemption claimed for this authorization. + enum: + - low_value_transaction + - transaction_risk_analysis + - unknown + type: string + x-stripeBypassValidation: true + required: + - claimed_by + - type + title: IssuingAuthorizationAuthenticationExemption + type: object + x-expandableFields: [] + issuing_authorization_fleet_cardholder_prompt_data: + description: '' + properties: + alphanumeric_id: + description: >- + [Deprecated] An alphanumeric ID, though typical point of sales only + support numeric entry. The card program can be configured to prompt + for a vehicle ID, driver ID, or generic ID. + maxLength: 5000 + nullable: true + type: string + driver_id: + description: Driver ID. + maxLength: 5000 + nullable: true + type: string + odometer: + description: Odometer reading. + nullable: true + type: integer + unspecified_id: + description: >- + An alphanumeric ID. This field is used when a vehicle ID, driver ID, + or generic ID is entered by the cardholder, but the merchant or card + network did not specify the prompt type. + maxLength: 5000 + nullable: true + type: string + user_id: + description: User ID. + maxLength: 5000 + nullable: true + type: string + vehicle_number: + description: Vehicle number. + maxLength: 5000 + nullable: true + type: string + title: IssuingAuthorizationFleetCardholderPromptData + type: object + x-expandableFields: [] + issuing_authorization_fleet_data: + description: '' + properties: + cardholder_prompt_data: + anyOf: + - $ref: >- + #/components/schemas/issuing_authorization_fleet_cardholder_prompt_data + description: >- + Answers to prompts presented to the cardholder at the point of sale. + Prompted fields vary depending on the configuration of your physical + fleet cards. Typical points of sale support only numeric entry. + nullable: true + purchase_type: + description: The type of purchase. + enum: + - fuel_and_non_fuel_purchase + - fuel_purchase + - non_fuel_purchase + nullable: true + type: string + reported_breakdown: + anyOf: + - $ref: >- + #/components/schemas/issuing_authorization_fleet_reported_breakdown + description: >- + More information about the total amount. Typically this information + is received from the merchant after the authorization has been + approved and the fuel dispensed. This information is not guaranteed + to be accurate as some merchants may provide unreliable data. + nullable: true + service_type: + description: The type of fuel service. + enum: + - full_service + - non_fuel_transaction + - self_service + nullable: true + type: string + title: IssuingAuthorizationFleetData + type: object + x-expandableFields: + - cardholder_prompt_data + - reported_breakdown + issuing_authorization_fleet_fuel_price_data: + description: '' + properties: + gross_amount_decimal: + description: >- + Gross fuel amount that should equal Fuel Quantity multiplied by Fuel + Unit Cost, inclusive of taxes. + format: decimal + nullable: true + type: string + title: IssuingAuthorizationFleetFuelPriceData + type: object + x-expandableFields: [] + issuing_authorization_fleet_non_fuel_price_data: + description: '' + properties: + gross_amount_decimal: + description: >- + Gross non-fuel amount that should equal the sum of the line items, + inclusive of taxes. + format: decimal + nullable: true + type: string + title: IssuingAuthorizationFleetNonFuelPriceData + type: object + x-expandableFields: [] + issuing_authorization_fleet_reported_breakdown: + description: '' + properties: + fuel: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_fleet_fuel_price_data' + description: Breakdown of fuel portion of the purchase. + nullable: true + non_fuel: + anyOf: + - $ref: >- + #/components/schemas/issuing_authorization_fleet_non_fuel_price_data + description: Breakdown of non-fuel portion of the purchase. + nullable: true + tax: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_fleet_tax_data' + description: Information about tax included in this transaction. + nullable: true + title: IssuingAuthorizationFleetReportedBreakdown + type: object + x-expandableFields: + - fuel + - non_fuel + - tax + issuing_authorization_fleet_tax_data: + description: '' + properties: + local_amount_decimal: + description: >- + Amount of state or provincial Sales Tax included in the transaction + amount. `null` if not reported by merchant or not subject to tax. + format: decimal + nullable: true + type: string + national_amount_decimal: + description: >- + Amount of national Sales Tax or VAT included in the transaction + amount. `null` if not reported by merchant or not subject to tax. + format: decimal + nullable: true + type: string + title: IssuingAuthorizationFleetTaxData + type: object + x-expandableFields: [] + issuing_authorization_fraud_challenge: + description: '' + properties: + channel: + description: >- + The method by which the fraud challenge was delivered to the + cardholder. + enum: + - sms + type: string + status: + description: The status of the fraud challenge. + enum: + - expired + - pending + - rejected + - undeliverable + - verified + type: string + undeliverable_reason: + description: 'If the challenge is not deliverable, the reason why.' + enum: + - no_phone_number + - unsupported_phone_number + nullable: true + type: string + required: + - channel + - status + title: IssuingAuthorizationFraudChallenge + type: object + x-expandableFields: [] + issuing_authorization_fuel_data: + description: '' + properties: + industry_product_code: + description: >- + [Conexxus Payment System Product + Code](https://www.conexxus.org/conexxus-payment-system-product-codes) + identifying the primary fuel product purchased. + maxLength: 5000 + nullable: true + type: string + quantity_decimal: + description: >- + The quantity of `unit`s of fuel that was dispensed, represented as a + decimal string with at most 12 decimal places. + format: decimal + nullable: true + type: string + type: + description: The type of fuel that was purchased. + enum: + - diesel + - other + - unleaded_plus + - unleaded_regular + - unleaded_super + nullable: true + type: string + unit: + description: The units for `quantity_decimal`. + enum: + - charging_minute + - imperial_gallon + - kilogram + - kilowatt_hour + - liter + - other + - pound + - us_gallon + nullable: true + type: string + unit_cost_decimal: + description: >- + The cost in cents per each unit of fuel, represented as a decimal + string with at most 12 decimal places. + format: decimal + nullable: true + type: string + title: IssuingAuthorizationFuelData + type: object + x-expandableFields: [] + issuing_authorization_merchant_data: + description: '' + properties: + category: + description: >- + A categorization of the seller's type of business. See our [merchant + categories + guide](https://stripe.com/docs/issuing/merchant-categories) for a + list of possible values. + maxLength: 5000 + type: string + category_code: + description: The merchant category code for the seller’s business + maxLength: 5000 + type: string + city: + description: City where the seller is located + maxLength: 5000 + nullable: true + type: string + country: + description: Country where the seller is located + maxLength: 5000 + nullable: true + type: string + name: + description: Name of the seller + maxLength: 5000 + nullable: true + type: string + network_id: + description: >- + Identifier assigned to the seller by the card network. Different + card networks may assign different network_id fields to the same + merchant. + maxLength: 5000 + type: string + postal_code: + description: Postal code where the seller is located + maxLength: 5000 + nullable: true + type: string + state: + description: State where the seller is located + maxLength: 5000 + nullable: true + type: string + tax_id: + description: >- + The seller's tax identification number. Currently populated for + French merchants only. + maxLength: 5000 + nullable: true + type: string + terminal_id: + description: An ID assigned by the seller to the location of the sale. + maxLength: 5000 + nullable: true + type: string + url: + description: URL provided by the merchant on a 3DS request + maxLength: 5000 + nullable: true + type: string + required: + - category + - category_code + - network_id + title: IssuingAuthorizationMerchantData + type: object + x-expandableFields: [] + issuing_authorization_network_data: + description: '' + properties: + acquiring_institution_id: + description: >- + Identifier assigned to the acquirer by the card network. Sometimes + this value is not provided by the network; in this case, the value + will be `null`. + maxLength: 5000 + nullable: true + type: string + system_trace_audit_number: + description: >- + The System Trace Audit Number (STAN) is a 6-digit identifier + assigned by the acquirer. Prefer `network_data.transaction_id` if + present, unless you have special requirements. + maxLength: 5000 + nullable: true + type: string + transaction_id: + description: >- + Unique identifier for the authorization assigned by the card network + used to match subsequent messages, disputes, and transactions. + maxLength: 5000 + nullable: true + type: string + title: IssuingAuthorizationNetworkData + type: object + x-expandableFields: [] + issuing_authorization_pending_request: + description: '' + properties: + amount: + description: >- + The additional amount Stripe will hold if the authorization is + approved, in the card's + [currency](https://stripe.com/docs/api#issuing_authorization_object-pending-request-currency) + and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + amount_details: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_amount_details' + description: >- + Detailed breakdown of amount components. These amounts are + denominated in `currency` and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + nullable: true + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + is_amount_controllable: + description: >- + If set `true`, you may provide + [amount](https://stripe.com/docs/api/issuing/authorizations/approve#approve_issuing_authorization-amount) + to control how much to hold for the authorization. + type: boolean + merchant_amount: + description: >- + The amount the merchant is requesting to be authorized in the + `merchant_currency`. The amount is in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + merchant_currency: + description: The local currency the merchant is requesting to authorize. + format: currency + type: string + network_risk_score: + description: >- + The card network's estimate of the likelihood that an authorization + is fraudulent. Takes on values between 1 and 99. + nullable: true + type: integer + required: + - amount + - currency + - is_amount_controllable + - merchant_amount + - merchant_currency + title: IssuingAuthorizationPendingRequest + type: object + x-expandableFields: + - amount_details + issuing_authorization_request: + description: '' + properties: + amount: + description: >- + The `pending_request.amount` at the time of the request, presented + in your card's currency and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). Stripe held + this amount from your account to fund the authorization if the + request was approved. + type: integer + amount_details: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_amount_details' + description: >- + Detailed breakdown of amount components. These amounts are + denominated in `currency` and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + nullable: true + approved: + description: Whether this request was approved. + type: boolean + authorization_code: + description: >- + A code created by Stripe which is shared with the merchant to + validate the authorization. This field will be populated if the + authorization message was approved. The code typically starts with + the letter "S", followed by a six-digit number. For example, + "S498162". Please note that the code is not guaranteed to be unique + across authorizations. + maxLength: 5000 + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + merchant_amount: + description: >- + The `pending_request.merchant_amount` at the time of the request, + presented in the `merchant_currency` and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + merchant_currency: + description: >- + The currency that was collected by the merchant and presented to the + cardholder for the authorization. Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + network_risk_score: + description: >- + The card network's estimate of the likelihood that an authorization + is fraudulent. Takes on values between 1 and 99. + nullable: true + type: integer + reason: + description: >- + When an authorization is approved or declined by you or by Stripe, + this field provides additional detail on the reason for the outcome. + enum: + - account_disabled + - card_active + - card_canceled + - card_expired + - card_inactive + - cardholder_blocked + - cardholder_inactive + - cardholder_verification_required + - insecure_authorization_method + - insufficient_funds + - network_fallback + - not_allowed + - pin_blocked + - spending_controls + - suspected_fraud + - verification_failed + - webhook_approved + - webhook_declined + - webhook_error + - webhook_timeout + type: string + x-stripeBypassValidation: true + reason_message: + description: >- + If the `request_history.reason` is `webhook_error` because the + direct webhook response is invalid (for example, parsing errors or + missing parameters), we surface a more detailed error message via + this field. + maxLength: 5000 + nullable: true + type: string + requested_at: + description: >- + Time when the card network received an authorization request from + the acquirer in UTC. Referred to by networks as transmission time. + format: unix-time + nullable: true + type: integer + required: + - amount + - approved + - created + - currency + - merchant_amount + - merchant_currency + - reason + title: IssuingAuthorizationRequest + type: object + x-expandableFields: + - amount_details + issuing_authorization_three_d_secure: + description: '' + properties: + result: + description: The outcome of the 3D Secure authentication request. + enum: + - attempt_acknowledged + - authenticated + - failed + - required + type: string + x-stripeBypassValidation: true + required: + - result + title: IssuingAuthorizationThreeDSecure + type: object + x-expandableFields: [] + issuing_authorization_treasury: + description: '' + properties: + received_credits: + description: >- + The array of + [ReceivedCredits](https://stripe.com/docs/api/treasury/received_credits) + associated with this authorization + items: + maxLength: 5000 + type: string + type: array + received_debits: + description: >- + The array of + [ReceivedDebits](https://stripe.com/docs/api/treasury/received_debits) + associated with this authorization + items: + maxLength: 5000 + type: string + type: array + transaction: + description: >- + The Treasury + [Transaction](https://stripe.com/docs/api/treasury/transactions) + associated with this authorization + maxLength: 5000 + nullable: true + type: string + required: + - received_credits + - received_debits + title: IssuingAuthorizationTreasury + type: object + x-expandableFields: [] + issuing_authorization_verification_data: + description: '' + properties: + address_line1_check: + description: >- + Whether the cardholder provided an address first line and if it + matched the cardholder’s `billing.address.line1`. + enum: + - match + - mismatch + - not_provided + type: string + address_postal_code_check: + description: >- + Whether the cardholder provided a postal code and if it matched the + cardholder’s `billing.address.postal_code`. + enum: + - match + - mismatch + - not_provided + type: string + authentication_exemption: + anyOf: + - $ref: >- + #/components/schemas/issuing_authorization_authentication_exemption + description: The exemption applied to this authorization. + nullable: true + cvc_check: + description: >- + Whether the cardholder provided a CVC and if it matched Stripe’s + record. + enum: + - match + - mismatch + - not_provided + type: string + expiry_check: + description: >- + Whether the cardholder provided an expiry date and if it matched + Stripe’s record. + enum: + - match + - mismatch + - not_provided + type: string + postal_code: + description: >- + The postal code submitted as part of the authorization used for + postal code verification. + maxLength: 5000 + nullable: true + type: string + three_d_secure: + anyOf: + - $ref: '#/components/schemas/issuing_authorization_three_d_secure' + description: 3D Secure details. + nullable: true + required: + - address_line1_check + - address_postal_code_check + - cvc_check + - expiry_check + title: IssuingAuthorizationVerificationData + type: object + x-expandableFields: + - authentication_exemption + - three_d_secure + issuing_card_apple_pay: + description: '' + properties: + eligible: + description: Apple Pay Eligibility + type: boolean + ineligible_reason: + description: Reason the card is ineligible for Apple Pay + enum: + - missing_agreement + - missing_cardholder_contact + - unsupported_region + nullable: true + type: string + required: + - eligible + title: IssuingCardApplePay + type: object + x-expandableFields: [] + issuing_card_authorization_controls: + description: '' + properties: + allowed_categories: + description: >- + Array of strings containing + [categories](https://stripe.com/docs/api#issuing_authorization_object-merchant_data-category) + of authorizations to allow. All other categories will be blocked. + Cannot be set with `blocked_categories`. + items: + enum: + - ac_refrigeration_repair + - accounting_bookkeeping_services + - advertising_services + - agricultural_cooperative + - airlines_air_carriers + - airports_flying_fields + - ambulance_services + - amusement_parks_carnivals + - antique_reproductions + - antique_shops + - aquariums + - architectural_surveying_services + - art_dealers_and_galleries + - artists_supply_and_craft_shops + - auto_and_home_supply_stores + - auto_body_repair_shops + - auto_paint_shops + - auto_service_shops + - automated_cash_disburse + - automated_fuel_dispensers + - automobile_associations + - automotive_parts_and_accessories_stores + - automotive_tire_stores + - bail_and_bond_payments + - bakeries + - bands_orchestras + - barber_and_beauty_shops + - betting_casino_gambling + - bicycle_shops + - billiard_pool_establishments + - boat_dealers + - boat_rentals_and_leases + - book_stores + - books_periodicals_and_newspapers + - bowling_alleys + - bus_lines + - business_secretarial_schools + - buying_shopping_services + - cable_satellite_and_other_pay_television_and_radio + - camera_and_photographic_supply_stores + - candy_nut_and_confectionery_stores + - car_and_truck_dealers_new_used + - car_and_truck_dealers_used_only + - car_rental_agencies + - car_washes + - carpentry_services + - carpet_upholstery_cleaning + - caterers + - charitable_and_social_service_organizations_fundraising + - chemicals_and_allied_products + - child_care_services + - childrens_and_infants_wear_stores + - chiropodists_podiatrists + - chiropractors + - cigar_stores_and_stands + - civic_social_fraternal_associations + - cleaning_and_maintenance + - clothing_rental + - colleges_universities + - commercial_equipment + - commercial_footwear + - commercial_photography_art_and_graphics + - commuter_transport_and_ferries + - computer_network_services + - computer_programming + - computer_repair + - computer_software_stores + - computers_peripherals_and_software + - concrete_work_services + - construction_materials + - consulting_public_relations + - correspondence_schools + - cosmetic_stores + - counseling_services + - country_clubs + - courier_services + - court_costs + - credit_reporting_agencies + - cruise_lines + - dairy_products_stores + - dance_hall_studios_schools + - dating_escort_services + - dentists_orthodontists + - department_stores + - detective_agencies + - digital_goods_applications + - digital_goods_games + - digital_goods_large_volume + - digital_goods_media + - direct_marketing_catalog_merchant + - direct_marketing_combination_catalog_and_retail_merchant + - direct_marketing_inbound_telemarketing + - direct_marketing_insurance_services + - direct_marketing_other + - direct_marketing_outbound_telemarketing + - direct_marketing_subscription + - direct_marketing_travel + - discount_stores + - doctors + - door_to_door_sales + - drapery_window_covering_and_upholstery_stores + - drinking_places + - drug_stores_and_pharmacies + - drugs_drug_proprietaries_and_druggist_sundries + - dry_cleaners + - durable_goods + - duty_free_stores + - eating_places_restaurants + - educational_services + - electric_razor_stores + - electric_vehicle_charging + - electrical_parts_and_equipment + - electrical_services + - electronics_repair_shops + - electronics_stores + - elementary_secondary_schools + - emergency_services_gcas_visa_use_only + - employment_temp_agencies + - equipment_rental + - exterminating_services + - family_clothing_stores + - fast_food_restaurants + - financial_institutions + - fines_government_administrative_entities + - fireplace_fireplace_screens_and_accessories_stores + - floor_covering_stores + - florists + - florists_supplies_nursery_stock_and_flowers + - freezer_and_locker_meat_provisioners + - fuel_dealers_non_automotive + - funeral_services_crematories + - >- + furniture_home_furnishings_and_equipment_stores_except_appliances + - furniture_repair_refinishing + - furriers_and_fur_shops + - general_services + - gift_card_novelty_and_souvenir_shops + - glass_paint_and_wallpaper_stores + - glassware_crystal_stores + - golf_courses_public + - government_licensed_horse_dog_racing_us_region_only + - >- + government_licensed_online_casions_online_gambling_us_region_only + - government_owned_lotteries_non_us_region + - government_owned_lotteries_us_region_only + - government_services + - grocery_stores_supermarkets + - hardware_equipment_and_supplies + - hardware_stores + - health_and_beauty_spas + - hearing_aids_sales_and_supplies + - heating_plumbing_a_c + - hobby_toy_and_game_shops + - home_supply_warehouse_stores + - hospitals + - hotels_motels_and_resorts + - household_appliance_stores + - industrial_supplies + - information_retrieval_services + - insurance_default + - insurance_underwriting_premiums + - intra_company_purchases + - jewelry_stores_watches_clocks_and_silverware_stores + - landscaping_services + - laundries + - laundry_cleaning_services + - legal_services_attorneys + - luggage_and_leather_goods_stores + - lumber_building_materials_stores + - manual_cash_disburse + - marinas_service_and_supplies + - marketplaces + - masonry_stonework_and_plaster + - massage_parlors + - medical_and_dental_labs + - medical_dental_ophthalmic_and_hospital_equipment_and_supplies + - medical_services + - membership_organizations + - mens_and_boys_clothing_and_accessories_stores + - mens_womens_clothing_stores + - metal_service_centers + - miscellaneous + - miscellaneous_apparel_and_accessory_shops + - miscellaneous_auto_dealers + - miscellaneous_business_services + - miscellaneous_food_stores + - miscellaneous_general_merchandise + - miscellaneous_general_services + - miscellaneous_home_furnishing_specialty_stores + - miscellaneous_publishing_and_printing + - miscellaneous_recreation_services + - miscellaneous_repair_shops + - miscellaneous_specialty_retail + - mobile_home_dealers + - motion_picture_theaters + - motor_freight_carriers_and_trucking + - motor_homes_dealers + - motor_vehicle_supplies_and_new_parts + - motorcycle_shops_and_dealers + - motorcycle_shops_dealers + - music_stores_musical_instruments_pianos_and_sheet_music + - news_dealers_and_newsstands + - non_fi_money_orders + - non_fi_stored_value_card_purchase_load + - nondurable_goods + - nurseries_lawn_and_garden_supply_stores + - nursing_personal_care + - office_and_commercial_furniture + - opticians_eyeglasses + - optometrists_ophthalmologist + - orthopedic_goods_prosthetic_devices + - osteopaths + - package_stores_beer_wine_and_liquor + - paints_varnishes_and_supplies + - parking_lots_garages + - passenger_railways + - pawn_shops + - pet_shops_pet_food_and_supplies + - petroleum_and_petroleum_products + - photo_developing + - photographic_photocopy_microfilm_equipment_and_supplies + - photographic_studios + - picture_video_production + - piece_goods_notions_and_other_dry_goods + - plumbing_heating_equipment_and_supplies + - political_organizations + - postal_services_government_only + - precious_stones_and_metals_watches_and_jewelry + - professional_services + - public_warehousing_and_storage + - quick_copy_repro_and_blueprint + - railroads + - real_estate_agents_and_managers_rentals + - record_stores + - recreational_vehicle_rentals + - religious_goods_stores + - religious_organizations + - roofing_siding_sheet_metal + - secretarial_support_services + - security_brokers_dealers + - service_stations + - sewing_needlework_fabric_and_piece_goods_stores + - shoe_repair_hat_cleaning + - shoe_stores + - small_appliance_repair + - snowmobile_dealers + - special_trade_services + - specialty_cleaning + - sporting_goods_stores + - sporting_recreation_camps + - sports_and_riding_apparel_stores + - sports_clubs_fields + - stamp_and_coin_stores + - stationary_office_supplies_printing_and_writing_paper + - stationery_stores_office_and_school_supply_stores + - swimming_pools_sales + - t_ui_travel_germany + - tailors_alterations + - tax_payments_government_agencies + - tax_preparation_services + - taxicabs_limousines + - telecommunication_equipment_and_telephone_sales + - telecommunication_services + - telegraph_services + - tent_and_awning_shops + - testing_laboratories + - theatrical_ticket_agencies + - timeshares + - tire_retreading_and_repair + - tolls_bridge_fees + - tourist_attractions_and_exhibits + - towing_services + - trailer_parks_campgrounds + - transportation_services + - travel_agencies_tour_operators + - truck_stop_iteration + - truck_utility_trailer_rentals + - typesetting_plate_making_and_related_services + - typewriter_stores + - u_s_federal_government_agencies_or_departments + - uniforms_commercial_clothing + - used_merchandise_and_secondhand_stores + - utilities + - variety_stores + - veterinary_services + - video_amusement_game_supplies + - video_game_arcades + - video_tape_rental_stores + - vocational_trade_schools + - watch_jewelry_repair + - welding_repair + - wholesale_clubs + - wig_and_toupee_stores + - wires_money_orders + - womens_accessory_and_specialty_shops + - womens_ready_to_wear_stores + - wrecking_and_salvage_yards + type: string + nullable: true + type: array + allowed_merchant_countries: + description: >- + Array of strings containing representing countries from which + authorizations will be allowed. Authorizations from merchants in all + other countries will be declined. Country codes should be ISO 3166 + alpha-2 country codes (e.g. `US`). Cannot be set with + `blocked_merchant_countries`. Provide an empty value to unset this + control. + items: + maxLength: 5000 + type: string + nullable: true + type: array + blocked_categories: + description: >- + Array of strings containing + [categories](https://stripe.com/docs/api#issuing_authorization_object-merchant_data-category) + of authorizations to decline. All other categories will be allowed. + Cannot be set with `allowed_categories`. + items: + enum: + - ac_refrigeration_repair + - accounting_bookkeeping_services + - advertising_services + - agricultural_cooperative + - airlines_air_carriers + - airports_flying_fields + - ambulance_services + - amusement_parks_carnivals + - antique_reproductions + - antique_shops + - aquariums + - architectural_surveying_services + - art_dealers_and_galleries + - artists_supply_and_craft_shops + - auto_and_home_supply_stores + - auto_body_repair_shops + - auto_paint_shops + - auto_service_shops + - automated_cash_disburse + - automated_fuel_dispensers + - automobile_associations + - automotive_parts_and_accessories_stores + - automotive_tire_stores + - bail_and_bond_payments + - bakeries + - bands_orchestras + - barber_and_beauty_shops + - betting_casino_gambling + - bicycle_shops + - billiard_pool_establishments + - boat_dealers + - boat_rentals_and_leases + - book_stores + - books_periodicals_and_newspapers + - bowling_alleys + - bus_lines + - business_secretarial_schools + - buying_shopping_services + - cable_satellite_and_other_pay_television_and_radio + - camera_and_photographic_supply_stores + - candy_nut_and_confectionery_stores + - car_and_truck_dealers_new_used + - car_and_truck_dealers_used_only + - car_rental_agencies + - car_washes + - carpentry_services + - carpet_upholstery_cleaning + - caterers + - charitable_and_social_service_organizations_fundraising + - chemicals_and_allied_products + - child_care_services + - childrens_and_infants_wear_stores + - chiropodists_podiatrists + - chiropractors + - cigar_stores_and_stands + - civic_social_fraternal_associations + - cleaning_and_maintenance + - clothing_rental + - colleges_universities + - commercial_equipment + - commercial_footwear + - commercial_photography_art_and_graphics + - commuter_transport_and_ferries + - computer_network_services + - computer_programming + - computer_repair + - computer_software_stores + - computers_peripherals_and_software + - concrete_work_services + - construction_materials + - consulting_public_relations + - correspondence_schools + - cosmetic_stores + - counseling_services + - country_clubs + - courier_services + - court_costs + - credit_reporting_agencies + - cruise_lines + - dairy_products_stores + - dance_hall_studios_schools + - dating_escort_services + - dentists_orthodontists + - department_stores + - detective_agencies + - digital_goods_applications + - digital_goods_games + - digital_goods_large_volume + - digital_goods_media + - direct_marketing_catalog_merchant + - direct_marketing_combination_catalog_and_retail_merchant + - direct_marketing_inbound_telemarketing + - direct_marketing_insurance_services + - direct_marketing_other + - direct_marketing_outbound_telemarketing + - direct_marketing_subscription + - direct_marketing_travel + - discount_stores + - doctors + - door_to_door_sales + - drapery_window_covering_and_upholstery_stores + - drinking_places + - drug_stores_and_pharmacies + - drugs_drug_proprietaries_and_druggist_sundries + - dry_cleaners + - durable_goods + - duty_free_stores + - eating_places_restaurants + - educational_services + - electric_razor_stores + - electric_vehicle_charging + - electrical_parts_and_equipment + - electrical_services + - electronics_repair_shops + - electronics_stores + - elementary_secondary_schools + - emergency_services_gcas_visa_use_only + - employment_temp_agencies + - equipment_rental + - exterminating_services + - family_clothing_stores + - fast_food_restaurants + - financial_institutions + - fines_government_administrative_entities + - fireplace_fireplace_screens_and_accessories_stores + - floor_covering_stores + - florists + - florists_supplies_nursery_stock_and_flowers + - freezer_and_locker_meat_provisioners + - fuel_dealers_non_automotive + - funeral_services_crematories + - >- + furniture_home_furnishings_and_equipment_stores_except_appliances + - furniture_repair_refinishing + - furriers_and_fur_shops + - general_services + - gift_card_novelty_and_souvenir_shops + - glass_paint_and_wallpaper_stores + - glassware_crystal_stores + - golf_courses_public + - government_licensed_horse_dog_racing_us_region_only + - >- + government_licensed_online_casions_online_gambling_us_region_only + - government_owned_lotteries_non_us_region + - government_owned_lotteries_us_region_only + - government_services + - grocery_stores_supermarkets + - hardware_equipment_and_supplies + - hardware_stores + - health_and_beauty_spas + - hearing_aids_sales_and_supplies + - heating_plumbing_a_c + - hobby_toy_and_game_shops + - home_supply_warehouse_stores + - hospitals + - hotels_motels_and_resorts + - household_appliance_stores + - industrial_supplies + - information_retrieval_services + - insurance_default + - insurance_underwriting_premiums + - intra_company_purchases + - jewelry_stores_watches_clocks_and_silverware_stores + - landscaping_services + - laundries + - laundry_cleaning_services + - legal_services_attorneys + - luggage_and_leather_goods_stores + - lumber_building_materials_stores + - manual_cash_disburse + - marinas_service_and_supplies + - marketplaces + - masonry_stonework_and_plaster + - massage_parlors + - medical_and_dental_labs + - medical_dental_ophthalmic_and_hospital_equipment_and_supplies + - medical_services + - membership_organizations + - mens_and_boys_clothing_and_accessories_stores + - mens_womens_clothing_stores + - metal_service_centers + - miscellaneous + - miscellaneous_apparel_and_accessory_shops + - miscellaneous_auto_dealers + - miscellaneous_business_services + - miscellaneous_food_stores + - miscellaneous_general_merchandise + - miscellaneous_general_services + - miscellaneous_home_furnishing_specialty_stores + - miscellaneous_publishing_and_printing + - miscellaneous_recreation_services + - miscellaneous_repair_shops + - miscellaneous_specialty_retail + - mobile_home_dealers + - motion_picture_theaters + - motor_freight_carriers_and_trucking + - motor_homes_dealers + - motor_vehicle_supplies_and_new_parts + - motorcycle_shops_and_dealers + - motorcycle_shops_dealers + - music_stores_musical_instruments_pianos_and_sheet_music + - news_dealers_and_newsstands + - non_fi_money_orders + - non_fi_stored_value_card_purchase_load + - nondurable_goods + - nurseries_lawn_and_garden_supply_stores + - nursing_personal_care + - office_and_commercial_furniture + - opticians_eyeglasses + - optometrists_ophthalmologist + - orthopedic_goods_prosthetic_devices + - osteopaths + - package_stores_beer_wine_and_liquor + - paints_varnishes_and_supplies + - parking_lots_garages + - passenger_railways + - pawn_shops + - pet_shops_pet_food_and_supplies + - petroleum_and_petroleum_products + - photo_developing + - photographic_photocopy_microfilm_equipment_and_supplies + - photographic_studios + - picture_video_production + - piece_goods_notions_and_other_dry_goods + - plumbing_heating_equipment_and_supplies + - political_organizations + - postal_services_government_only + - precious_stones_and_metals_watches_and_jewelry + - professional_services + - public_warehousing_and_storage + - quick_copy_repro_and_blueprint + - railroads + - real_estate_agents_and_managers_rentals + - record_stores + - recreational_vehicle_rentals + - religious_goods_stores + - religious_organizations + - roofing_siding_sheet_metal + - secretarial_support_services + - security_brokers_dealers + - service_stations + - sewing_needlework_fabric_and_piece_goods_stores + - shoe_repair_hat_cleaning + - shoe_stores + - small_appliance_repair + - snowmobile_dealers + - special_trade_services + - specialty_cleaning + - sporting_goods_stores + - sporting_recreation_camps + - sports_and_riding_apparel_stores + - sports_clubs_fields + - stamp_and_coin_stores + - stationary_office_supplies_printing_and_writing_paper + - stationery_stores_office_and_school_supply_stores + - swimming_pools_sales + - t_ui_travel_germany + - tailors_alterations + - tax_payments_government_agencies + - tax_preparation_services + - taxicabs_limousines + - telecommunication_equipment_and_telephone_sales + - telecommunication_services + - telegraph_services + - tent_and_awning_shops + - testing_laboratories + - theatrical_ticket_agencies + - timeshares + - tire_retreading_and_repair + - tolls_bridge_fees + - tourist_attractions_and_exhibits + - towing_services + - trailer_parks_campgrounds + - transportation_services + - travel_agencies_tour_operators + - truck_stop_iteration + - truck_utility_trailer_rentals + - typesetting_plate_making_and_related_services + - typewriter_stores + - u_s_federal_government_agencies_or_departments + - uniforms_commercial_clothing + - used_merchandise_and_secondhand_stores + - utilities + - variety_stores + - veterinary_services + - video_amusement_game_supplies + - video_game_arcades + - video_tape_rental_stores + - vocational_trade_schools + - watch_jewelry_repair + - welding_repair + - wholesale_clubs + - wig_and_toupee_stores + - wires_money_orders + - womens_accessory_and_specialty_shops + - womens_ready_to_wear_stores + - wrecking_and_salvage_yards + type: string + nullable: true + type: array + blocked_merchant_countries: + description: >- + Array of strings containing representing countries from which + authorizations will be declined. Country codes should be ISO 3166 + alpha-2 country codes (e.g. `US`). Cannot be set with + `allowed_merchant_countries`. Provide an empty value to unset this + control. + items: + maxLength: 5000 + type: string + nullable: true + type: array + spending_limits: + description: >- + Limit spending with amount-based rules that apply across any cards + this card replaced (i.e., its `replacement_for` card and _that_ + card's `replacement_for` card, up the chain). + items: + $ref: '#/components/schemas/issuing_card_spending_limit' + nullable: true + type: array + spending_limits_currency: + description: >- + Currency of the amounts within `spending_limits`. Always the same as + the currency of the card. + format: currency + nullable: true + type: string + title: IssuingCardAuthorizationControls + type: object + x-expandableFields: + - spending_limits + issuing_card_google_pay: + description: '' + properties: + eligible: + description: Google Pay Eligibility + type: boolean + ineligible_reason: + description: Reason the card is ineligible for Google Pay + enum: + - missing_agreement + - missing_cardholder_contact + - unsupported_region + nullable: true + type: string + required: + - eligible + title: IssuingCardGooglePay + type: object + x-expandableFields: [] + issuing_card_shipping: + description: '' + properties: + address: + $ref: '#/components/schemas/address' + address_validation: + anyOf: + - $ref: '#/components/schemas/issuing_card_shipping_address_validation' + description: Address validation details for the shipment. + nullable: true + carrier: + description: The delivery company that shipped a card. + enum: + - dhl + - fedex + - royal_mail + - usps + nullable: true + type: string + customs: + anyOf: + - $ref: '#/components/schemas/issuing_card_shipping_customs' + description: Additional information that may be required for clearing customs. + nullable: true + eta: + description: >- + A unix timestamp representing a best estimate of when the card will + be delivered. + format: unix-time + nullable: true + type: integer + name: + description: Recipient name. + maxLength: 5000 + type: string + phone_number: + description: >- + The phone number of the receiver of the shipment. Our courier + partners will use this number to contact you in the event of card + delivery issues. For individual shipments to the EU/UK, if this + field is empty, we will provide them with the phone number provided + when the cardholder was initially created. + maxLength: 5000 + nullable: true + type: string + require_signature: + description: >- + Whether a signature is required for card delivery. This feature is + only supported for US users. Standard shipping service does not + support signature on delivery. The default value for standard + shipping service is false and for express and priority services is + true. + nullable: true + type: boolean + service: + description: 'Shipment service, such as `standard` or `express`.' + enum: + - express + - priority + - standard + type: string + x-stripeBypassValidation: true + status: + description: The delivery status of the card. + enum: + - canceled + - delivered + - failure + - pending + - returned + - shipped + - submitted + nullable: true + type: string + tracking_number: + description: A tracking number for a card shipment. + maxLength: 5000 + nullable: true + type: string + tracking_url: + description: >- + A link to the shipping carrier's site where you can view detailed + information about a card shipment. + maxLength: 5000 + nullable: true + type: string + type: + description: Packaging options. + enum: + - bulk + - individual + type: string + required: + - address + - name + - service + - type + title: IssuingCardShipping + type: object + x-expandableFields: + - address + - address_validation + - customs + issuing_card_shipping_address_validation: + description: '' + properties: + mode: + description: The address validation capabilities to use. + enum: + - disabled + - normalization_only + - validation_and_normalization + type: string + normalized_address: + anyOf: + - $ref: '#/components/schemas/address' + description: The normalized shipping address. + nullable: true + result: + description: The validation result for the shipping address. + enum: + - indeterminate + - likely_deliverable + - likely_undeliverable + nullable: true + type: string + required: + - mode + title: IssuingCardShippingAddressValidation + type: object + x-expandableFields: + - normalized_address + issuing_card_shipping_customs: + description: '' + properties: + eori_number: + description: >- + A registration number used for customs in Europe. See + [https://www.gov.uk/eori](https://www.gov.uk/eori) for the UK and + [https://ec.europa.eu/taxation_customs/business/customs-procedures-import-and-export/customs-procedures/economic-operators-registration-and-identification-number-eori_en](https://ec.europa.eu/taxation_customs/business/customs-procedures-import-and-export/customs-procedures/economic-operators-registration-and-identification-number-eori_en) + for the EU. + maxLength: 5000 + nullable: true + type: string + title: IssuingCardShippingCustoms + type: object + x-expandableFields: [] + issuing_card_spending_limit: + description: '' + properties: + amount: + description: >- + Maximum amount allowed to spend per interval. This amount is in the + card's currency and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + categories: + description: >- + Array of strings containing + [categories](https://stripe.com/docs/api#issuing_authorization_object-merchant_data-category) + this limit applies to. Omitting this field will apply the limit to + all categories. + items: + enum: + - ac_refrigeration_repair + - accounting_bookkeeping_services + - advertising_services + - agricultural_cooperative + - airlines_air_carriers + - airports_flying_fields + - ambulance_services + - amusement_parks_carnivals + - antique_reproductions + - antique_shops + - aquariums + - architectural_surveying_services + - art_dealers_and_galleries + - artists_supply_and_craft_shops + - auto_and_home_supply_stores + - auto_body_repair_shops + - auto_paint_shops + - auto_service_shops + - automated_cash_disburse + - automated_fuel_dispensers + - automobile_associations + - automotive_parts_and_accessories_stores + - automotive_tire_stores + - bail_and_bond_payments + - bakeries + - bands_orchestras + - barber_and_beauty_shops + - betting_casino_gambling + - bicycle_shops + - billiard_pool_establishments + - boat_dealers + - boat_rentals_and_leases + - book_stores + - books_periodicals_and_newspapers + - bowling_alleys + - bus_lines + - business_secretarial_schools + - buying_shopping_services + - cable_satellite_and_other_pay_television_and_radio + - camera_and_photographic_supply_stores + - candy_nut_and_confectionery_stores + - car_and_truck_dealers_new_used + - car_and_truck_dealers_used_only + - car_rental_agencies + - car_washes + - carpentry_services + - carpet_upholstery_cleaning + - caterers + - charitable_and_social_service_organizations_fundraising + - chemicals_and_allied_products + - child_care_services + - childrens_and_infants_wear_stores + - chiropodists_podiatrists + - chiropractors + - cigar_stores_and_stands + - civic_social_fraternal_associations + - cleaning_and_maintenance + - clothing_rental + - colleges_universities + - commercial_equipment + - commercial_footwear + - commercial_photography_art_and_graphics + - commuter_transport_and_ferries + - computer_network_services + - computer_programming + - computer_repair + - computer_software_stores + - computers_peripherals_and_software + - concrete_work_services + - construction_materials + - consulting_public_relations + - correspondence_schools + - cosmetic_stores + - counseling_services + - country_clubs + - courier_services + - court_costs + - credit_reporting_agencies + - cruise_lines + - dairy_products_stores + - dance_hall_studios_schools + - dating_escort_services + - dentists_orthodontists + - department_stores + - detective_agencies + - digital_goods_applications + - digital_goods_games + - digital_goods_large_volume + - digital_goods_media + - direct_marketing_catalog_merchant + - direct_marketing_combination_catalog_and_retail_merchant + - direct_marketing_inbound_telemarketing + - direct_marketing_insurance_services + - direct_marketing_other + - direct_marketing_outbound_telemarketing + - direct_marketing_subscription + - direct_marketing_travel + - discount_stores + - doctors + - door_to_door_sales + - drapery_window_covering_and_upholstery_stores + - drinking_places + - drug_stores_and_pharmacies + - drugs_drug_proprietaries_and_druggist_sundries + - dry_cleaners + - durable_goods + - duty_free_stores + - eating_places_restaurants + - educational_services + - electric_razor_stores + - electric_vehicle_charging + - electrical_parts_and_equipment + - electrical_services + - electronics_repair_shops + - electronics_stores + - elementary_secondary_schools + - emergency_services_gcas_visa_use_only + - employment_temp_agencies + - equipment_rental + - exterminating_services + - family_clothing_stores + - fast_food_restaurants + - financial_institutions + - fines_government_administrative_entities + - fireplace_fireplace_screens_and_accessories_stores + - floor_covering_stores + - florists + - florists_supplies_nursery_stock_and_flowers + - freezer_and_locker_meat_provisioners + - fuel_dealers_non_automotive + - funeral_services_crematories + - >- + furniture_home_furnishings_and_equipment_stores_except_appliances + - furniture_repair_refinishing + - furriers_and_fur_shops + - general_services + - gift_card_novelty_and_souvenir_shops + - glass_paint_and_wallpaper_stores + - glassware_crystal_stores + - golf_courses_public + - government_licensed_horse_dog_racing_us_region_only + - >- + government_licensed_online_casions_online_gambling_us_region_only + - government_owned_lotteries_non_us_region + - government_owned_lotteries_us_region_only + - government_services + - grocery_stores_supermarkets + - hardware_equipment_and_supplies + - hardware_stores + - health_and_beauty_spas + - hearing_aids_sales_and_supplies + - heating_plumbing_a_c + - hobby_toy_and_game_shops + - home_supply_warehouse_stores + - hospitals + - hotels_motels_and_resorts + - household_appliance_stores + - industrial_supplies + - information_retrieval_services + - insurance_default + - insurance_underwriting_premiums + - intra_company_purchases + - jewelry_stores_watches_clocks_and_silverware_stores + - landscaping_services + - laundries + - laundry_cleaning_services + - legal_services_attorneys + - luggage_and_leather_goods_stores + - lumber_building_materials_stores + - manual_cash_disburse + - marinas_service_and_supplies + - marketplaces + - masonry_stonework_and_plaster + - massage_parlors + - medical_and_dental_labs + - medical_dental_ophthalmic_and_hospital_equipment_and_supplies + - medical_services + - membership_organizations + - mens_and_boys_clothing_and_accessories_stores + - mens_womens_clothing_stores + - metal_service_centers + - miscellaneous + - miscellaneous_apparel_and_accessory_shops + - miscellaneous_auto_dealers + - miscellaneous_business_services + - miscellaneous_food_stores + - miscellaneous_general_merchandise + - miscellaneous_general_services + - miscellaneous_home_furnishing_specialty_stores + - miscellaneous_publishing_and_printing + - miscellaneous_recreation_services + - miscellaneous_repair_shops + - miscellaneous_specialty_retail + - mobile_home_dealers + - motion_picture_theaters + - motor_freight_carriers_and_trucking + - motor_homes_dealers + - motor_vehicle_supplies_and_new_parts + - motorcycle_shops_and_dealers + - motorcycle_shops_dealers + - music_stores_musical_instruments_pianos_and_sheet_music + - news_dealers_and_newsstands + - non_fi_money_orders + - non_fi_stored_value_card_purchase_load + - nondurable_goods + - nurseries_lawn_and_garden_supply_stores + - nursing_personal_care + - office_and_commercial_furniture + - opticians_eyeglasses + - optometrists_ophthalmologist + - orthopedic_goods_prosthetic_devices + - osteopaths + - package_stores_beer_wine_and_liquor + - paints_varnishes_and_supplies + - parking_lots_garages + - passenger_railways + - pawn_shops + - pet_shops_pet_food_and_supplies + - petroleum_and_petroleum_products + - photo_developing + - photographic_photocopy_microfilm_equipment_and_supplies + - photographic_studios + - picture_video_production + - piece_goods_notions_and_other_dry_goods + - plumbing_heating_equipment_and_supplies + - political_organizations + - postal_services_government_only + - precious_stones_and_metals_watches_and_jewelry + - professional_services + - public_warehousing_and_storage + - quick_copy_repro_and_blueprint + - railroads + - real_estate_agents_and_managers_rentals + - record_stores + - recreational_vehicle_rentals + - religious_goods_stores + - religious_organizations + - roofing_siding_sheet_metal + - secretarial_support_services + - security_brokers_dealers + - service_stations + - sewing_needlework_fabric_and_piece_goods_stores + - shoe_repair_hat_cleaning + - shoe_stores + - small_appliance_repair + - snowmobile_dealers + - special_trade_services + - specialty_cleaning + - sporting_goods_stores + - sporting_recreation_camps + - sports_and_riding_apparel_stores + - sports_clubs_fields + - stamp_and_coin_stores + - stationary_office_supplies_printing_and_writing_paper + - stationery_stores_office_and_school_supply_stores + - swimming_pools_sales + - t_ui_travel_germany + - tailors_alterations + - tax_payments_government_agencies + - tax_preparation_services + - taxicabs_limousines + - telecommunication_equipment_and_telephone_sales + - telecommunication_services + - telegraph_services + - tent_and_awning_shops + - testing_laboratories + - theatrical_ticket_agencies + - timeshares + - tire_retreading_and_repair + - tolls_bridge_fees + - tourist_attractions_and_exhibits + - towing_services + - trailer_parks_campgrounds + - transportation_services + - travel_agencies_tour_operators + - truck_stop_iteration + - truck_utility_trailer_rentals + - typesetting_plate_making_and_related_services + - typewriter_stores + - u_s_federal_government_agencies_or_departments + - uniforms_commercial_clothing + - used_merchandise_and_secondhand_stores + - utilities + - variety_stores + - veterinary_services + - video_amusement_game_supplies + - video_game_arcades + - video_tape_rental_stores + - vocational_trade_schools + - watch_jewelry_repair + - welding_repair + - wholesale_clubs + - wig_and_toupee_stores + - wires_money_orders + - womens_accessory_and_specialty_shops + - womens_ready_to_wear_stores + - wrecking_and_salvage_yards + type: string + nullable: true + type: array + interval: + description: Interval (or event) to which the amount applies. + enum: + - all_time + - daily + - monthly + - per_authorization + - weekly + - yearly + type: string + required: + - amount + - interval + title: IssuingCardSpendingLimit + type: object + x-expandableFields: [] + issuing_card_wallets: + description: '' + properties: + apple_pay: + $ref: '#/components/schemas/issuing_card_apple_pay' + google_pay: + $ref: '#/components/schemas/issuing_card_google_pay' + primary_account_identifier: + description: Unique identifier for a card used with digital wallets + maxLength: 5000 + nullable: true + type: string + required: + - apple_pay + - google_pay + title: IssuingCardWallets + type: object + x-expandableFields: + - apple_pay + - google_pay + issuing_cardholder_address: + description: '' + properties: + address: + $ref: '#/components/schemas/address' + required: + - address + title: IssuingCardholderAddress + type: object + x-expandableFields: + - address + issuing_cardholder_authorization_controls: + description: '' + properties: + allowed_categories: + description: >- + Array of strings containing + [categories](https://stripe.com/docs/api#issuing_authorization_object-merchant_data-category) + of authorizations to allow. All other categories will be blocked. + Cannot be set with `blocked_categories`. + items: + enum: + - ac_refrigeration_repair + - accounting_bookkeeping_services + - advertising_services + - agricultural_cooperative + - airlines_air_carriers + - airports_flying_fields + - ambulance_services + - amusement_parks_carnivals + - antique_reproductions + - antique_shops + - aquariums + - architectural_surveying_services + - art_dealers_and_galleries + - artists_supply_and_craft_shops + - auto_and_home_supply_stores + - auto_body_repair_shops + - auto_paint_shops + - auto_service_shops + - automated_cash_disburse + - automated_fuel_dispensers + - automobile_associations + - automotive_parts_and_accessories_stores + - automotive_tire_stores + - bail_and_bond_payments + - bakeries + - bands_orchestras + - barber_and_beauty_shops + - betting_casino_gambling + - bicycle_shops + - billiard_pool_establishments + - boat_dealers + - boat_rentals_and_leases + - book_stores + - books_periodicals_and_newspapers + - bowling_alleys + - bus_lines + - business_secretarial_schools + - buying_shopping_services + - cable_satellite_and_other_pay_television_and_radio + - camera_and_photographic_supply_stores + - candy_nut_and_confectionery_stores + - car_and_truck_dealers_new_used + - car_and_truck_dealers_used_only + - car_rental_agencies + - car_washes + - carpentry_services + - carpet_upholstery_cleaning + - caterers + - charitable_and_social_service_organizations_fundraising + - chemicals_and_allied_products + - child_care_services + - childrens_and_infants_wear_stores + - chiropodists_podiatrists + - chiropractors + - cigar_stores_and_stands + - civic_social_fraternal_associations + - cleaning_and_maintenance + - clothing_rental + - colleges_universities + - commercial_equipment + - commercial_footwear + - commercial_photography_art_and_graphics + - commuter_transport_and_ferries + - computer_network_services + - computer_programming + - computer_repair + - computer_software_stores + - computers_peripherals_and_software + - concrete_work_services + - construction_materials + - consulting_public_relations + - correspondence_schools + - cosmetic_stores + - counseling_services + - country_clubs + - courier_services + - court_costs + - credit_reporting_agencies + - cruise_lines + - dairy_products_stores + - dance_hall_studios_schools + - dating_escort_services + - dentists_orthodontists + - department_stores + - detective_agencies + - digital_goods_applications + - digital_goods_games + - digital_goods_large_volume + - digital_goods_media + - direct_marketing_catalog_merchant + - direct_marketing_combination_catalog_and_retail_merchant + - direct_marketing_inbound_telemarketing + - direct_marketing_insurance_services + - direct_marketing_other + - direct_marketing_outbound_telemarketing + - direct_marketing_subscription + - direct_marketing_travel + - discount_stores + - doctors + - door_to_door_sales + - drapery_window_covering_and_upholstery_stores + - drinking_places + - drug_stores_and_pharmacies + - drugs_drug_proprietaries_and_druggist_sundries + - dry_cleaners + - durable_goods + - duty_free_stores + - eating_places_restaurants + - educational_services + - electric_razor_stores + - electric_vehicle_charging + - electrical_parts_and_equipment + - electrical_services + - electronics_repair_shops + - electronics_stores + - elementary_secondary_schools + - emergency_services_gcas_visa_use_only + - employment_temp_agencies + - equipment_rental + - exterminating_services + - family_clothing_stores + - fast_food_restaurants + - financial_institutions + - fines_government_administrative_entities + - fireplace_fireplace_screens_and_accessories_stores + - floor_covering_stores + - florists + - florists_supplies_nursery_stock_and_flowers + - freezer_and_locker_meat_provisioners + - fuel_dealers_non_automotive + - funeral_services_crematories + - >- + furniture_home_furnishings_and_equipment_stores_except_appliances + - furniture_repair_refinishing + - furriers_and_fur_shops + - general_services + - gift_card_novelty_and_souvenir_shops + - glass_paint_and_wallpaper_stores + - glassware_crystal_stores + - golf_courses_public + - government_licensed_horse_dog_racing_us_region_only + - >- + government_licensed_online_casions_online_gambling_us_region_only + - government_owned_lotteries_non_us_region + - government_owned_lotteries_us_region_only + - government_services + - grocery_stores_supermarkets + - hardware_equipment_and_supplies + - hardware_stores + - health_and_beauty_spas + - hearing_aids_sales_and_supplies + - heating_plumbing_a_c + - hobby_toy_and_game_shops + - home_supply_warehouse_stores + - hospitals + - hotels_motels_and_resorts + - household_appliance_stores + - industrial_supplies + - information_retrieval_services + - insurance_default + - insurance_underwriting_premiums + - intra_company_purchases + - jewelry_stores_watches_clocks_and_silverware_stores + - landscaping_services + - laundries + - laundry_cleaning_services + - legal_services_attorneys + - luggage_and_leather_goods_stores + - lumber_building_materials_stores + - manual_cash_disburse + - marinas_service_and_supplies + - marketplaces + - masonry_stonework_and_plaster + - massage_parlors + - medical_and_dental_labs + - medical_dental_ophthalmic_and_hospital_equipment_and_supplies + - medical_services + - membership_organizations + - mens_and_boys_clothing_and_accessories_stores + - mens_womens_clothing_stores + - metal_service_centers + - miscellaneous + - miscellaneous_apparel_and_accessory_shops + - miscellaneous_auto_dealers + - miscellaneous_business_services + - miscellaneous_food_stores + - miscellaneous_general_merchandise + - miscellaneous_general_services + - miscellaneous_home_furnishing_specialty_stores + - miscellaneous_publishing_and_printing + - miscellaneous_recreation_services + - miscellaneous_repair_shops + - miscellaneous_specialty_retail + - mobile_home_dealers + - motion_picture_theaters + - motor_freight_carriers_and_trucking + - motor_homes_dealers + - motor_vehicle_supplies_and_new_parts + - motorcycle_shops_and_dealers + - motorcycle_shops_dealers + - music_stores_musical_instruments_pianos_and_sheet_music + - news_dealers_and_newsstands + - non_fi_money_orders + - non_fi_stored_value_card_purchase_load + - nondurable_goods + - nurseries_lawn_and_garden_supply_stores + - nursing_personal_care + - office_and_commercial_furniture + - opticians_eyeglasses + - optometrists_ophthalmologist + - orthopedic_goods_prosthetic_devices + - osteopaths + - package_stores_beer_wine_and_liquor + - paints_varnishes_and_supplies + - parking_lots_garages + - passenger_railways + - pawn_shops + - pet_shops_pet_food_and_supplies + - petroleum_and_petroleum_products + - photo_developing + - photographic_photocopy_microfilm_equipment_and_supplies + - photographic_studios + - picture_video_production + - piece_goods_notions_and_other_dry_goods + - plumbing_heating_equipment_and_supplies + - political_organizations + - postal_services_government_only + - precious_stones_and_metals_watches_and_jewelry + - professional_services + - public_warehousing_and_storage + - quick_copy_repro_and_blueprint + - railroads + - real_estate_agents_and_managers_rentals + - record_stores + - recreational_vehicle_rentals + - religious_goods_stores + - religious_organizations + - roofing_siding_sheet_metal + - secretarial_support_services + - security_brokers_dealers + - service_stations + - sewing_needlework_fabric_and_piece_goods_stores + - shoe_repair_hat_cleaning + - shoe_stores + - small_appliance_repair + - snowmobile_dealers + - special_trade_services + - specialty_cleaning + - sporting_goods_stores + - sporting_recreation_camps + - sports_and_riding_apparel_stores + - sports_clubs_fields + - stamp_and_coin_stores + - stationary_office_supplies_printing_and_writing_paper + - stationery_stores_office_and_school_supply_stores + - swimming_pools_sales + - t_ui_travel_germany + - tailors_alterations + - tax_payments_government_agencies + - tax_preparation_services + - taxicabs_limousines + - telecommunication_equipment_and_telephone_sales + - telecommunication_services + - telegraph_services + - tent_and_awning_shops + - testing_laboratories + - theatrical_ticket_agencies + - timeshares + - tire_retreading_and_repair + - tolls_bridge_fees + - tourist_attractions_and_exhibits + - towing_services + - trailer_parks_campgrounds + - transportation_services + - travel_agencies_tour_operators + - truck_stop_iteration + - truck_utility_trailer_rentals + - typesetting_plate_making_and_related_services + - typewriter_stores + - u_s_federal_government_agencies_or_departments + - uniforms_commercial_clothing + - used_merchandise_and_secondhand_stores + - utilities + - variety_stores + - veterinary_services + - video_amusement_game_supplies + - video_game_arcades + - video_tape_rental_stores + - vocational_trade_schools + - watch_jewelry_repair + - welding_repair + - wholesale_clubs + - wig_and_toupee_stores + - wires_money_orders + - womens_accessory_and_specialty_shops + - womens_ready_to_wear_stores + - wrecking_and_salvage_yards + type: string + nullable: true + type: array + allowed_merchant_countries: + description: >- + Array of strings containing representing countries from which + authorizations will be allowed. Authorizations from merchants in all + other countries will be declined. Country codes should be ISO 3166 + alpha-2 country codes (e.g. `US`). Cannot be set with + `blocked_merchant_countries`. Provide an empty value to unset this + control. + items: + maxLength: 5000 + type: string + nullable: true + type: array + blocked_categories: + description: >- + Array of strings containing + [categories](https://stripe.com/docs/api#issuing_authorization_object-merchant_data-category) + of authorizations to decline. All other categories will be allowed. + Cannot be set with `allowed_categories`. + items: + enum: + - ac_refrigeration_repair + - accounting_bookkeeping_services + - advertising_services + - agricultural_cooperative + - airlines_air_carriers + - airports_flying_fields + - ambulance_services + - amusement_parks_carnivals + - antique_reproductions + - antique_shops + - aquariums + - architectural_surveying_services + - art_dealers_and_galleries + - artists_supply_and_craft_shops + - auto_and_home_supply_stores + - auto_body_repair_shops + - auto_paint_shops + - auto_service_shops + - automated_cash_disburse + - automated_fuel_dispensers + - automobile_associations + - automotive_parts_and_accessories_stores + - automotive_tire_stores + - bail_and_bond_payments + - bakeries + - bands_orchestras + - barber_and_beauty_shops + - betting_casino_gambling + - bicycle_shops + - billiard_pool_establishments + - boat_dealers + - boat_rentals_and_leases + - book_stores + - books_periodicals_and_newspapers + - bowling_alleys + - bus_lines + - business_secretarial_schools + - buying_shopping_services + - cable_satellite_and_other_pay_television_and_radio + - camera_and_photographic_supply_stores + - candy_nut_and_confectionery_stores + - car_and_truck_dealers_new_used + - car_and_truck_dealers_used_only + - car_rental_agencies + - car_washes + - carpentry_services + - carpet_upholstery_cleaning + - caterers + - charitable_and_social_service_organizations_fundraising + - chemicals_and_allied_products + - child_care_services + - childrens_and_infants_wear_stores + - chiropodists_podiatrists + - chiropractors + - cigar_stores_and_stands + - civic_social_fraternal_associations + - cleaning_and_maintenance + - clothing_rental + - colleges_universities + - commercial_equipment + - commercial_footwear + - commercial_photography_art_and_graphics + - commuter_transport_and_ferries + - computer_network_services + - computer_programming + - computer_repair + - computer_software_stores + - computers_peripherals_and_software + - concrete_work_services + - construction_materials + - consulting_public_relations + - correspondence_schools + - cosmetic_stores + - counseling_services + - country_clubs + - courier_services + - court_costs + - credit_reporting_agencies + - cruise_lines + - dairy_products_stores + - dance_hall_studios_schools + - dating_escort_services + - dentists_orthodontists + - department_stores + - detective_agencies + - digital_goods_applications + - digital_goods_games + - digital_goods_large_volume + - digital_goods_media + - direct_marketing_catalog_merchant + - direct_marketing_combination_catalog_and_retail_merchant + - direct_marketing_inbound_telemarketing + - direct_marketing_insurance_services + - direct_marketing_other + - direct_marketing_outbound_telemarketing + - direct_marketing_subscription + - direct_marketing_travel + - discount_stores + - doctors + - door_to_door_sales + - drapery_window_covering_and_upholstery_stores + - drinking_places + - drug_stores_and_pharmacies + - drugs_drug_proprietaries_and_druggist_sundries + - dry_cleaners + - durable_goods + - duty_free_stores + - eating_places_restaurants + - educational_services + - electric_razor_stores + - electric_vehicle_charging + - electrical_parts_and_equipment + - electrical_services + - electronics_repair_shops + - electronics_stores + - elementary_secondary_schools + - emergency_services_gcas_visa_use_only + - employment_temp_agencies + - equipment_rental + - exterminating_services + - family_clothing_stores + - fast_food_restaurants + - financial_institutions + - fines_government_administrative_entities + - fireplace_fireplace_screens_and_accessories_stores + - floor_covering_stores + - florists + - florists_supplies_nursery_stock_and_flowers + - freezer_and_locker_meat_provisioners + - fuel_dealers_non_automotive + - funeral_services_crematories + - >- + furniture_home_furnishings_and_equipment_stores_except_appliances + - furniture_repair_refinishing + - furriers_and_fur_shops + - general_services + - gift_card_novelty_and_souvenir_shops + - glass_paint_and_wallpaper_stores + - glassware_crystal_stores + - golf_courses_public + - government_licensed_horse_dog_racing_us_region_only + - >- + government_licensed_online_casions_online_gambling_us_region_only + - government_owned_lotteries_non_us_region + - government_owned_lotteries_us_region_only + - government_services + - grocery_stores_supermarkets + - hardware_equipment_and_supplies + - hardware_stores + - health_and_beauty_spas + - hearing_aids_sales_and_supplies + - heating_plumbing_a_c + - hobby_toy_and_game_shops + - home_supply_warehouse_stores + - hospitals + - hotels_motels_and_resorts + - household_appliance_stores + - industrial_supplies + - information_retrieval_services + - insurance_default + - insurance_underwriting_premiums + - intra_company_purchases + - jewelry_stores_watches_clocks_and_silverware_stores + - landscaping_services + - laundries + - laundry_cleaning_services + - legal_services_attorneys + - luggage_and_leather_goods_stores + - lumber_building_materials_stores + - manual_cash_disburse + - marinas_service_and_supplies + - marketplaces + - masonry_stonework_and_plaster + - massage_parlors + - medical_and_dental_labs + - medical_dental_ophthalmic_and_hospital_equipment_and_supplies + - medical_services + - membership_organizations + - mens_and_boys_clothing_and_accessories_stores + - mens_womens_clothing_stores + - metal_service_centers + - miscellaneous + - miscellaneous_apparel_and_accessory_shops + - miscellaneous_auto_dealers + - miscellaneous_business_services + - miscellaneous_food_stores + - miscellaneous_general_merchandise + - miscellaneous_general_services + - miscellaneous_home_furnishing_specialty_stores + - miscellaneous_publishing_and_printing + - miscellaneous_recreation_services + - miscellaneous_repair_shops + - miscellaneous_specialty_retail + - mobile_home_dealers + - motion_picture_theaters + - motor_freight_carriers_and_trucking + - motor_homes_dealers + - motor_vehicle_supplies_and_new_parts + - motorcycle_shops_and_dealers + - motorcycle_shops_dealers + - music_stores_musical_instruments_pianos_and_sheet_music + - news_dealers_and_newsstands + - non_fi_money_orders + - non_fi_stored_value_card_purchase_load + - nondurable_goods + - nurseries_lawn_and_garden_supply_stores + - nursing_personal_care + - office_and_commercial_furniture + - opticians_eyeglasses + - optometrists_ophthalmologist + - orthopedic_goods_prosthetic_devices + - osteopaths + - package_stores_beer_wine_and_liquor + - paints_varnishes_and_supplies + - parking_lots_garages + - passenger_railways + - pawn_shops + - pet_shops_pet_food_and_supplies + - petroleum_and_petroleum_products + - photo_developing + - photographic_photocopy_microfilm_equipment_and_supplies + - photographic_studios + - picture_video_production + - piece_goods_notions_and_other_dry_goods + - plumbing_heating_equipment_and_supplies + - political_organizations + - postal_services_government_only + - precious_stones_and_metals_watches_and_jewelry + - professional_services + - public_warehousing_and_storage + - quick_copy_repro_and_blueprint + - railroads + - real_estate_agents_and_managers_rentals + - record_stores + - recreational_vehicle_rentals + - religious_goods_stores + - religious_organizations + - roofing_siding_sheet_metal + - secretarial_support_services + - security_brokers_dealers + - service_stations + - sewing_needlework_fabric_and_piece_goods_stores + - shoe_repair_hat_cleaning + - shoe_stores + - small_appliance_repair + - snowmobile_dealers + - special_trade_services + - specialty_cleaning + - sporting_goods_stores + - sporting_recreation_camps + - sports_and_riding_apparel_stores + - sports_clubs_fields + - stamp_and_coin_stores + - stationary_office_supplies_printing_and_writing_paper + - stationery_stores_office_and_school_supply_stores + - swimming_pools_sales + - t_ui_travel_germany + - tailors_alterations + - tax_payments_government_agencies + - tax_preparation_services + - taxicabs_limousines + - telecommunication_equipment_and_telephone_sales + - telecommunication_services + - telegraph_services + - tent_and_awning_shops + - testing_laboratories + - theatrical_ticket_agencies + - timeshares + - tire_retreading_and_repair + - tolls_bridge_fees + - tourist_attractions_and_exhibits + - towing_services + - trailer_parks_campgrounds + - transportation_services + - travel_agencies_tour_operators + - truck_stop_iteration + - truck_utility_trailer_rentals + - typesetting_plate_making_and_related_services + - typewriter_stores + - u_s_federal_government_agencies_or_departments + - uniforms_commercial_clothing + - used_merchandise_and_secondhand_stores + - utilities + - variety_stores + - veterinary_services + - video_amusement_game_supplies + - video_game_arcades + - video_tape_rental_stores + - vocational_trade_schools + - watch_jewelry_repair + - welding_repair + - wholesale_clubs + - wig_and_toupee_stores + - wires_money_orders + - womens_accessory_and_specialty_shops + - womens_ready_to_wear_stores + - wrecking_and_salvage_yards + type: string + nullable: true + type: array + blocked_merchant_countries: + description: >- + Array of strings containing representing countries from which + authorizations will be declined. Country codes should be ISO 3166 + alpha-2 country codes (e.g. `US`). Cannot be set with + `allowed_merchant_countries`. Provide an empty value to unset this + control. + items: + maxLength: 5000 + type: string + nullable: true + type: array + spending_limits: + description: >- + Limit spending with amount-based rules that apply across this + cardholder's cards. + items: + $ref: '#/components/schemas/issuing_cardholder_spending_limit' + nullable: true + type: array + spending_limits_currency: + description: Currency of the amounts within `spending_limits`. + format: currency + nullable: true + type: string + title: IssuingCardholderAuthorizationControls + type: object + x-expandableFields: + - spending_limits + issuing_cardholder_card_issuing: + description: '' + properties: + user_terms_acceptance: + anyOf: + - $ref: '#/components/schemas/issuing_cardholder_user_terms_acceptance' + description: >- + Information about cardholder acceptance of Celtic [Authorized User + Terms](https://stripe.com/docs/issuing/cards#accept-authorized-user-terms). + Required for cards backed by a Celtic program. + nullable: true + title: IssuingCardholderCardIssuing + type: object + x-expandableFields: + - user_terms_acceptance + issuing_cardholder_company: + description: '' + properties: + tax_id_provided: + description: Whether the company's business ID number was provided. + type: boolean + required: + - tax_id_provided + title: IssuingCardholderCompany + type: object + x-expandableFields: [] + issuing_cardholder_id_document: + description: '' + properties: + back: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + The back of a document returned by a [file + upload](https://stripe.com/docs/api#create_file) with a `purpose` + value of `identity_document`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + front: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + The front of a document returned by a [file + upload](https://stripe.com/docs/api#create_file) with a `purpose` + value of `identity_document`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + title: IssuingCardholderIdDocument + type: object + x-expandableFields: + - back + - front + issuing_cardholder_individual: + description: '' + properties: + card_issuing: + anyOf: + - $ref: '#/components/schemas/issuing_cardholder_card_issuing' + description: Information related to the card_issuing program for this cardholder. + nullable: true + dob: + anyOf: + - $ref: '#/components/schemas/issuing_cardholder_individual_dob' + description: The date of birth of this cardholder. + nullable: true + first_name: + description: >- + The first name of this cardholder. Required before activating Cards. + This field cannot contain any numbers, special characters (except + periods, commas, hyphens, spaces and apostrophes) or non-latin + letters. + maxLength: 5000 + nullable: true + type: string + last_name: + description: >- + The last name of this cardholder. Required before activating Cards. + This field cannot contain any numbers, special characters (except + periods, commas, hyphens, spaces and apostrophes) or non-latin + letters. + maxLength: 5000 + nullable: true + type: string + verification: + anyOf: + - $ref: '#/components/schemas/issuing_cardholder_verification' + description: Government-issued ID document for this cardholder. + nullable: true + title: IssuingCardholderIndividual + type: object + x-expandableFields: + - card_issuing + - dob + - verification + issuing_cardholder_individual_dob: + description: '' + properties: + day: + description: 'The day of birth, between 1 and 31.' + nullable: true + type: integer + month: + description: 'The month of birth, between 1 and 12.' + nullable: true + type: integer + year: + description: The four-digit year of birth. + nullable: true + type: integer + title: IssuingCardholderIndividualDOB + type: object + x-expandableFields: [] + issuing_cardholder_requirements: + description: '' + properties: + disabled_reason: + description: >- + If `disabled_reason` is present, all cards will decline + authorizations with `cardholder_verification_required` reason. + enum: + - listed + - rejected.listed + - requirements.past_due + - under_review + nullable: true + type: string + past_due: + description: >- + Array of fields that need to be collected in order to verify and + re-enable the cardholder. + items: + enum: + - company.tax_id + - individual.card_issuing.user_terms_acceptance.date + - individual.card_issuing.user_terms_acceptance.ip + - individual.dob.day + - individual.dob.month + - individual.dob.year + - individual.first_name + - individual.last_name + - individual.verification.document + type: string + x-stripeBypassValidation: true + nullable: true + type: array + title: IssuingCardholderRequirements + type: object + x-expandableFields: [] + issuing_cardholder_spending_limit: + description: '' + properties: + amount: + description: >- + Maximum amount allowed to spend per interval. This amount is in the + card's currency and in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + categories: + description: >- + Array of strings containing + [categories](https://stripe.com/docs/api#issuing_authorization_object-merchant_data-category) + this limit applies to. Omitting this field will apply the limit to + all categories. + items: + enum: + - ac_refrigeration_repair + - accounting_bookkeeping_services + - advertising_services + - agricultural_cooperative + - airlines_air_carriers + - airports_flying_fields + - ambulance_services + - amusement_parks_carnivals + - antique_reproductions + - antique_shops + - aquariums + - architectural_surveying_services + - art_dealers_and_galleries + - artists_supply_and_craft_shops + - auto_and_home_supply_stores + - auto_body_repair_shops + - auto_paint_shops + - auto_service_shops + - automated_cash_disburse + - automated_fuel_dispensers + - automobile_associations + - automotive_parts_and_accessories_stores + - automotive_tire_stores + - bail_and_bond_payments + - bakeries + - bands_orchestras + - barber_and_beauty_shops + - betting_casino_gambling + - bicycle_shops + - billiard_pool_establishments + - boat_dealers + - boat_rentals_and_leases + - book_stores + - books_periodicals_and_newspapers + - bowling_alleys + - bus_lines + - business_secretarial_schools + - buying_shopping_services + - cable_satellite_and_other_pay_television_and_radio + - camera_and_photographic_supply_stores + - candy_nut_and_confectionery_stores + - car_and_truck_dealers_new_used + - car_and_truck_dealers_used_only + - car_rental_agencies + - car_washes + - carpentry_services + - carpet_upholstery_cleaning + - caterers + - charitable_and_social_service_organizations_fundraising + - chemicals_and_allied_products + - child_care_services + - childrens_and_infants_wear_stores + - chiropodists_podiatrists + - chiropractors + - cigar_stores_and_stands + - civic_social_fraternal_associations + - cleaning_and_maintenance + - clothing_rental + - colleges_universities + - commercial_equipment + - commercial_footwear + - commercial_photography_art_and_graphics + - commuter_transport_and_ferries + - computer_network_services + - computer_programming + - computer_repair + - computer_software_stores + - computers_peripherals_and_software + - concrete_work_services + - construction_materials + - consulting_public_relations + - correspondence_schools + - cosmetic_stores + - counseling_services + - country_clubs + - courier_services + - court_costs + - credit_reporting_agencies + - cruise_lines + - dairy_products_stores + - dance_hall_studios_schools + - dating_escort_services + - dentists_orthodontists + - department_stores + - detective_agencies + - digital_goods_applications + - digital_goods_games + - digital_goods_large_volume + - digital_goods_media + - direct_marketing_catalog_merchant + - direct_marketing_combination_catalog_and_retail_merchant + - direct_marketing_inbound_telemarketing + - direct_marketing_insurance_services + - direct_marketing_other + - direct_marketing_outbound_telemarketing + - direct_marketing_subscription + - direct_marketing_travel + - discount_stores + - doctors + - door_to_door_sales + - drapery_window_covering_and_upholstery_stores + - drinking_places + - drug_stores_and_pharmacies + - drugs_drug_proprietaries_and_druggist_sundries + - dry_cleaners + - durable_goods + - duty_free_stores + - eating_places_restaurants + - educational_services + - electric_razor_stores + - electric_vehicle_charging + - electrical_parts_and_equipment + - electrical_services + - electronics_repair_shops + - electronics_stores + - elementary_secondary_schools + - emergency_services_gcas_visa_use_only + - employment_temp_agencies + - equipment_rental + - exterminating_services + - family_clothing_stores + - fast_food_restaurants + - financial_institutions + - fines_government_administrative_entities + - fireplace_fireplace_screens_and_accessories_stores + - floor_covering_stores + - florists + - florists_supplies_nursery_stock_and_flowers + - freezer_and_locker_meat_provisioners + - fuel_dealers_non_automotive + - funeral_services_crematories + - >- + furniture_home_furnishings_and_equipment_stores_except_appliances + - furniture_repair_refinishing + - furriers_and_fur_shops + - general_services + - gift_card_novelty_and_souvenir_shops + - glass_paint_and_wallpaper_stores + - glassware_crystal_stores + - golf_courses_public + - government_licensed_horse_dog_racing_us_region_only + - >- + government_licensed_online_casions_online_gambling_us_region_only + - government_owned_lotteries_non_us_region + - government_owned_lotteries_us_region_only + - government_services + - grocery_stores_supermarkets + - hardware_equipment_and_supplies + - hardware_stores + - health_and_beauty_spas + - hearing_aids_sales_and_supplies + - heating_plumbing_a_c + - hobby_toy_and_game_shops + - home_supply_warehouse_stores + - hospitals + - hotels_motels_and_resorts + - household_appliance_stores + - industrial_supplies + - information_retrieval_services + - insurance_default + - insurance_underwriting_premiums + - intra_company_purchases + - jewelry_stores_watches_clocks_and_silverware_stores + - landscaping_services + - laundries + - laundry_cleaning_services + - legal_services_attorneys + - luggage_and_leather_goods_stores + - lumber_building_materials_stores + - manual_cash_disburse + - marinas_service_and_supplies + - marketplaces + - masonry_stonework_and_plaster + - massage_parlors + - medical_and_dental_labs + - medical_dental_ophthalmic_and_hospital_equipment_and_supplies + - medical_services + - membership_organizations + - mens_and_boys_clothing_and_accessories_stores + - mens_womens_clothing_stores + - metal_service_centers + - miscellaneous + - miscellaneous_apparel_and_accessory_shops + - miscellaneous_auto_dealers + - miscellaneous_business_services + - miscellaneous_food_stores + - miscellaneous_general_merchandise + - miscellaneous_general_services + - miscellaneous_home_furnishing_specialty_stores + - miscellaneous_publishing_and_printing + - miscellaneous_recreation_services + - miscellaneous_repair_shops + - miscellaneous_specialty_retail + - mobile_home_dealers + - motion_picture_theaters + - motor_freight_carriers_and_trucking + - motor_homes_dealers + - motor_vehicle_supplies_and_new_parts + - motorcycle_shops_and_dealers + - motorcycle_shops_dealers + - music_stores_musical_instruments_pianos_and_sheet_music + - news_dealers_and_newsstands + - non_fi_money_orders + - non_fi_stored_value_card_purchase_load + - nondurable_goods + - nurseries_lawn_and_garden_supply_stores + - nursing_personal_care + - office_and_commercial_furniture + - opticians_eyeglasses + - optometrists_ophthalmologist + - orthopedic_goods_prosthetic_devices + - osteopaths + - package_stores_beer_wine_and_liquor + - paints_varnishes_and_supplies + - parking_lots_garages + - passenger_railways + - pawn_shops + - pet_shops_pet_food_and_supplies + - petroleum_and_petroleum_products + - photo_developing + - photographic_photocopy_microfilm_equipment_and_supplies + - photographic_studios + - picture_video_production + - piece_goods_notions_and_other_dry_goods + - plumbing_heating_equipment_and_supplies + - political_organizations + - postal_services_government_only + - precious_stones_and_metals_watches_and_jewelry + - professional_services + - public_warehousing_and_storage + - quick_copy_repro_and_blueprint + - railroads + - real_estate_agents_and_managers_rentals + - record_stores + - recreational_vehicle_rentals + - religious_goods_stores + - religious_organizations + - roofing_siding_sheet_metal + - secretarial_support_services + - security_brokers_dealers + - service_stations + - sewing_needlework_fabric_and_piece_goods_stores + - shoe_repair_hat_cleaning + - shoe_stores + - small_appliance_repair + - snowmobile_dealers + - special_trade_services + - specialty_cleaning + - sporting_goods_stores + - sporting_recreation_camps + - sports_and_riding_apparel_stores + - sports_clubs_fields + - stamp_and_coin_stores + - stationary_office_supplies_printing_and_writing_paper + - stationery_stores_office_and_school_supply_stores + - swimming_pools_sales + - t_ui_travel_germany + - tailors_alterations + - tax_payments_government_agencies + - tax_preparation_services + - taxicabs_limousines + - telecommunication_equipment_and_telephone_sales + - telecommunication_services + - telegraph_services + - tent_and_awning_shops + - testing_laboratories + - theatrical_ticket_agencies + - timeshares + - tire_retreading_and_repair + - tolls_bridge_fees + - tourist_attractions_and_exhibits + - towing_services + - trailer_parks_campgrounds + - transportation_services + - travel_agencies_tour_operators + - truck_stop_iteration + - truck_utility_trailer_rentals + - typesetting_plate_making_and_related_services + - typewriter_stores + - u_s_federal_government_agencies_or_departments + - uniforms_commercial_clothing + - used_merchandise_and_secondhand_stores + - utilities + - variety_stores + - veterinary_services + - video_amusement_game_supplies + - video_game_arcades + - video_tape_rental_stores + - vocational_trade_schools + - watch_jewelry_repair + - welding_repair + - wholesale_clubs + - wig_and_toupee_stores + - wires_money_orders + - womens_accessory_and_specialty_shops + - womens_ready_to_wear_stores + - wrecking_and_salvage_yards + type: string + nullable: true + type: array + interval: + description: Interval (or event) to which the amount applies. + enum: + - all_time + - daily + - monthly + - per_authorization + - weekly + - yearly + type: string + required: + - amount + - interval + title: IssuingCardholderSpendingLimit + type: object + x-expandableFields: [] + issuing_cardholder_user_terms_acceptance: + description: '' + properties: + date: + description: >- + The Unix timestamp marking when the cardholder accepted the + Authorized User Terms. + format: unix-time + nullable: true + type: integer + ip: + description: >- + The IP address from which the cardholder accepted the Authorized + User Terms. + maxLength: 5000 + nullable: true + type: string + user_agent: + description: >- + The user agent of the browser from which the cardholder accepted the + Authorized User Terms. + maxLength: 5000 + nullable: true + type: string + title: IssuingCardholderUserTermsAcceptance + type: object + x-expandableFields: [] + issuing_cardholder_verification: + description: '' + properties: + document: + anyOf: + - $ref: '#/components/schemas/issuing_cardholder_id_document' + description: 'An identifying document, either a passport or local ID card.' + nullable: true + title: IssuingCardholderVerification + type: object + x-expandableFields: + - document + issuing_dispute_canceled_evidence: + description: '' + properties: + additional_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Additional documentation supporting the dispute. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + canceled_at: + description: Date when order was canceled. + format: unix-time + nullable: true + type: integer + cancellation_policy_provided: + description: Whether the cardholder was provided with a cancellation policy. + nullable: true + type: boolean + cancellation_reason: + description: Reason for canceling the order. + maxLength: 5000 + nullable: true + type: string + expected_at: + description: Date when the cardholder expected to receive the product. + format: unix-time + nullable: true + type: integer + explanation: + description: Explanation of why the cardholder is disputing this transaction. + maxLength: 5000 + nullable: true + type: string + product_description: + description: Description of the merchandise or service that was purchased. + maxLength: 5000 + nullable: true + type: string + product_type: + description: Whether the product was a merchandise or service. + enum: + - merchandise + - service + nullable: true + type: string + return_status: + description: Result of cardholder's attempt to return the product. + enum: + - merchant_rejected + - successful + nullable: true + type: string + returned_at: + description: Date when the product was returned or attempted to be returned. + format: unix-time + nullable: true + type: integer + title: IssuingDisputeCanceledEvidence + type: object + x-expandableFields: + - additional_documentation + issuing_dispute_duplicate_evidence: + description: '' + properties: + additional_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Additional documentation supporting the dispute. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + card_statement: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Copy of the card statement showing that the product had already been + paid for. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + cash_receipt: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Copy of the receipt showing that the product had been paid for in + cash. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + check_image: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Image of the front and back of the check that was used to pay for + the product. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + explanation: + description: Explanation of why the cardholder is disputing this transaction. + maxLength: 5000 + nullable: true + type: string + original_transaction: + description: >- + Transaction (e.g., ipi_...) that the disputed transaction is a + duplicate of. Of the two or more transactions that are copies of + each other, this is original undisputed one. + maxLength: 5000 + nullable: true + type: string + title: IssuingDisputeDuplicateEvidence + type: object + x-expandableFields: + - additional_documentation + - card_statement + - cash_receipt + - check_image + issuing_dispute_evidence: + description: '' + properties: + canceled: + $ref: '#/components/schemas/issuing_dispute_canceled_evidence' + duplicate: + $ref: '#/components/schemas/issuing_dispute_duplicate_evidence' + fraudulent: + $ref: '#/components/schemas/issuing_dispute_fraudulent_evidence' + merchandise_not_as_described: + $ref: >- + #/components/schemas/issuing_dispute_merchandise_not_as_described_evidence + no_valid_authorization: + $ref: '#/components/schemas/issuing_dispute_no_valid_authorization_evidence' + not_received: + $ref: '#/components/schemas/issuing_dispute_not_received_evidence' + other: + $ref: '#/components/schemas/issuing_dispute_other_evidence' + reason: + description: >- + The reason for filing the dispute. Its value will match the field + containing the evidence. + enum: + - canceled + - duplicate + - fraudulent + - merchandise_not_as_described + - no_valid_authorization + - not_received + - other + - service_not_as_described + type: string + x-stripeBypassValidation: true + service_not_as_described: + $ref: >- + #/components/schemas/issuing_dispute_service_not_as_described_evidence + required: + - reason + title: IssuingDisputeEvidence + type: object + x-expandableFields: + - canceled + - duplicate + - fraudulent + - merchandise_not_as_described + - no_valid_authorization + - not_received + - other + - service_not_as_described + issuing_dispute_fraudulent_evidence: + description: '' + properties: + additional_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Additional documentation supporting the dispute. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + explanation: + description: Explanation of why the cardholder is disputing this transaction. + maxLength: 5000 + nullable: true + type: string + title: IssuingDisputeFraudulentEvidence + type: object + x-expandableFields: + - additional_documentation + issuing_dispute_merchandise_not_as_described_evidence: + description: '' + properties: + additional_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Additional documentation supporting the dispute. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + explanation: + description: Explanation of why the cardholder is disputing this transaction. + maxLength: 5000 + nullable: true + type: string + received_at: + description: Date when the product was received. + format: unix-time + nullable: true + type: integer + return_description: + description: Description of the cardholder's attempt to return the product. + maxLength: 5000 + nullable: true + type: string + return_status: + description: Result of cardholder's attempt to return the product. + enum: + - merchant_rejected + - successful + nullable: true + type: string + returned_at: + description: Date when the product was returned or attempted to be returned. + format: unix-time + nullable: true + type: integer + title: IssuingDisputeMerchandiseNotAsDescribedEvidence + type: object + x-expandableFields: + - additional_documentation + issuing_dispute_no_valid_authorization_evidence: + description: '' + properties: + additional_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Additional documentation supporting the dispute. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + explanation: + description: Explanation of why the cardholder is disputing this transaction. + maxLength: 5000 + nullable: true + type: string + title: IssuingDisputeNoValidAuthorizationEvidence + type: object + x-expandableFields: + - additional_documentation + issuing_dispute_not_received_evidence: + description: '' + properties: + additional_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Additional documentation supporting the dispute. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + expected_at: + description: Date when the cardholder expected to receive the product. + format: unix-time + nullable: true + type: integer + explanation: + description: Explanation of why the cardholder is disputing this transaction. + maxLength: 5000 + nullable: true + type: string + product_description: + description: Description of the merchandise or service that was purchased. + maxLength: 5000 + nullable: true + type: string + product_type: + description: Whether the product was a merchandise or service. + enum: + - merchandise + - service + nullable: true + type: string + title: IssuingDisputeNotReceivedEvidence + type: object + x-expandableFields: + - additional_documentation + issuing_dispute_other_evidence: + description: '' + properties: + additional_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Additional documentation supporting the dispute. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + explanation: + description: Explanation of why the cardholder is disputing this transaction. + maxLength: 5000 + nullable: true + type: string + product_description: + description: Description of the merchandise or service that was purchased. + maxLength: 5000 + nullable: true + type: string + product_type: + description: Whether the product was a merchandise or service. + enum: + - merchandise + - service + nullable: true + type: string + title: IssuingDisputeOtherEvidence + type: object + x-expandableFields: + - additional_documentation + issuing_dispute_service_not_as_described_evidence: + description: '' + properties: + additional_documentation: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + (ID of a [file upload](https://stripe.com/docs/guides/file-upload)) + Additional documentation supporting the dispute. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + canceled_at: + description: Date when order was canceled. + format: unix-time + nullable: true + type: integer + cancellation_reason: + description: Reason for canceling the order. + maxLength: 5000 + nullable: true + type: string + explanation: + description: Explanation of why the cardholder is disputing this transaction. + maxLength: 5000 + nullable: true + type: string + received_at: + description: Date when the product was received. + format: unix-time + nullable: true + type: integer + title: IssuingDisputeServiceNotAsDescribedEvidence + type: object + x-expandableFields: + - additional_documentation + issuing_dispute_treasury: + description: '' + properties: + debit_reversal: + description: >- + The Treasury + [DebitReversal](https://stripe.com/docs/api/treasury/debit_reversals) + representing this Issuing dispute + maxLength: 5000 + nullable: true + type: string + received_debit: + description: >- + The Treasury + [ReceivedDebit](https://stripe.com/docs/api/treasury/received_debits) + that is being disputed. + maxLength: 5000 + type: string + required: + - received_debit + title: IssuingDisputeTreasury + type: object + x-expandableFields: [] + issuing_network_token_address: + description: '' + properties: + line1: + description: The street address of the cardholder tokenizing the card. + maxLength: 5000 + type: string + postal_code: + description: The postal code of the cardholder tokenizing the card. + maxLength: 5000 + type: string + required: + - line1 + - postal_code + title: IssuingNetworkTokenAddress + type: object + x-expandableFields: [] + issuing_network_token_device: + description: '' + properties: + device_fingerprint: + description: An obfuscated ID derived from the device ID. + maxLength: 5000 + type: string + ip_address: + description: The IP address of the device at provisioning time. + maxLength: 5000 + type: string + location: + description: >- + The geographic latitude/longitude coordinates of the device at + provisioning time. The format is [+-]decimal/[+-]decimal. + maxLength: 5000 + type: string + name: + description: The name of the device used for tokenization. + maxLength: 5000 + type: string + phone_number: + description: The phone number of the device used for tokenization. + maxLength: 5000 + type: string + type: + description: The type of device used for tokenization. + enum: + - other + - phone + - watch + type: string + title: IssuingNetworkTokenDevice + type: object + x-expandableFields: [] + issuing_network_token_mastercard: + description: '' + properties: + card_reference_id: + description: >- + A unique reference ID from MasterCard to represent the card account + number. + maxLength: 5000 + type: string + token_reference_id: + description: The network-unique identifier for the token. + maxLength: 5000 + type: string + token_requestor_id: + description: >- + The ID of the entity requesting tokenization, specific to + MasterCard. + maxLength: 5000 + type: string + token_requestor_name: + description: >- + The name of the entity requesting tokenization, if known. This is + directly provided from MasterCard. + maxLength: 5000 + type: string + required: + - token_reference_id + - token_requestor_id + title: IssuingNetworkTokenMastercard + type: object + x-expandableFields: [] + issuing_network_token_network_data: + description: '' + properties: + device: + $ref: '#/components/schemas/issuing_network_token_device' + mastercard: + $ref: '#/components/schemas/issuing_network_token_mastercard' + type: + description: >- + The network that the token is associated with. An additional hash is + included with a name matching this value, containing tokenization + data specific to the card network. + enum: + - mastercard + - visa + type: string + visa: + $ref: '#/components/schemas/issuing_network_token_visa' + wallet_provider: + $ref: '#/components/schemas/issuing_network_token_wallet_provider' + required: + - type + title: IssuingNetworkTokenNetworkData + type: object + x-expandableFields: + - device + - mastercard + - visa + - wallet_provider + issuing_network_token_visa: + description: '' + properties: + card_reference_id: + description: >- + A unique reference ID from Visa to represent the card account + number. + maxLength: 5000 + type: string + token_reference_id: + description: The network-unique identifier for the token. + maxLength: 5000 + type: string + token_requestor_id: + description: 'The ID of the entity requesting tokenization, specific to Visa.' + maxLength: 5000 + type: string + token_risk_score: + description: >- + Degree of risk associated with the token between `01` and `99`, with + higher number indicating higher risk. A `00` value indicates the + token was not scored by Visa. + maxLength: 5000 + type: string + required: + - card_reference_id + - token_reference_id + - token_requestor_id + title: IssuingNetworkTokenVisa + type: object + x-expandableFields: [] + issuing_network_token_wallet_provider: + description: '' + properties: + account_id: + description: >- + The wallet provider-given account ID of the digital wallet the token + belongs to. + maxLength: 5000 + type: string + account_trust_score: + description: >- + An evaluation on the trustworthiness of the wallet account between 1 + and 5. A higher score indicates more trustworthy. + type: integer + card_number_source: + description: The method used for tokenizing a card. + enum: + - app + - manual + - on_file + - other + type: string + cardholder_address: + $ref: '#/components/schemas/issuing_network_token_address' + cardholder_name: + description: The name of the cardholder tokenizing the card. + maxLength: 5000 + type: string + device_trust_score: + description: >- + An evaluation on the trustworthiness of the device. A higher score + indicates more trustworthy. + type: integer + hashed_account_email_address: + description: >- + The hashed email address of the cardholder's account with the wallet + provider. + maxLength: 5000 + type: string + reason_codes: + description: The reasons for suggested tokenization given by the card network. + items: + enum: + - account_card_too_new + - account_recently_changed + - account_too_new + - account_too_new_since_launch + - additional_device + - data_expired + - defer_id_v_decision + - device_recently_lost + - good_activity_history + - has_suspended_tokens + - high_risk + - inactive_account + - long_account_tenure + - low_account_score + - low_device_score + - low_phone_number_score + - network_service_error + - outside_home_territory + - provisioning_cardholder_mismatch + - provisioning_device_and_cardholder_mismatch + - provisioning_device_mismatch + - same_device_no_prior_authentication + - same_device_successful_prior_authentication + - software_update + - suspicious_activity + - too_many_different_cardholders + - too_many_recent_attempts + - too_many_recent_tokens + type: string + type: array + suggested_decision: + description: The recommendation on responding to the tokenization request. + enum: + - approve + - decline + - require_auth + type: string + suggested_decision_version: + description: >- + The version of the standard for mapping reason codes followed by the + wallet provider. + maxLength: 5000 + type: string + title: IssuingNetworkTokenWalletProvider + type: object + x-expandableFields: + - cardholder_address + issuing_personalization_design_carrier_text: + description: '' + properties: + footer_body: + description: The footer body text of the carrier letter. + maxLength: 5000 + nullable: true + type: string + footer_title: + description: The footer title text of the carrier letter. + maxLength: 5000 + nullable: true + type: string + header_body: + description: The header body text of the carrier letter. + maxLength: 5000 + nullable: true + type: string + header_title: + description: The header title text of the carrier letter. + maxLength: 5000 + nullable: true + type: string + title: IssuingPersonalizationDesignCarrierText + type: object + x-expandableFields: [] + issuing_personalization_design_preferences: + description: '' + properties: + is_default: + description: >- + Whether we use this personalization design to create cards when one + isn't specified. A connected account uses the Connect platform's + default design if no personalization design is set as the default + design. + type: boolean + is_platform_default: + description: >- + Whether this personalization design is used to create cards when one + is not specified and a default for this connected account does not + exist. + nullable: true + type: boolean + required: + - is_default + title: IssuingPersonalizationDesignPreferences + type: object + x-expandableFields: [] + issuing_personalization_design_rejection_reasons: + description: '' + properties: + card_logo: + description: The reason(s) the card logo was rejected. + items: + enum: + - geographic_location + - inappropriate + - network_name + - non_binary_image + - non_fiat_currency + - other + - other_entity + - promotional_material + type: string + nullable: true + type: array + carrier_text: + description: The reason(s) the carrier text was rejected. + items: + enum: + - geographic_location + - inappropriate + - network_name + - non_fiat_currency + - other + - other_entity + - promotional_material + type: string + nullable: true + type: array + title: IssuingPersonalizationDesignRejectionReasons + type: object + x-expandableFields: [] + issuing_physical_bundle_features: + description: '' + properties: + card_logo: + description: >- + The policy for how to use card logo images in a card design with + this physical bundle. + enum: + - optional + - required + - unsupported + type: string + carrier_text: + description: >- + The policy for how to use carrier letter text in a card design with + this physical bundle. + enum: + - optional + - required + - unsupported + type: string + second_line: + description: >- + The policy for how to use a second line on a card with this physical + bundle. + enum: + - optional + - required + - unsupported + type: string + required: + - card_logo + - carrier_text + - second_line + title: IssuingPhysicalBundleFeatures + type: object + x-expandableFields: [] + issuing_transaction_amount_details: + description: '' + properties: + atm_fee: + description: The fee charged by the ATM for the cash withdrawal. + nullable: true + type: integer + cashback_amount: + description: The amount of cash requested by the cardholder. + nullable: true + type: integer + title: IssuingTransactionAmountDetails + type: object + x-expandableFields: [] + issuing_transaction_fleet_cardholder_prompt_data: + description: '' + properties: + driver_id: + description: Driver ID. + maxLength: 5000 + nullable: true + type: string + odometer: + description: Odometer reading. + nullable: true + type: integer + unspecified_id: + description: >- + An alphanumeric ID. This field is used when a vehicle ID, driver ID, + or generic ID is entered by the cardholder, but the merchant or card + network did not specify the prompt type. + maxLength: 5000 + nullable: true + type: string + user_id: + description: User ID. + maxLength: 5000 + nullable: true + type: string + vehicle_number: + description: Vehicle number. + maxLength: 5000 + nullable: true + type: string + title: IssuingTransactionFleetCardholderPromptData + type: object + x-expandableFields: [] + issuing_transaction_fleet_data: + description: '' + properties: + cardholder_prompt_data: + anyOf: + - $ref: >- + #/components/schemas/issuing_transaction_fleet_cardholder_prompt_data + description: Answers to prompts presented to cardholder at point of sale. + nullable: true + purchase_type: + description: >- + The type of purchase. One of `fuel_purchase`, `non_fuel_purchase`, + or `fuel_and_non_fuel_purchase`. + maxLength: 5000 + nullable: true + type: string + reported_breakdown: + anyOf: + - $ref: >- + #/components/schemas/issuing_transaction_fleet_reported_breakdown + description: >- + More information about the total amount. This information is not + guaranteed to be accurate as some merchants may provide unreliable + data. + nullable: true + service_type: + description: >- + The type of fuel service. One of `non_fuel_transaction`, + `full_service`, or `self_service`. + maxLength: 5000 + nullable: true + type: string + title: IssuingTransactionFleetData + type: object + x-expandableFields: + - cardholder_prompt_data + - reported_breakdown + issuing_transaction_fleet_fuel_price_data: + description: '' + properties: + gross_amount_decimal: + description: >- + Gross fuel amount that should equal Fuel Volume multipled by Fuel + Unit Cost, inclusive of taxes. + format: decimal + nullable: true + type: string + title: IssuingTransactionFleetFuelPriceData + type: object + x-expandableFields: [] + issuing_transaction_fleet_non_fuel_price_data: + description: '' + properties: + gross_amount_decimal: + description: >- + Gross non-fuel amount that should equal the sum of the line items, + inclusive of taxes. + format: decimal + nullable: true + type: string + title: IssuingTransactionFleetNonFuelPriceData + type: object + x-expandableFields: [] + issuing_transaction_fleet_reported_breakdown: + description: '' + properties: + fuel: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_fleet_fuel_price_data' + description: Breakdown of fuel portion of the purchase. + nullable: true + non_fuel: + anyOf: + - $ref: >- + #/components/schemas/issuing_transaction_fleet_non_fuel_price_data + description: Breakdown of non-fuel portion of the purchase. + nullable: true + tax: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_fleet_tax_data' + description: Information about tax included in this transaction. + nullable: true + title: IssuingTransactionFleetReportedBreakdown + type: object + x-expandableFields: + - fuel + - non_fuel + - tax + issuing_transaction_fleet_tax_data: + description: '' + properties: + local_amount_decimal: + description: >- + Amount of state or provincial Sales Tax included in the transaction + amount. Null if not reported by merchant or not subject to tax. + format: decimal + nullable: true + type: string + national_amount_decimal: + description: >- + Amount of national Sales Tax or VAT included in the transaction + amount. Null if not reported by merchant or not subject to tax. + format: decimal + nullable: true + type: string + title: IssuingTransactionFleetTaxData + type: object + x-expandableFields: [] + issuing_transaction_flight_data: + description: '' + properties: + departure_at: + description: The time that the flight departed. + nullable: true + type: integer + passenger_name: + description: The name of the passenger. + maxLength: 5000 + nullable: true + type: string + refundable: + description: Whether the ticket is refundable. + nullable: true + type: boolean + segments: + description: The legs of the trip. + items: + $ref: '#/components/schemas/issuing_transaction_flight_data_leg' + nullable: true + type: array + travel_agency: + description: The travel agency that issued the ticket. + maxLength: 5000 + nullable: true + type: string + title: IssuingTransactionFlightData + type: object + x-expandableFields: + - segments + issuing_transaction_flight_data_leg: + description: '' + properties: + arrival_airport_code: + description: The three-letter IATA airport code of the flight's destination. + maxLength: 5000 + nullable: true + type: string + carrier: + description: The airline carrier code. + maxLength: 5000 + nullable: true + type: string + departure_airport_code: + description: The three-letter IATA airport code that the flight departed from. + maxLength: 5000 + nullable: true + type: string + flight_number: + description: The flight number. + maxLength: 5000 + nullable: true + type: string + service_class: + description: The flight's service class. + maxLength: 5000 + nullable: true + type: string + stopover_allowed: + description: Whether a stopover is allowed on this flight. + nullable: true + type: boolean + title: IssuingTransactionFlightDataLeg + type: object + x-expandableFields: [] + issuing_transaction_fuel_data: + description: '' + properties: + industry_product_code: + description: >- + [Conexxus Payment System Product + Code](https://www.conexxus.org/conexxus-payment-system-product-codes) + identifying the primary fuel product purchased. + maxLength: 5000 + nullable: true + type: string + quantity_decimal: + description: >- + The quantity of `unit`s of fuel that was dispensed, represented as a + decimal string with at most 12 decimal places. + format: decimal + nullable: true + type: string + type: + description: >- + The type of fuel that was purchased. One of `diesel`, + `unleaded_plus`, `unleaded_regular`, `unleaded_super`, or `other`. + maxLength: 5000 + type: string + unit: + description: >- + The units for `quantity_decimal`. One of `charging_minute`, + `imperial_gallon`, `kilogram`, `kilowatt_hour`, `liter`, `pound`, + `us_gallon`, or `other`. + maxLength: 5000 + type: string + unit_cost_decimal: + description: >- + The cost in cents per each unit of fuel, represented as a decimal + string with at most 12 decimal places. + format: decimal + type: string + required: + - type + - unit + - unit_cost_decimal + title: IssuingTransactionFuelData + type: object + x-expandableFields: [] + issuing_transaction_lodging_data: + description: '' + properties: + check_in_at: + description: The time of checking into the lodging. + nullable: true + type: integer + nights: + description: The number of nights stayed at the lodging. + nullable: true + type: integer + title: IssuingTransactionLodgingData + type: object + x-expandableFields: [] + issuing_transaction_network_data: + description: '' + properties: + authorization_code: + description: >- + A code created by Stripe which is shared with the merchant to + validate the authorization. This field will be populated if the + authorization message was approved. The code typically starts with + the letter "S", followed by a six-digit number. For example, + "S498162". Please note that the code is not guaranteed to be unique + across authorizations. + maxLength: 5000 + nullable: true + type: string + processing_date: + description: >- + The date the transaction was processed by the card network. This can + be different from the date the seller recorded the transaction + depending on when the acquirer submits the transaction to the + network. + maxLength: 5000 + nullable: true + type: string + transaction_id: + description: >- + Unique identifier for the authorization assigned by the card network + used to match subsequent messages, disputes, and transactions. + maxLength: 5000 + nullable: true + type: string + title: IssuingTransactionNetworkData + type: object + x-expandableFields: [] + issuing_transaction_purchase_details: + description: '' + properties: + fleet: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_fleet_data' + description: Fleet-specific information for transactions using Fleet cards. + nullable: true + flight: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_flight_data' + description: >- + Information about the flight that was purchased with this + transaction. + nullable: true + fuel: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_fuel_data' + description: Information about fuel that was purchased with this transaction. + nullable: true + lodging: + anyOf: + - $ref: '#/components/schemas/issuing_transaction_lodging_data' + description: Information about lodging that was purchased with this transaction. + nullable: true + receipt: + description: The line items in the purchase. + items: + $ref: '#/components/schemas/issuing_transaction_receipt_data' + nullable: true + type: array + reference: + description: A merchant-specific order number. + maxLength: 5000 + nullable: true + type: string + title: IssuingTransactionPurchaseDetails + type: object + x-expandableFields: + - fleet + - flight + - fuel + - lodging + - receipt + issuing_transaction_receipt_data: + description: '' + properties: + description: + description: >- + The description of the item. The maximum length of this field is 26 + characters. + maxLength: 5000 + nullable: true + type: string + quantity: + description: The quantity of the item. + nullable: true + type: number + total: + description: The total for this line item in cents. + nullable: true + type: integer + unit_cost: + description: The unit cost of the item in cents. + nullable: true + type: integer + title: IssuingTransactionReceiptData + type: object + x-expandableFields: [] + issuing_transaction_treasury: + description: '' + properties: + received_credit: + description: >- + The Treasury + [ReceivedCredit](https://stripe.com/docs/api/treasury/received_credits) + representing this Issuing transaction if it is a refund + maxLength: 5000 + nullable: true + type: string + received_debit: + description: >- + The Treasury + [ReceivedDebit](https://stripe.com/docs/api/treasury/received_debits) + representing this Issuing transaction if it is a capture + maxLength: 5000 + nullable: true + type: string + title: IssuingTransactionTreasury + type: object + x-expandableFields: [] + item: + description: A line item. + properties: + amount_discount: + description: >- + Total discount amount applied. If no discounts were applied, + defaults to 0. + type: integer + amount_subtotal: + description: Total before any discounts or taxes are applied. + type: integer + amount_tax: + description: 'Total tax amount applied. If no tax was applied, defaults to 0.' + type: integer + amount_total: + description: Total after discounts and taxes. + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. Defaults to product name. + maxLength: 5000 + nullable: true + type: string + discounts: + description: The discounts applied to the line item. + items: + $ref: '#/components/schemas/line_items_discount_amount' + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - item + type: string + price: + anyOf: + - $ref: '#/components/schemas/price' + description: The price used to generate the line item. + nullable: true + quantity: + description: The quantity of products being purchased. + nullable: true + type: integer + taxes: + description: The taxes applied to the line item. + items: + $ref: '#/components/schemas/line_items_tax_amount' + type: array + required: + - amount_discount + - amount_subtotal + - amount_tax + - amount_total + - currency + - id + - object + title: LineItem + type: object + x-expandableFields: + - discounts + - price + - taxes + x-resourceId: item + klarna_address: + description: '' + properties: + country: + description: The payer address country + maxLength: 5000 + nullable: true + type: string + title: klarna_address + type: object + x-expandableFields: [] + klarna_payer_details: + description: '' + properties: + address: + anyOf: + - $ref: '#/components/schemas/klarna_address' + description: The payer's address + nullable: true + title: klarna_payer_details + type: object + x-expandableFields: + - address + legal_entity_company: + description: '' + properties: + address: + $ref: '#/components/schemas/address' + address_kana: + anyOf: + - $ref: '#/components/schemas/legal_entity_japan_address' + description: The Kana variation of the company's primary address (Japan only). + nullable: true + address_kanji: + anyOf: + - $ref: '#/components/schemas/legal_entity_japan_address' + description: The Kanji variation of the company's primary address (Japan only). + nullable: true + directors_provided: + description: >- + Whether the company's directors have been provided. This Boolean + will be `true` if you've manually indicated that all directors are + provided via [the `directors_provided` + parameter](https://stripe.com/docs/api/accounts/update#update_account-company-directors_provided). + type: boolean + directorship_declaration: + anyOf: + - $ref: '#/components/schemas/legal_entity_directorship_declaration' + description: >- + This hash is used to attest that the director information provided + to Stripe is both current and correct. + nullable: true + executives_provided: + description: >- + Whether the company's executives have been provided. This Boolean + will be `true` if you've manually indicated that all executives are + provided via [the `executives_provided` + parameter](https://stripe.com/docs/api/accounts/update#update_account-company-executives_provided), + or if Stripe determined that sufficient executives were provided. + type: boolean + export_license_id: + description: >- + The export license ID number of the company, also referred as Import + Export Code (India only). + maxLength: 5000 + type: string + export_purpose_code: + description: The purpose code to use for export transactions (India only). + maxLength: 5000 + type: string + name: + description: >- + The company's legal name. Also available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + name_kana: + description: >- + The Kana variation of the company's legal name (Japan only). Also + available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + name_kanji: + description: >- + The Kanji variation of the company's legal name (Japan only). Also + available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + owners_provided: + description: >- + Whether the company's owners have been provided. This Boolean will + be `true` if you've manually indicated that all owners are provided + via [the `owners_provided` + parameter](https://stripe.com/docs/api/accounts/update#update_account-company-owners_provided), + or if Stripe determined that sufficient owners were provided. Stripe + determines ownership requirements using both the number of owners + provided and their total percent ownership (calculated by adding the + `percent_ownership` of each owner together). + type: boolean + ownership_declaration: + anyOf: + - $ref: '#/components/schemas/legal_entity_ubo_declaration' + description: >- + This hash is used to attest that the beneficial owner information + provided to Stripe is both current and correct. + nullable: true + ownership_exemption_reason: + description: >- + This value is used to determine if a business is exempt from + providing ultimate beneficial owners. See [this support + article](https://support.stripe.com/questions/exemption-from-providing-ownership-details) + and + [changelog](https://docs.stripe.com/changelog/acacia/2025-01-27/ownership-exemption-reason-accounts-api) + for more details. + enum: + - qualified_entity_exceeds_ownership_threshold + - qualifies_as_financial_institution + type: string + phone: + description: The company's phone number (used for verification). + maxLength: 5000 + nullable: true + type: string + registration_date: + $ref: '#/components/schemas/legal_entity_registration_date' + structure: + description: >- + The category identifying the legal structure of the company or legal + entity. Also available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. See [Business + structure](https://stripe.com/docs/connect/identity-verification#business-structure) + for more details. + enum: + - free_zone_establishment + - free_zone_llc + - government_instrumentality + - governmental_unit + - incorporated_non_profit + - incorporated_partnership + - limited_liability_partnership + - llc + - multi_member_llc + - private_company + - private_corporation + - private_partnership + - public_company + - public_corporation + - public_partnership + - registered_charity + - single_member_llc + - sole_establishment + - sole_proprietorship + - tax_exempt_government_instrumentality + - unincorporated_association + - unincorporated_non_profit + - unincorporated_partnership + type: string + x-stripeBypassValidation: true + tax_id_provided: + description: Whether the company's business ID number was provided. + type: boolean + tax_id_registrar: + description: >- + The jurisdiction in which the `tax_id` is registered (Germany-based + companies only). + maxLength: 5000 + type: string + vat_id_provided: + description: Whether the company's business VAT number was provided. + type: boolean + verification: + anyOf: + - $ref: '#/components/schemas/legal_entity_company_verification' + description: Information on the verification state of the company. + nullable: true + title: LegalEntityCompany + type: object + x-expandableFields: + - address + - address_kana + - address_kanji + - directorship_declaration + - ownership_declaration + - registration_date + - verification + legal_entity_company_verification: + description: '' + properties: + document: + $ref: '#/components/schemas/legal_entity_company_verification_document' + required: + - document + title: LegalEntityCompanyVerification + type: object + x-expandableFields: + - document + legal_entity_company_verification_document: + description: '' + properties: + back: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + The back of a document returned by a [file + upload](https://stripe.com/docs/api#create_file) with a `purpose` + value of `additional_verification`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + details: + description: >- + A user-displayable string describing the verification state of this + document. + maxLength: 5000 + nullable: true + type: string + details_code: + description: >- + One of `document_corrupt`, `document_expired`, + `document_failed_copy`, `document_failed_greyscale`, + `document_failed_other`, `document_failed_test_mode`, + `document_fraudulent`, `document_incomplete`, `document_invalid`, + `document_manipulated`, `document_not_readable`, + `document_not_uploaded`, `document_type_not_supported`, or + `document_too_large`. A machine-readable code specifying the + verification state for this document. + maxLength: 5000 + nullable: true + type: string + front: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + The front of a document returned by a [file + upload](https://stripe.com/docs/api#create_file) with a `purpose` + value of `additional_verification`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + title: LegalEntityCompanyVerificationDocument + type: object + x-expandableFields: + - back + - front + legal_entity_directorship_declaration: + description: '' + properties: + date: + description: >- + The Unix timestamp marking when the directorship declaration + attestation was made. + format: unix-time + nullable: true + type: integer + ip: + description: >- + The IP address from which the directorship declaration attestation + was made. + maxLength: 5000 + nullable: true + type: string + user_agent: + description: >- + The user-agent string from the browser where the directorship + declaration attestation was made. + maxLength: 5000 + nullable: true + type: string + title: LegalEntityDirectorshipDeclaration + type: object + x-expandableFields: [] + legal_entity_dob: + description: '' + properties: + day: + description: 'The day of birth, between 1 and 31.' + nullable: true + type: integer + month: + description: 'The month of birth, between 1 and 12.' + nullable: true + type: integer + year: + description: The four-digit year of birth. + nullable: true + type: integer + title: LegalEntityDOB + type: object + x-expandableFields: [] + legal_entity_japan_address: + description: '' + properties: + city: + description: City/Ward. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + nullable: true + type: string + line1: + description: Block/Building number. + maxLength: 5000 + nullable: true + type: string + line2: + description: Building details. + maxLength: 5000 + nullable: true + type: string + postal_code: + description: ZIP or postal code. + maxLength: 5000 + nullable: true + type: string + state: + description: Prefecture. + maxLength: 5000 + nullable: true + type: string + town: + description: Town/cho-me. + maxLength: 5000 + nullable: true + type: string + title: LegalEntityJapanAddress + type: object + x-expandableFields: [] + legal_entity_person_verification: + description: '' + properties: + additional_document: + anyOf: + - $ref: '#/components/schemas/legal_entity_person_verification_document' + description: >- + A document showing address, either a passport, local ID card, or + utility bill from a well-known utility company. + nullable: true + details: + description: >- + A user-displayable string describing the verification state for the + person. For example, this may say "Provided identity information + could not be verified". + maxLength: 5000 + nullable: true + type: string + details_code: + description: >- + One of `document_address_mismatch`, `document_dob_mismatch`, + `document_duplicate_type`, `document_id_number_mismatch`, + `document_name_mismatch`, `document_nationality_mismatch`, + `failed_keyed_identity`, or `failed_other`. A machine-readable code + specifying the verification state for the person. + maxLength: 5000 + nullable: true + type: string + document: + $ref: '#/components/schemas/legal_entity_person_verification_document' + status: + description: >- + The state of verification for the person. Possible values are + `unverified`, `pending`, or `verified`. Please refer + [guide](https://stripe.com/docs/connect/handling-api-verification) + to handle verification updates. + maxLength: 5000 + type: string + required: + - status + title: LegalEntityPersonVerification + type: object + x-expandableFields: + - additional_document + - document + legal_entity_person_verification_document: + description: '' + properties: + back: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + The back of an ID returned by a [file + upload](https://stripe.com/docs/api#create_file) with a `purpose` + value of `identity_document`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + details: + description: >- + A user-displayable string describing the verification state of this + document. For example, if a document is uploaded and the picture is + too fuzzy, this may say "Identity document is too unclear to read". + maxLength: 5000 + nullable: true + type: string + details_code: + description: >- + One of `document_corrupt`, `document_country_not_supported`, + `document_expired`, `document_failed_copy`, `document_failed_other`, + `document_failed_test_mode`, `document_fraudulent`, + `document_failed_greyscale`, `document_incomplete`, + `document_invalid`, `document_manipulated`, `document_missing_back`, + `document_missing_front`, `document_not_readable`, + `document_not_uploaded`, `document_photo_mismatch`, + `document_too_large`, or `document_type_not_supported`. A + machine-readable code specifying the verification state for this + document. + maxLength: 5000 + nullable: true + type: string + front: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: >- + The front of an ID returned by a [file + upload](https://stripe.com/docs/api#create_file) with a `purpose` + value of `identity_document`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + title: LegalEntityPersonVerificationDocument + type: object + x-expandableFields: + - back + - front + legal_entity_registration_date: + description: '' + properties: + day: + description: 'The day of registration, between 1 and 31.' + nullable: true + type: integer + month: + description: 'The month of registration, between 1 and 12.' + nullable: true + type: integer + year: + description: The four-digit year of registration. + nullable: true + type: integer + title: LegalEntityRegistrationDate + type: object + x-expandableFields: [] + legal_entity_ubo_declaration: + description: '' + properties: + date: + description: >- + The Unix timestamp marking when the beneficial owner attestation was + made. + format: unix-time + nullable: true + type: integer + ip: + description: The IP address from which the beneficial owner attestation was made. + maxLength: 5000 + nullable: true + type: string + user_agent: + description: >- + The user-agent string from the browser where the beneficial owner + attestation was made. + maxLength: 5000 + nullable: true + type: string + title: LegalEntityUBODeclaration + type: object + x-expandableFields: [] + line_item: + description: >- + Invoice Line Items represent the individual lines within an + [invoice](https://stripe.com/docs/api/invoices) and only exist within + the context of an invoice. + + + Each line item is backed by either an [invoice + item](https://stripe.com/docs/api/invoiceitems) or a [subscription + item](https://stripe.com/docs/api/subscription_items). + properties: + amount: + description: 'The amount, in cents (or local equivalent).' + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + discount_amounts: + description: The amount of discount calculated per discount for this line item. + items: + $ref: '#/components/schemas/discounts_resource_discount_amount' + nullable: true + type: array + discountable: + description: >- + If true, discounts will apply to this line item. Always false for + prorations. + type: boolean + discounts: + description: >- + The discounts applied to the invoice line item. Line item discounts + are applied before invoice discounts. Use `expand[]=discounts` to + expand each discount. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice: + description: The ID of the invoice that contains this line item. + maxLength: 5000 + nullable: true + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. Note + that for line items with `type=subscription`, `metadata` reflects + the current metadata from the subscription associated with the line + item, unless the invoice line was directly updated with different + metadata after creation. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - line_item + type: string + parent: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_lines_parents_invoice_line_item_parent + description: The parent that generated this line item. + nullable: true + period: + $ref: '#/components/schemas/invoice_line_item_period' + pretax_credit_amounts: + description: >- + Contains pretax credit amounts (ex: discount, credit grants, etc) + that apply to this line item. + items: + $ref: '#/components/schemas/invoices_resource_pretax_credit_amount' + nullable: true + type: array + pricing: + anyOf: + - $ref: >- + #/components/schemas/billing_bill_resource_invoicing_pricing_pricing + description: The pricing information of the line item. + nullable: true + quantity: + description: >- + The quantity of the subscription, if the line item is a subscription + or a proration. + nullable: true + type: integer + subscription: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/subscription' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/subscription' + taxes: + description: The tax information of the line item. + items: + $ref: '#/components/schemas/billing_bill_resource_invoicing_taxes_tax' + nullable: true + type: array + required: + - amount + - currency + - discountable + - discounts + - id + - livemode + - metadata + - object + - period + title: InvoiceLineItem + type: object + x-expandableFields: + - discount_amounts + - discounts + - parent + - period + - pretax_credit_amounts + - pricing + - subscription + - taxes + x-resourceId: line_item + line_items_discount_amount: + description: '' + properties: + amount: + description: The amount discounted. + type: integer + discount: + $ref: '#/components/schemas/discount' + required: + - amount + - discount + title: LineItemsDiscountAmount + type: object + x-expandableFields: + - discount + line_items_tax_amount: + description: '' + properties: + amount: + description: Amount of tax applied for this rate. + type: integer + rate: + $ref: '#/components/schemas/tax_rate' + taxability_reason: + description: >- + The reasoning behind this tax, for example, if the product is tax + exempt. The possible values for this field may be extended as new + tax rules are supported. + enum: + - customer_exempt + - not_collecting + - not_subject_to_tax + - not_supported + - portion_product_exempt + - portion_reduced_rated + - portion_standard_rated + - product_exempt + - product_exempt_holiday + - proportionally_rated + - reduced_rated + - reverse_charge + - standard_rated + - taxable_basis_reduced + - zero_rated + nullable: true + type: string + x-stripeBypassValidation: true + taxable_amount: + description: >- + The amount on which tax is calculated, in cents (or local + equivalent). + nullable: true + type: integer + required: + - amount + - rate + title: LineItemsTaxAmount + type: object + x-expandableFields: + - rate + linked_account_options_common: + description: '' + properties: + filters: + $ref: >- + #/components/schemas/payment_flows_private_payment_methods_financial_connections_common_linked_account_options_filters + permissions: + description: >- + The list of permissions to request. The `payment_method` permission + must be included. + items: + enum: + - balances + - ownership + - payment_method + - transactions + type: string + type: array + prefetch: + description: Data features requested to be retrieved upon account creation. + items: + enum: + - balances + - ownership + - transactions + type: string + x-stripeBypassValidation: true + nullable: true + type: array + return_url: + description: >- + For webview integrations only. Upon completing OAuth login in the + native browser, the user will be redirected to this URL to return to + your app. + maxLength: 5000 + type: string + title: linked_account_options_common + type: object + x-expandableFields: + - filters + login_link: + description: >- + Login Links are single-use URLs that takes an Express account to the + login page for their Stripe dashboard. + + A Login Link differs from an [Account + Link](https://stripe.com/docs/api/account_links) in that it takes the + user directly to their [Express dashboard for the specified + account](https://stripe.com/docs/connect/integrate-express-dashboard#create-login-link) + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - login_link + type: string + url: + description: The URL for the login link. + maxLength: 5000 + type: string + required: + - created + - object + - url + title: LoginLink + type: object + x-expandableFields: [] + x-resourceId: login_link + mandate: + description: >- + A Mandate is a record of the permission that your customer gives you to + debit their payment method. + properties: + customer_acceptance: + $ref: '#/components/schemas/customer_acceptance' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + multi_use: + $ref: '#/components/schemas/mandate_multi_use' + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - mandate + type: string + on_behalf_of: + description: The account (if any) that the mandate is intended for. + maxLength: 5000 + type: string + payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: ID of the payment method associated with this mandate. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + payment_method_details: + $ref: '#/components/schemas/mandate_payment_method_details' + single_use: + $ref: '#/components/schemas/mandate_single_use' + status: + description: >- + The mandate status indicates whether or not you can use it to + initiate a payment. + enum: + - active + - inactive + - pending + type: string + type: + description: The type of the mandate. + enum: + - multi_use + - single_use + type: string + required: + - customer_acceptance + - id + - livemode + - object + - payment_method + - payment_method_details + - status + - type + title: Mandate + type: object + x-expandableFields: + - customer_acceptance + - multi_use + - payment_method + - payment_method_details + - single_use + x-resourceId: mandate + mandate_acss_debit: + description: '' + properties: + default_for: + description: >- + List of Stripe products where this mandate can be selected + automatically. + items: + enum: + - invoice + - subscription + type: string + type: array + interval_description: + description: >- + Description of the interval. Only required if the 'payment_schedule' + parameter is 'interval' or 'combined'. + maxLength: 5000 + nullable: true + type: string + payment_schedule: + description: Payment schedule for the mandate. + enum: + - combined + - interval + - sporadic + type: string + transaction_type: + description: Transaction type of the mandate. + enum: + - business + - personal + type: string + required: + - payment_schedule + - transaction_type + title: mandate_acss_debit + type: object + x-expandableFields: [] + mandate_amazon_pay: + description: '' + properties: {} + title: mandate_amazon_pay + type: object + x-expandableFields: [] + mandate_au_becs_debit: + description: '' + properties: + url: + description: >- + The URL of the mandate. This URL generally contains sensitive + information about the customer and should be shared with them + exclusively. + maxLength: 5000 + type: string + required: + - url + title: mandate_au_becs_debit + type: object + x-expandableFields: [] + mandate_bacs_debit: + description: '' + properties: + network_status: + description: >- + The status of the mandate on the Bacs network. Can be one of + `pending`, `revoked`, `refused`, or `accepted`. + enum: + - accepted + - pending + - refused + - revoked + type: string + reference: + description: The unique reference identifying the mandate on the Bacs network. + maxLength: 5000 + type: string + revocation_reason: + description: >- + When the mandate is revoked on the Bacs network this field displays + the reason for the revocation. + enum: + - account_closed + - bank_account_restricted + - bank_ownership_changed + - could_not_process + - debit_not_authorized + nullable: true + type: string + url: + description: The URL that will contain the mandate that the customer has signed. + maxLength: 5000 + type: string + required: + - network_status + - reference + - url + title: mandate_bacs_debit + type: object + x-expandableFields: [] + mandate_cashapp: + description: '' + properties: {} + title: mandate_cashapp + type: object + x-expandableFields: [] + mandate_kakao_pay: + description: '' + properties: {} + title: mandate_kakao_pay + type: object + x-expandableFields: [] + mandate_kr_card: + description: '' + properties: {} + title: mandate_kr_card + type: object + x-expandableFields: [] + mandate_link: + description: '' + properties: {} + title: mandate_link + type: object + x-expandableFields: [] + mandate_multi_use: + description: '' + properties: {} + title: mandate_multi_use + type: object + x-expandableFields: [] + mandate_naver_pay: + description: '' + properties: {} + title: mandate_naver_pay + type: object + x-expandableFields: [] + mandate_nz_bank_account: + description: '' + properties: {} + title: mandate_nz_bank_account + type: object + x-expandableFields: [] + mandate_payment_method_details: + description: '' + properties: + acss_debit: + $ref: '#/components/schemas/mandate_acss_debit' + amazon_pay: + $ref: '#/components/schemas/mandate_amazon_pay' + au_becs_debit: + $ref: '#/components/schemas/mandate_au_becs_debit' + bacs_debit: + $ref: '#/components/schemas/mandate_bacs_debit' + card: + $ref: '#/components/schemas/card_mandate_payment_method_details' + cashapp: + $ref: '#/components/schemas/mandate_cashapp' + kakao_pay: + $ref: '#/components/schemas/mandate_kakao_pay' + kr_card: + $ref: '#/components/schemas/mandate_kr_card' + link: + $ref: '#/components/schemas/mandate_link' + naver_pay: + $ref: '#/components/schemas/mandate_naver_pay' + nz_bank_account: + $ref: '#/components/schemas/mandate_nz_bank_account' + paypal: + $ref: '#/components/schemas/mandate_paypal' + revolut_pay: + $ref: '#/components/schemas/mandate_revolut_pay' + sepa_debit: + $ref: '#/components/schemas/mandate_sepa_debit' + type: + description: >- + This mandate corresponds with a specific payment method type. The + `payment_method_details` includes an additional hash with the same + name and contains mandate information that's specific to that + payment method. + maxLength: 5000 + type: string + us_bank_account: + $ref: '#/components/schemas/mandate_us_bank_account' + required: + - type + title: mandate_payment_method_details + type: object + x-expandableFields: + - acss_debit + - amazon_pay + - au_becs_debit + - bacs_debit + - card + - cashapp + - kakao_pay + - kr_card + - link + - naver_pay + - nz_bank_account + - paypal + - revolut_pay + - sepa_debit + - us_bank_account + mandate_paypal: + description: '' + properties: + billing_agreement_id: + description: >- + The PayPal Billing Agreement ID (BAID). This is an ID generated by + PayPal which represents the mandate between the merchant and the + customer. + maxLength: 5000 + nullable: true + type: string + payer_id: + description: >- + PayPal account PayerID. This identifier uniquely identifies the + PayPal customer. + maxLength: 5000 + nullable: true + type: string + title: mandate_paypal + type: object + x-expandableFields: [] + mandate_revolut_pay: + description: '' + properties: {} + title: mandate_revolut_pay + type: object + x-expandableFields: [] + mandate_sepa_debit: + description: '' + properties: + reference: + description: The unique reference of the mandate. + maxLength: 5000 + type: string + url: + description: >- + The URL of the mandate. This URL generally contains sensitive + information about the customer and should be shared with them + exclusively. + maxLength: 5000 + type: string + required: + - reference + - url + title: mandate_sepa_debit + type: object + x-expandableFields: [] + mandate_single_use: + description: '' + properties: + amount: + description: The amount of the payment on a single use mandate. + type: integer + currency: + description: The currency of the payment on a single use mandate. + format: currency + type: string + required: + - amount + - currency + title: mandate_single_use + type: object + x-expandableFields: [] + mandate_us_bank_account: + description: '' + properties: + collection_method: + description: Mandate collection method + enum: + - paper + type: string + x-stripeBypassValidation: true + title: mandate_us_bank_account + type: object + x-expandableFields: [] + networks: + description: '' + properties: + available: + description: >- + All networks available for selection via + [payment_method_options.card.network](/api/payment_intents/confirm#confirm_payment_intent-payment_method_options-card-network). + items: + maxLength: 5000 + type: string + type: array + preferred: + description: >- + The preferred network for co-branded cards. Can be + `cartes_bancaires`, `mastercard`, `visa` or `invalid_preference` if + requested network is not valid for the card. + maxLength: 5000 + nullable: true + type: string + required: + - available + title: networks + type: object + x-expandableFields: [] + notification_event_data: + description: '' + properties: + object: + description: >- + Object containing the API resource relevant to the event. For + example, an `invoice.created` event will have a full [invoice + object](https://stripe.com/docs/api#invoice_object) as the value of + the object key. + type: object + previous_attributes: + description: >- + Object containing the names of the updated attributes and their + values prior to the event (only included in events of type + `*.updated`). If an array attribute has any updated elements, this + object contains the entire array. In Stripe API versions 2017-04-06 + or earlier, an updated array attribute in this object includes only + the updated array elements. + type: object + required: + - object + title: NotificationEventData + type: object + x-expandableFields: [] + notification_event_request: + description: '' + properties: + id: + description: >- + ID of the API request that caused the event. If null, the event was + automatic (e.g., Stripe's automatic subscription handling). Request + logs are available in the + [dashboard](https://dashboard.stripe.com/logs), but currently not in + the API. + maxLength: 5000 + nullable: true + type: string + idempotency_key: + description: >- + The idempotency key transmitted during the request, if any. *Note: + This property is populated only for events on or after May 23, + 2017*. + maxLength: 5000 + nullable: true + type: string + title: NotificationEventRequest + type: object + x-expandableFields: [] + offline_acceptance: + description: '' + properties: {} + title: offline_acceptance + type: object + x-expandableFields: [] + online_acceptance: + description: '' + properties: + ip_address: + description: The customer accepts the mandate from this IP address. + maxLength: 5000 + nullable: true + type: string + user_agent: + description: >- + The customer accepts the mandate using the user agent of the + browser. + maxLength: 5000 + nullable: true + type: string + title: online_acceptance + type: object + x-expandableFields: [] + outbound_payments_payment_method_details: + description: '' + properties: + billing_details: + $ref: '#/components/schemas/treasury_shared_resource_billing_details' + financial_account: + $ref: >- + #/components/schemas/outbound_payments_payment_method_details_financial_account + type: + description: The type of the payment method used in the OutboundPayment. + enum: + - financial_account + - us_bank_account + type: string + x-stripeBypassValidation: true + us_bank_account: + $ref: >- + #/components/schemas/outbound_payments_payment_method_details_us_bank_account + required: + - billing_details + - type + title: OutboundPaymentsPaymentMethodDetails + type: object + x-expandableFields: + - billing_details + - financial_account + - us_bank_account + outbound_payments_payment_method_details_financial_account: + description: '' + properties: + id: + description: Token of the FinancialAccount. + maxLength: 5000 + type: string + network: + description: The rails used to send funds. + enum: + - stripe + type: string + required: + - id + - network + title: outbound_payments_payment_method_details_financial_account + type: object + x-expandableFields: [] + outbound_payments_payment_method_details_us_bank_account: + description: '' + properties: + account_holder_type: + description: 'Account holder type: individual or company.' + enum: + - company + - individual + nullable: true + type: string + account_type: + description: 'Account type: checkings or savings. Defaults to checking if omitted.' + enum: + - checking + - savings + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: ID of the mandate used to make this payment. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + network: + description: >- + The network rails used. See the + [docs](https://stripe.com/docs/treasury/money-movement/timelines) to + learn more about money movement timelines for each network type. + enum: + - ach + - us_domestic_wire + type: string + routing_number: + description: Routing number of the bank account. + maxLength: 5000 + nullable: true + type: string + required: + - network + title: outbound_payments_payment_method_details_us_bank_account + type: object + x-expandableFields: + - mandate + outbound_transfers_payment_method_details: + description: '' + properties: + billing_details: + $ref: '#/components/schemas/treasury_shared_resource_billing_details' + financial_account: + $ref: >- + #/components/schemas/outbound_transfers_payment_method_details_financial_account + type: + description: The type of the payment method used in the OutboundTransfer. + enum: + - financial_account + - us_bank_account + type: string + us_bank_account: + $ref: >- + #/components/schemas/outbound_transfers_payment_method_details_us_bank_account + required: + - billing_details + - type + title: OutboundTransfersPaymentMethodDetails + type: object + x-expandableFields: + - billing_details + - financial_account + - us_bank_account + outbound_transfers_payment_method_details_financial_account: + description: '' + properties: + id: + description: Token of the FinancialAccount. + maxLength: 5000 + type: string + network: + description: The rails used to send funds. + enum: + - stripe + type: string + required: + - id + - network + title: outbound_transfers_payment_method_details_financial_account + type: object + x-expandableFields: [] + outbound_transfers_payment_method_details_us_bank_account: + description: '' + properties: + account_holder_type: + description: 'Account holder type: individual or company.' + enum: + - company + - individual + nullable: true + type: string + account_type: + description: 'Account type: checkings or savings. Defaults to checking if omitted.' + enum: + - checking + - savings + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: ID of the mandate used to make this payment. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + network: + description: >- + The network rails used. See the + [docs](https://stripe.com/docs/treasury/money-movement/timelines) to + learn more about money movement timelines for each network type. + enum: + - ach + - us_domestic_wire + type: string + routing_number: + description: Routing number of the bank account. + maxLength: 5000 + nullable: true + type: string + required: + - network + title: outbound_transfers_payment_method_details_us_bank_account + type: object + x-expandableFields: + - mandate + package_dimensions: + description: '' + properties: + height: + description: 'Height, in inches.' + type: number + length: + description: 'Length, in inches.' + type: number + weight: + description: 'Weight, in ounces.' + type: number + width: + description: 'Width, in inches.' + type: number + required: + - height + - length + - weight + - width + title: PackageDimensions + type: object + x-expandableFields: [] + payment_flows_amount_details: + description: '' + properties: + tip: + $ref: >- + #/components/schemas/payment_flows_amount_details_client_resource_tip + title: PaymentFlowsAmountDetails + type: object + x-expandableFields: + - tip + payment_flows_amount_details_client: + description: '' + properties: + tip: + $ref: >- + #/components/schemas/payment_flows_amount_details_client_resource_tip + title: PaymentFlowsAmountDetailsClient + type: object + x-expandableFields: + - tip + payment_flows_amount_details_client_resource_tip: + description: '' + properties: + amount: + description: Portion of the amount that corresponds to a tip. + type: integer + title: PaymentFlowsAmountDetailsClientResourceTip + type: object + x-expandableFields: [] + payment_flows_automatic_payment_methods_payment_intent: + description: '' + properties: + allow_redirects: + description: >- + Controls whether this PaymentIntent will accept redirect-based + payment methods. + + + Redirect-based payment methods may require your customer to be + redirected to a payment method's app or site for authentication or + additional steps. To + [confirm](https://stripe.com/docs/api/payment_intents/confirm) this + PaymentIntent, you may be required to provide a `return_url` to + redirect customers back to your site after they authenticate or + complete the payment. + enum: + - always + - never + type: string + enabled: + description: Automatically calculates compatible payment methods + type: boolean + required: + - enabled + title: PaymentFlowsAutomaticPaymentMethodsPaymentIntent + type: object + x-expandableFields: [] + payment_flows_automatic_payment_methods_setup_intent: + description: '' + properties: + allow_redirects: + description: >- + Controls whether this SetupIntent will accept redirect-based payment + methods. + + + Redirect-based payment methods may require your customer to be + redirected to a payment method's app or site for authentication or + additional steps. To + [confirm](https://stripe.com/docs/api/setup_intents/confirm) this + SetupIntent, you may be required to provide a `return_url` to + redirect customers back to your site after they authenticate or + complete the setup. + enum: + - always + - never + type: string + enabled: + description: Automatically calculates compatible payment methods + nullable: true + type: boolean + title: PaymentFlowsAutomaticPaymentMethodsSetupIntent + type: object + x-expandableFields: [] + payment_flows_installment_options: + description: '' + properties: + enabled: + type: boolean + plan: + $ref: '#/components/schemas/payment_method_details_card_installments_plan' + required: + - enabled + title: PaymentFlowsInstallmentOptions + type: object + x-expandableFields: + - plan + payment_flows_payment_intent_presentment_details: + description: '' + properties: + presentment_amount: + description: >- + Amount intended to be collected by this payment, denominated in + presentment_currency. + type: integer + presentment_currency: + description: Currency presented to the customer during payment. + maxLength: 5000 + type: string + required: + - presentment_amount + - presentment_currency + title: PaymentFlowsPaymentIntentPresentmentDetails + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_alipay: + description: '' + properties: {} + title: PaymentFlowsPrivatePaymentMethodsAlipay + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_alipay_details: + description: '' + properties: + buyer_id: + description: >- + Uniquely identifies this particular Alipay account. You can use this + attribute to check whether two Alipay accounts are the same. + maxLength: 5000 + type: string + fingerprint: + description: >- + Uniquely identifies this particular Alipay account. You can use this + attribute to check whether two Alipay accounts are the same. + maxLength: 5000 + nullable: true + type: string + transaction_id: + description: Transaction ID of this particular Alipay transaction. + maxLength: 5000 + nullable: true + type: string + title: PaymentFlowsPrivatePaymentMethodsAlipayDetails + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_card_details_api_resource_enterprise_features_extended_authorization_extended_authorization: + description: '' + properties: + status: + description: >- + Indicates whether or not the capture window is extended beyond the + standard authorization. + enum: + - disabled + - enabled + type: string + required: + - status + title: >- + PaymentFlowsPrivatePaymentMethodsCardDetailsAPIResourceEnterpriseFeaturesExtendedAuthorizationExtendedAuthorization + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_card_details_api_resource_enterprise_features_incremental_authorization_incremental_authorization: + description: '' + properties: + status: + description: >- + Indicates whether or not the incremental authorization feature is + supported. + enum: + - available + - unavailable + type: string + required: + - status + title: >- + PaymentFlowsPrivatePaymentMethodsCardDetailsAPIResourceEnterpriseFeaturesIncrementalAuthorizationIncrementalAuthorization + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_card_details_api_resource_enterprise_features_overcapture_overcapture: + description: '' + properties: + maximum_amount_capturable: + description: The maximum amount that can be captured. + type: integer + status: + description: Indicates whether or not the authorized amount can be over-captured. + enum: + - available + - unavailable + type: string + required: + - maximum_amount_capturable + - status + title: >- + PaymentFlowsPrivatePaymentMethodsCardDetailsAPIResourceEnterpriseFeaturesOvercaptureOvercapture + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_card_details_api_resource_multicapture: + description: '' + properties: + status: + description: Indicates whether or not multiple captures are supported. + enum: + - available + - unavailable + type: string + required: + - status + title: PaymentFlowsPrivatePaymentMethodsCardDetailsAPIResourceMulticapture + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_card_present_common_wallet: + description: '' + properties: + type: + description: >- + The type of mobile wallet, one of `apple_pay`, `google_pay`, + `samsung_pay`, or `unknown`. + enum: + - apple_pay + - google_pay + - samsung_pay + - unknown + type: string + required: + - type + title: PaymentFlowsPrivatePaymentMethodsCardPresentCommonWallet + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_financial_connections_common_linked_account_options_filters: + description: '' + properties: + account_subcategories: + description: >- + The account subcategories to use to filter for possible accounts to + link. Valid subcategories are `checking` and `savings`. + items: + enum: + - checking + - savings + type: string + type: array + title: >- + PaymentFlowsPrivatePaymentMethodsFinancialConnectionsCommonLinkedAccountOptionsFilters + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_kakao_pay_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: PaymentFlowsPrivatePaymentMethodsKakaoPayPaymentMethodOptions + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_klarna_dob: + description: '' + properties: + day: + description: 'The day of birth, between 1 and 31.' + nullable: true + type: integer + month: + description: 'The month of birth, between 1 and 12.' + nullable: true + type: integer + year: + description: The four-digit year of birth. + nullable: true + type: integer + title: PaymentFlowsPrivatePaymentMethodsKlarnaDOB + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_naver_pay_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: PaymentFlowsPrivatePaymentMethodsNaverPayPaymentMethodOptions + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_payco_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + title: PaymentFlowsPrivatePaymentMethodsPaycoPaymentMethodOptions + type: object + x-expandableFields: [] + payment_flows_private_payment_methods_samsung_pay_payment_method_options: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + title: PaymentFlowsPrivatePaymentMethodsSamsungPayPaymentMethodOptions + type: object + x-expandableFields: [] + payment_intent: + description: >- + A PaymentIntent guides you through the process of collecting a payment + from your customer. + + We recommend that you create exactly one PaymentIntent for each order or + + customer session in your system. You can reference the PaymentIntent + later to + + see the history of payment attempts for a particular session. + + + A PaymentIntent transitions through + + [multiple + statuses](https://stripe.com/docs/payments/intents#intent-statuses) + + throughout its lifetime as it interfaces with Stripe.js to perform + + authentication flows and ultimately creates at most one successful + charge. + + + Related guide: [Payment Intents + API](https://stripe.com/docs/payments/payment-intents) + properties: + amount: + description: >- + Amount intended to be collected by this PaymentIntent. A positive + integer representing how much to charge in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal) (e.g., 100 + cents to charge $1.00 or 100 to charge ¥100, a zero-decimal + currency). The minimum amount is $0.50 US or [equivalent in charge + currency](https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts). + The amount value supports up to eight digits (e.g., a value of + 99999999 for a USD charge of $999,999.99). + type: integer + amount_capturable: + description: Amount that can be captured from this PaymentIntent. + type: integer + amount_details: + anyOf: + - $ref: '#/components/schemas/payment_flows_amount_details' + - $ref: '#/components/schemas/payment_flows_amount_details_client' + amount_received: + description: Amount that this PaymentIntent collects. + type: integer + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + description: ID of the Connect application that created the PaymentIntent. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + application_fee_amount: + description: >- + The amount of the application fee (if any) that will be requested to + be applied to the payment and transferred to the application owner's + Stripe account. The amount of the application fee collected will be + capped at the total amount captured. For more information, see the + PaymentIntents [use case for connected + accounts](https://stripe.com/docs/payments/connected-accounts). + nullable: true + type: integer + automatic_payment_methods: + anyOf: + - $ref: >- + #/components/schemas/payment_flows_automatic_payment_methods_payment_intent + description: >- + Settings to configure compatible payment methods from the [Stripe + Dashboard](https://dashboard.stripe.com/settings/payment_methods) + nullable: true + canceled_at: + description: >- + Populated when `status` is `canceled`, this is the time at which the + PaymentIntent was canceled. Measured in seconds since the Unix + epoch. + format: unix-time + nullable: true + type: integer + cancellation_reason: + description: >- + Reason for cancellation of this PaymentIntent, either user-provided + (`duplicate`, `fraudulent`, `requested_by_customer`, or `abandoned`) + or generated by Stripe internally (`failed_invoice`, `void_invoice`, + `automatic`, or `expired`). + enum: + - abandoned + - automatic + - duplicate + - expired + - failed_invoice + - fraudulent + - requested_by_customer + - void_invoice + nullable: true + type: string + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - automatic + - automatic_async + - manual + type: string + client_secret: + description: >- + The client secret of this PaymentIntent. Used for client-side + retrieval using a publishable key. + + + The client secret can be used to complete a payment from your + frontend. It should not be stored, logged, or exposed to anyone + other than the customer. Make sure that you have TLS enabled on any + page that includes the client secret. + + + Refer to our docs to [accept a + payment](https://stripe.com/docs/payments/accept-a-payment?ui=elements) + and learn about how `client_secret` should be handled. + maxLength: 5000 + nullable: true + type: string + confirmation_method: + description: >- + Describes whether we can confirm this PaymentIntent automatically, + or if it requires customer action to confirm the payment. + enum: + - automatic + - manual + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: >- + ID of the Customer this PaymentIntent belongs to, if one exists. + + + Payment methods attached to other Customers cannot be used with this + PaymentIntent. + + + If + [setup_future_usage](https://stripe.com/docs/api#payment_intent_object-setup_future_usage) + is set and this PaymentIntent's payment method is not + `card_present`, then the payment method attaches to the Customer + after the PaymentIntent has been confirmed and any required actions + from the user are complete. If the payment method is `card_present` + and isn't a digital wallet, then a + [generated_card](https://docs.stripe.com/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card is created and attached to the + Customer instead. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + last_payment_error: + anyOf: + - $ref: '#/components/schemas/api_errors' + description: >- + The payment error encountered in the previous PaymentIntent + confirmation. It will be cleared if the PaymentIntent is later + updated for any reason. + nullable: true + latest_charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: >- + ID of the latest [Charge + object](https://stripe.com/docs/api/charges) created by this + PaymentIntent. This property is `null` until PaymentIntent + confirmation is attempted. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + Learn more about [storing information in + metadata](https://stripe.com/docs/payments/payment-intents/creating-payment-intents#storing-information-in-metadata). + type: object + next_action: + anyOf: + - $ref: '#/components/schemas/payment_intent_next_action' + description: >- + If present, this property tells you what actions you need to take in + order for your customer to fulfill a payment using the provided + source. + nullable: true + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - payment_intent + type: string + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account (if any) for which the funds of the PaymentIntent are + intended. See the PaymentIntents [use case for connected + accounts](https://stripe.com/docs/payments/connected-accounts) for + details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: ID of the payment method used in this PaymentIntent. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + payment_method_configuration_details: + anyOf: + - $ref: >- + #/components/schemas/payment_method_config_biz_payment_method_configuration_details + description: >- + Information about the [payment method + configuration](https://stripe.com/docs/api/payment_method_configurations) + used for this PaymentIntent. + nullable: true + payment_method_options: + anyOf: + - $ref: '#/components/schemas/payment_intent_payment_method_options' + description: Payment-method-specific configuration for this PaymentIntent. + nullable: true + payment_method_types: + description: >- + The list of payment method types (e.g. card) that this PaymentIntent + is allowed to use. A comprehensive list of valid payment method + types can be found + [here](https://docs.stripe.com/api/payment_methods/object#payment_method_object-type). + items: + maxLength: 5000 + type: string + type: array + presentment_details: + $ref: >- + #/components/schemas/payment_flows_payment_intent_presentment_details + processing: + anyOf: + - $ref: '#/components/schemas/payment_intent_processing' + description: >- + If present, this property tells you about the processing state of + the payment. + nullable: true + receipt_email: + description: >- + Email address that the receipt for the resulting payment will be + sent to. If `receipt_email` is specified for a payment in live mode, + a receipt will be sent regardless of your [email + settings](https://dashboard.stripe.com/account/emails). + maxLength: 5000 + nullable: true + type: string + review: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/review' + description: 'ID of the review associated with this PaymentIntent, if any.' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/review' + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - off_session + - on_session + nullable: true + type: string + shipping: + anyOf: + - $ref: '#/components/schemas/shipping' + description: Shipping information for this PaymentIntent. + nullable: true + statement_descriptor: + description: >- + Text that appears on the customer's statement as the statement + descriptor for a non-card charge. This value overrides the account's + default statement descriptor. For information about requirements, + including the 22-character limit, see [the Statement Descriptor + docs](https://docs.stripe.com/get-started/account/statement-descriptors). + + + Setting this value for a card charge returns an error. For card + charges, set the + [statement_descriptor_suffix](https://docs.stripe.com/get-started/account/statement-descriptors#dynamic) + instead. + maxLength: 5000 + nullable: true + type: string + statement_descriptor_suffix: + description: >- + Provides information about a card charge. Concatenated to the + account's [statement descriptor + prefix](https://docs.stripe.com/get-started/account/statement-descriptors#static) + to form the complete statement descriptor that appears on the + customer's statement. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + Status of this PaymentIntent, one of `requires_payment_method`, + `requires_confirmation`, `requires_action`, `processing`, + `requires_capture`, `canceled`, or `succeeded`. Read more about each + PaymentIntent + [status](https://stripe.com/docs/payments/intents#intent-statuses). + enum: + - canceled + - processing + - requires_action + - requires_capture + - requires_confirmation + - requires_payment_method + - succeeded + type: string + transfer_data: + anyOf: + - $ref: '#/components/schemas/transfer_data' + description: >- + The data that automatically creates a Transfer after the payment + finalizes. Learn more about the [use case for connected + accounts](https://stripe.com/docs/payments/connected-accounts). + nullable: true + transfer_group: + description: >- + A string that identifies the resulting payment as part of a group. + Learn more about the [use case for connected + accounts](https://stripe.com/docs/connect/separate-charges-and-transfers). + maxLength: 5000 + nullable: true + type: string + required: + - amount + - capture_method + - confirmation_method + - created + - currency + - id + - livemode + - object + - payment_method_types + - status + title: PaymentIntent + type: object + x-expandableFields: + - amount_details + - application + - automatic_payment_methods + - customer + - last_payment_error + - latest_charge + - next_action + - on_behalf_of + - payment_method + - payment_method_configuration_details + - payment_method_options + - presentment_details + - processing + - review + - shipping + - transfer_data + x-resourceId: payment_intent + payment_intent_card_processing: + description: '' + properties: + customer_notification: + $ref: '#/components/schemas/payment_intent_processing_customer_notification' + title: PaymentIntentCardProcessing + type: object + x-expandableFields: + - customer_notification + payment_intent_next_action: + description: '' + properties: + alipay_handle_redirect: + $ref: >- + #/components/schemas/payment_intent_next_action_alipay_handle_redirect + boleto_display_details: + $ref: '#/components/schemas/payment_intent_next_action_boleto' + card_await_notification: + $ref: >- + #/components/schemas/payment_intent_next_action_card_await_notification + cashapp_handle_redirect_or_display_qr_code: + $ref: >- + #/components/schemas/payment_intent_next_action_cashapp_handle_redirect_or_display_qr_code + display_bank_transfer_instructions: + $ref: >- + #/components/schemas/payment_intent_next_action_display_bank_transfer_instructions + konbini_display_details: + $ref: '#/components/schemas/payment_intent_next_action_konbini' + multibanco_display_details: + $ref: >- + #/components/schemas/payment_intent_next_action_display_multibanco_details + oxxo_display_details: + $ref: '#/components/schemas/payment_intent_next_action_display_oxxo_details' + paynow_display_qr_code: + $ref: >- + #/components/schemas/payment_intent_next_action_paynow_display_qr_code + pix_display_qr_code: + $ref: '#/components/schemas/payment_intent_next_action_pix_display_qr_code' + promptpay_display_qr_code: + $ref: >- + #/components/schemas/payment_intent_next_action_promptpay_display_qr_code + redirect_to_url: + $ref: '#/components/schemas/payment_intent_next_action_redirect_to_url' + swish_handle_redirect_or_display_qr_code: + $ref: >- + #/components/schemas/payment_intent_next_action_swish_handle_redirect_or_display_qr_code + type: + description: >- + Type of the next action to perform. Refer to the other child + attributes under `next_action` for available values. Examples + include: `redirect_to_url`, `use_stripe_sdk`, + `alipay_handle_redirect`, `oxxo_display_details`, or + `verify_with_microdeposits`. + maxLength: 5000 + type: string + use_stripe_sdk: + description: >- + When confirming a PaymentIntent with Stripe.js, Stripe.js depends on + the contents of this dictionary to invoke authentication flows. The + shape of the contents is subject to change and is only intended to + be used by Stripe.js. + type: object + verify_with_microdeposits: + $ref: >- + #/components/schemas/payment_intent_next_action_verify_with_microdeposits + wechat_pay_display_qr_code: + $ref: >- + #/components/schemas/payment_intent_next_action_wechat_pay_display_qr_code + wechat_pay_redirect_to_android_app: + $ref: >- + #/components/schemas/payment_intent_next_action_wechat_pay_redirect_to_android_app + wechat_pay_redirect_to_ios_app: + $ref: >- + #/components/schemas/payment_intent_next_action_wechat_pay_redirect_to_ios_app + required: + - type + title: PaymentIntentNextAction + type: object + x-expandableFields: + - alipay_handle_redirect + - boleto_display_details + - card_await_notification + - cashapp_handle_redirect_or_display_qr_code + - display_bank_transfer_instructions + - konbini_display_details + - multibanco_display_details + - oxxo_display_details + - paynow_display_qr_code + - pix_display_qr_code + - promptpay_display_qr_code + - redirect_to_url + - swish_handle_redirect_or_display_qr_code + - verify_with_microdeposits + - wechat_pay_display_qr_code + - wechat_pay_redirect_to_android_app + - wechat_pay_redirect_to_ios_app + payment_intent_next_action_alipay_handle_redirect: + description: '' + properties: + native_data: + description: >- + The native data to be used with Alipay SDK you must redirect your + customer to in order to authenticate the payment in an Android App. + maxLength: 5000 + nullable: true + type: string + native_url: + description: >- + The native URL you must redirect your customer to in order to + authenticate the payment in an iOS App. + maxLength: 5000 + nullable: true + type: string + return_url: + description: >- + If the customer does not exit their browser while authenticating, + they will be redirected to this specified URL after completion. + maxLength: 5000 + nullable: true + type: string + url: + description: >- + The URL you must redirect your customer to in order to authenticate + the payment. + maxLength: 5000 + nullable: true + type: string + title: PaymentIntentNextActionAlipayHandleRedirect + type: object + x-expandableFields: [] + payment_intent_next_action_boleto: + description: '' + properties: + expires_at: + description: The timestamp after which the boleto expires. + format: unix-time + nullable: true + type: integer + hosted_voucher_url: + description: >- + The URL to the hosted boleto voucher page, which allows customers to + view the boleto voucher. + maxLength: 5000 + nullable: true + type: string + number: + description: The boleto number. + maxLength: 5000 + nullable: true + type: string + pdf: + description: The URL to the downloadable boleto voucher PDF. + maxLength: 5000 + nullable: true + type: string + title: payment_intent_next_action_boleto + type: object + x-expandableFields: [] + payment_intent_next_action_card_await_notification: + description: '' + properties: + charge_attempt_at: + description: >- + The time that payment will be attempted. If customer approval is + required, they need to provide approval before this time. + format: unix-time + nullable: true + type: integer + customer_approval_required: + description: >- + For payments greater than INR 15000, the customer must provide + explicit approval of the payment with their bank. For payments of + lower amount, no customer action is required. + nullable: true + type: boolean + title: PaymentIntentNextActionCardAwaitNotification + type: object + x-expandableFields: [] + payment_intent_next_action_cashapp_handle_redirect_or_display_qr_code: + description: '' + properties: + hosted_instructions_url: + description: >- + The URL to the hosted Cash App Pay instructions page, which allows + customers to view the QR code, and supports QR code refreshing on + expiration. + maxLength: 5000 + type: string + mobile_auth_url: + description: The url for mobile redirect based auth + maxLength: 5000 + type: string + qr_code: + $ref: '#/components/schemas/payment_intent_next_action_cashapp_qr_code' + required: + - hosted_instructions_url + - mobile_auth_url + - qr_code + title: PaymentIntentNextActionCashappHandleRedirectOrDisplayQrCode + type: object + x-expandableFields: + - qr_code + payment_intent_next_action_cashapp_qr_code: + description: '' + properties: + expires_at: + description: The date (unix timestamp) when the QR code expires. + format: unix-time + type: integer + image_url_png: + description: The image_url_png string used to render QR code + maxLength: 5000 + type: string + image_url_svg: + description: The image_url_svg string used to render QR code + maxLength: 5000 + type: string + required: + - expires_at + - image_url_png + - image_url_svg + title: PaymentIntentNextActionCashappQRCode + type: object + x-expandableFields: [] + payment_intent_next_action_display_bank_transfer_instructions: + description: '' + properties: + amount_remaining: + description: >- + The remaining amount that needs to be transferred to complete the + payment. + nullable: true + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + nullable: true + type: string + financial_addresses: + description: >- + A list of financial addresses that can be used to fund the customer + balance + items: + $ref: >- + #/components/schemas/funding_instructions_bank_transfer_financial_address + type: array + hosted_instructions_url: + description: >- + A link to a hosted page that guides your customer through completing + the transfer. + maxLength: 5000 + nullable: true + type: string + reference: + description: >- + A string identifying this payment. Instruct your customer to include + this code in the reference or memo field of their bank transfer. + maxLength: 5000 + nullable: true + type: string + type: + description: Type of bank transfer + enum: + - eu_bank_transfer + - gb_bank_transfer + - jp_bank_transfer + - mx_bank_transfer + - us_bank_transfer + type: string + x-stripeBypassValidation: true + required: + - type + title: PaymentIntentNextActionDisplayBankTransferInstructions + type: object + x-expandableFields: + - financial_addresses + payment_intent_next_action_display_multibanco_details: + description: '' + properties: + entity: + description: Entity number associated with this Multibanco payment. + maxLength: 5000 + nullable: true + type: string + expires_at: + description: The timestamp at which the Multibanco voucher expires. + format: unix-time + nullable: true + type: integer + hosted_voucher_url: + description: >- + The URL for the hosted Multibanco voucher page, which allows + customers to view a Multibanco voucher. + maxLength: 5000 + nullable: true + type: string + reference: + description: Reference number associated with this Multibanco payment. + maxLength: 5000 + nullable: true + type: string + title: PaymentIntentNextActionDisplayMultibancoDetails + type: object + x-expandableFields: [] + payment_intent_next_action_display_oxxo_details: + description: '' + properties: + expires_after: + description: The timestamp after which the OXXO voucher expires. + format: unix-time + nullable: true + type: integer + hosted_voucher_url: + description: >- + The URL for the hosted OXXO voucher page, which allows customers to + view and print an OXXO voucher. + maxLength: 5000 + nullable: true + type: string + number: + description: OXXO reference number. + maxLength: 5000 + nullable: true + type: string + title: PaymentIntentNextActionDisplayOxxoDetails + type: object + x-expandableFields: [] + payment_intent_next_action_konbini: + description: '' + properties: + expires_at: + description: The timestamp at which the pending Konbini payment expires. + format: unix-time + type: integer + hosted_voucher_url: + description: >- + The URL for the Konbini payment instructions page, which allows + customers to view and print a Konbini voucher. + maxLength: 5000 + nullable: true + type: string + stores: + $ref: '#/components/schemas/payment_intent_next_action_konbini_stores' + required: + - expires_at + - stores + title: payment_intent_next_action_konbini + type: object + x-expandableFields: + - stores + payment_intent_next_action_konbini_familymart: + description: '' + properties: + confirmation_number: + description: The confirmation number. + maxLength: 5000 + type: string + payment_code: + description: The payment code. + maxLength: 5000 + type: string + required: + - payment_code + title: payment_intent_next_action_konbini_familymart + type: object + x-expandableFields: [] + payment_intent_next_action_konbini_lawson: + description: '' + properties: + confirmation_number: + description: The confirmation number. + maxLength: 5000 + type: string + payment_code: + description: The payment code. + maxLength: 5000 + type: string + required: + - payment_code + title: payment_intent_next_action_konbini_lawson + type: object + x-expandableFields: [] + payment_intent_next_action_konbini_ministop: + description: '' + properties: + confirmation_number: + description: The confirmation number. + maxLength: 5000 + type: string + payment_code: + description: The payment code. + maxLength: 5000 + type: string + required: + - payment_code + title: payment_intent_next_action_konbini_ministop + type: object + x-expandableFields: [] + payment_intent_next_action_konbini_seicomart: + description: '' + properties: + confirmation_number: + description: The confirmation number. + maxLength: 5000 + type: string + payment_code: + description: The payment code. + maxLength: 5000 + type: string + required: + - payment_code + title: payment_intent_next_action_konbini_seicomart + type: object + x-expandableFields: [] + payment_intent_next_action_konbini_stores: + description: '' + properties: + familymart: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_next_action_konbini_familymart + description: FamilyMart instruction details. + nullable: true + lawson: + anyOf: + - $ref: '#/components/schemas/payment_intent_next_action_konbini_lawson' + description: Lawson instruction details. + nullable: true + ministop: + anyOf: + - $ref: '#/components/schemas/payment_intent_next_action_konbini_ministop' + description: Ministop instruction details. + nullable: true + seicomart: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_next_action_konbini_seicomart + description: Seicomart instruction details. + nullable: true + title: payment_intent_next_action_konbini_stores + type: object + x-expandableFields: + - familymart + - lawson + - ministop + - seicomart + payment_intent_next_action_paynow_display_qr_code: + description: '' + properties: + data: + description: >- + The raw data string used to generate QR code, it should be used + together with QR code library. + maxLength: 5000 + type: string + hosted_instructions_url: + description: >- + The URL to the hosted PayNow instructions page, which allows + customers to view the PayNow QR code. + maxLength: 5000 + nullable: true + type: string + image_url_png: + description: The image_url_png string used to render QR code + maxLength: 5000 + type: string + image_url_svg: + description: The image_url_svg string used to render QR code + maxLength: 5000 + type: string + required: + - data + - image_url_png + - image_url_svg + title: PaymentIntentNextActionPaynowDisplayQrCode + type: object + x-expandableFields: [] + payment_intent_next_action_pix_display_qr_code: + description: '' + properties: + data: + description: >- + The raw data string used to generate QR code, it should be used + together with QR code library. + maxLength: 5000 + type: string + expires_at: + description: The date (unix timestamp) when the PIX expires. + type: integer + hosted_instructions_url: + description: >- + The URL to the hosted pix instructions page, which allows customers + to view the pix QR code. + maxLength: 5000 + type: string + image_url_png: + description: The image_url_png string used to render png QR code + maxLength: 5000 + type: string + image_url_svg: + description: The image_url_svg string used to render svg QR code + maxLength: 5000 + type: string + title: PaymentIntentNextActionPixDisplayQrCode + type: object + x-expandableFields: [] + payment_intent_next_action_promptpay_display_qr_code: + description: '' + properties: + data: + description: >- + The raw data string used to generate QR code, it should be used + together with QR code library. + maxLength: 5000 + type: string + hosted_instructions_url: + description: >- + The URL to the hosted PromptPay instructions page, which allows + customers to view the PromptPay QR code. + maxLength: 5000 + type: string + image_url_png: + description: >- + The PNG path used to render the QR code, can be used as the source + in an HTML img tag + maxLength: 5000 + type: string + image_url_svg: + description: >- + The SVG path used to render the QR code, can be used as the source + in an HTML img tag + maxLength: 5000 + type: string + required: + - data + - hosted_instructions_url + - image_url_png + - image_url_svg + title: PaymentIntentNextActionPromptpayDisplayQrCode + type: object + x-expandableFields: [] + payment_intent_next_action_redirect_to_url: + description: '' + properties: + return_url: + description: >- + If the customer does not exit their browser while authenticating, + they will be redirected to this specified URL after completion. + maxLength: 5000 + nullable: true + type: string + url: + description: >- + The URL you must redirect your customer to in order to authenticate + the payment. + maxLength: 5000 + nullable: true + type: string + title: PaymentIntentNextActionRedirectToUrl + type: object + x-expandableFields: [] + payment_intent_next_action_swish_handle_redirect_or_display_qr_code: + description: '' + properties: + hosted_instructions_url: + description: >- + The URL to the hosted Swish instructions page, which allows + customers to view the QR code. + maxLength: 5000 + type: string + qr_code: + $ref: '#/components/schemas/payment_intent_next_action_swish_qr_code' + required: + - hosted_instructions_url + - qr_code + title: PaymentIntentNextActionSwishHandleRedirectOrDisplayQrCode + type: object + x-expandableFields: + - qr_code + payment_intent_next_action_swish_qr_code: + description: '' + properties: + data: + description: >- + The raw data string used to generate QR code, it should be used + together with QR code library. + maxLength: 5000 + type: string + image_url_png: + description: The image_url_png string used to render QR code + maxLength: 5000 + type: string + image_url_svg: + description: The image_url_svg string used to render QR code + maxLength: 5000 + type: string + required: + - data + - image_url_png + - image_url_svg + title: PaymentIntentNextActionSwishQRCode + type: object + x-expandableFields: [] + payment_intent_next_action_verify_with_microdeposits: + description: '' + properties: + arrival_date: + description: The timestamp when the microdeposits are expected to land. + format: unix-time + type: integer + hosted_verification_url: + description: >- + The URL for the hosted verification page, which allows customers to + verify their bank account. + maxLength: 5000 + type: string + microdeposit_type: + description: >- + The type of the microdeposit sent to the customer. Used to + distinguish between different verification methods. + enum: + - amounts + - descriptor_code + nullable: true + type: string + required: + - arrival_date + - hosted_verification_url + title: PaymentIntentNextActionVerifyWithMicrodeposits + type: object + x-expandableFields: [] + payment_intent_next_action_wechat_pay_display_qr_code: + description: '' + properties: + data: + description: The data being used to generate QR code + maxLength: 5000 + type: string + hosted_instructions_url: + description: >- + The URL to the hosted WeChat Pay instructions page, which allows + customers to view the WeChat Pay QR code. + maxLength: 5000 + type: string + image_data_url: + description: The base64 image data for a pre-generated QR code + maxLength: 5000 + type: string + image_url_png: + description: The image_url_png string used to render QR code + maxLength: 5000 + type: string + image_url_svg: + description: The image_url_svg string used to render QR code + maxLength: 5000 + type: string + required: + - data + - hosted_instructions_url + - image_data_url + - image_url_png + - image_url_svg + title: PaymentIntentNextActionWechatPayDisplayQrCode + type: object + x-expandableFields: [] + payment_intent_next_action_wechat_pay_redirect_to_android_app: + description: '' + properties: + app_id: + description: app_id is the APP ID registered on WeChat open platform + maxLength: 5000 + type: string + nonce_str: + description: nonce_str is a random string + maxLength: 5000 + type: string + package: + description: package is static value + maxLength: 5000 + type: string + partner_id: + description: an unique merchant ID assigned by WeChat Pay + maxLength: 5000 + type: string + prepay_id: + description: an unique trading ID assigned by WeChat Pay + maxLength: 5000 + type: string + sign: + description: A signature + maxLength: 5000 + type: string + timestamp: + description: Specifies the current time in epoch format + maxLength: 5000 + type: string + required: + - app_id + - nonce_str + - package + - partner_id + - prepay_id + - sign + - timestamp + title: PaymentIntentNextActionWechatPayRedirectToAndroidApp + type: object + x-expandableFields: [] + payment_intent_next_action_wechat_pay_redirect_to_ios_app: + description: '' + properties: + native_url: + description: An universal link that redirect to WeChat Pay app + maxLength: 5000 + type: string + required: + - native_url + title: PaymentIntentNextActionWechatPayRedirectToIOSApp + type: object + x-expandableFields: [] + payment_intent_payment_method_options: + description: '' + properties: + acss_debit: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_payment_method_options_acss_debit + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + affirm: + anyOf: + - $ref: '#/components/schemas/payment_method_options_affirm' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + afterpay_clearpay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_afterpay_clearpay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + alipay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_alipay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + alma: + anyOf: + - $ref: '#/components/schemas/payment_method_options_alma' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + amazon_pay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_amazon_pay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + au_becs_debit: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_payment_method_options_au_becs_debit + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + bacs_debit: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_payment_method_options_bacs_debit + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + bancontact: + anyOf: + - $ref: '#/components/schemas/payment_method_options_bancontact' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + billie: + anyOf: + - $ref: '#/components/schemas/payment_method_options_billie' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + blik: + anyOf: + - $ref: '#/components/schemas/payment_intent_payment_method_options_blik' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + boleto: + anyOf: + - $ref: '#/components/schemas/payment_method_options_boleto' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + card: + anyOf: + - $ref: '#/components/schemas/payment_intent_payment_method_options_card' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + card_present: + anyOf: + - $ref: '#/components/schemas/payment_method_options_card_present' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + cashapp: + anyOf: + - $ref: '#/components/schemas/payment_method_options_cashapp' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + customer_balance: + anyOf: + - $ref: '#/components/schemas/payment_method_options_customer_balance' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + eps: + anyOf: + - $ref: '#/components/schemas/payment_intent_payment_method_options_eps' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + fpx: + anyOf: + - $ref: '#/components/schemas/payment_method_options_fpx' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + giropay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_giropay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + grabpay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_grabpay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + ideal: + anyOf: + - $ref: '#/components/schemas/payment_method_options_ideal' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + interac_present: + anyOf: + - $ref: '#/components/schemas/payment_method_options_interac_present' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + kakao_pay: + anyOf: + - $ref: >- + #/components/schemas/payment_flows_private_payment_methods_kakao_pay_payment_method_options + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + klarna: + anyOf: + - $ref: '#/components/schemas/payment_method_options_klarna' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + konbini: + anyOf: + - $ref: '#/components/schemas/payment_method_options_konbini' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + kr_card: + anyOf: + - $ref: '#/components/schemas/payment_method_options_kr_card' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + link: + anyOf: + - $ref: '#/components/schemas/payment_intent_payment_method_options_link' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + mobilepay: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_payment_method_options_mobilepay + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + multibanco: + anyOf: + - $ref: '#/components/schemas/payment_method_options_multibanco' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + naver_pay: + anyOf: + - $ref: >- + #/components/schemas/payment_flows_private_payment_methods_naver_pay_payment_method_options + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + nz_bank_account: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_payment_method_options_nz_bank_account + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + oxxo: + anyOf: + - $ref: '#/components/schemas/payment_method_options_oxxo' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + p24: + anyOf: + - $ref: '#/components/schemas/payment_method_options_p24' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + pay_by_bank: + anyOf: + - $ref: '#/components/schemas/payment_method_options_pay_by_bank' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + payco: + anyOf: + - $ref: >- + #/components/schemas/payment_flows_private_payment_methods_payco_payment_method_options + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + paynow: + anyOf: + - $ref: '#/components/schemas/payment_method_options_paynow' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + paypal: + anyOf: + - $ref: '#/components/schemas/payment_method_options_paypal' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + pix: + anyOf: + - $ref: '#/components/schemas/payment_method_options_pix' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + promptpay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_promptpay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + revolut_pay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_revolut_pay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + samsung_pay: + anyOf: + - $ref: >- + #/components/schemas/payment_flows_private_payment_methods_samsung_pay_payment_method_options + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + satispay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_satispay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + sepa_debit: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_payment_method_options_sepa_debit + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + sofort: + anyOf: + - $ref: '#/components/schemas/payment_method_options_sofort' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + swish: + anyOf: + - $ref: '#/components/schemas/payment_intent_payment_method_options_swish' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + twint: + anyOf: + - $ref: '#/components/schemas/payment_method_options_twint' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + us_bank_account: + anyOf: + - $ref: >- + #/components/schemas/payment_intent_payment_method_options_us_bank_account + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + wechat_pay: + anyOf: + - $ref: '#/components/schemas/payment_method_options_wechat_pay' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + zip: + anyOf: + - $ref: '#/components/schemas/payment_method_options_zip' + - $ref: >- + #/components/schemas/payment_intent_type_specific_payment_method_options_client + title: PaymentIntentPaymentMethodOptions + type: object + x-expandableFields: + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - billie + - blik + - boleto + - card + - card_present + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - interac_present + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - nz_bank_account + - oxxo + - p24 + - pay_by_bank + - payco + - paynow + - paypal + - pix + - promptpay + - revolut_pay + - samsung_pay + - satispay + - sepa_debit + - sofort + - swish + - twint + - us_bank_account + - wechat_pay + - zip + payment_intent_payment_method_options_acss_debit: + description: '' + properties: + mandate_options: + $ref: >- + #/components/schemas/payment_intent_payment_method_options_mandate_options_acss_debit + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: payment_intent_payment_method_options_acss_debit + type: object + x-expandableFields: + - mandate_options + payment_intent_payment_method_options_au_becs_debit: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + title: payment_intent_payment_method_options_au_becs_debit + type: object + x-expandableFields: [] + payment_intent_payment_method_options_bacs_debit: + description: '' + properties: + mandate_options: + $ref: >- + #/components/schemas/payment_intent_payment_method_options_mandate_options_bacs_debit + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + title: payment_intent_payment_method_options_bacs_debit + type: object + x-expandableFields: + - mandate_options + payment_intent_payment_method_options_blik: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + x-stripeBypassValidation: true + title: payment_intent_payment_method_options_blik + type: object + x-expandableFields: [] + payment_intent_payment_method_options_card: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + installments: + anyOf: + - $ref: '#/components/schemas/payment_method_options_card_installments' + description: >- + Installment details for this payment (Mexico only). + + + For more information, see the [installments integration + guide](https://stripe.com/docs/payments/installments). + nullable: true + mandate_options: + anyOf: + - $ref: '#/components/schemas/payment_method_options_card_mandate_options' + description: >- + Configuration options for setting up an eMandate for cards issued in + India. + nullable: true + network: + description: >- + Selected network to process this payment intent on. Depends on the + available networks of the card attached to the payment intent. Can + be only set confirm-time. + enum: + - amex + - cartes_bancaires + - diners + - discover + - eftpos_au + - girocard + - interac + - jcb + - link + - mastercard + - unionpay + - unknown + - visa + nullable: true + type: string + request_extended_authorization: + description: >- + Request ability to [capture beyond the standard authorization + validity + window](https://stripe.com/docs/payments/extended-authorization) for + this PaymentIntent. + enum: + - if_available + - never + type: string + request_incremental_authorization: + description: >- + Request ability to [increment the + authorization](https://stripe.com/docs/payments/incremental-authorization) + for this PaymentIntent. + enum: + - if_available + - never + type: string + request_multicapture: + description: >- + Request ability to make [multiple + captures](https://stripe.com/docs/payments/multicapture) for this + PaymentIntent. + enum: + - if_available + - never + type: string + request_overcapture: + description: >- + Request ability to + [overcapture](https://stripe.com/docs/payments/overcapture) for this + PaymentIntent. + enum: + - if_available + - never + type: string + request_three_d_secure: + description: >- + We strongly recommend that you rely on our SCA Engine to + automatically prompt your customers for authentication based on risk + level and [other + requirements](https://stripe.com/docs/strong-customer-authentication). + However, if you wish to request 3D Secure based on logic from your + own fraud engine, provide this option. If not provided, this value + defaults to `automatic`. Read our guide on [manually requesting 3D + Secure](https://stripe.com/docs/payments/3d-secure/authentication-flow#manual-three-ds) + for more information on how this configuration interacts with Radar + and our SCA Engine. + enum: + - any + - automatic + - challenge + nullable: true + type: string + x-stripeBypassValidation: true + require_cvc_recollection: + description: >- + When enabled, using a card that is attached to a customer will + require the CVC to be provided again (i.e. using the cvc_token + parameter). + type: boolean + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + statement_descriptor_suffix_kana: + description: >- + Provides information about a card payment that customers see on + their statements. Concatenated with the Kana prefix (shortened Kana + descriptor) or Kana statement descriptor that’s set on the account + to form the complete statement descriptor. Maximum 22 characters. On + card statements, the *concatenation* of both prefix and suffix + (including separators) will appear truncated to 22 characters. + maxLength: 5000 + type: string + statement_descriptor_suffix_kanji: + description: >- + Provides information about a card payment that customers see on + their statements. Concatenated with the Kanji prefix (shortened + Kanji descriptor) or Kanji statement descriptor that’s set on the + account to form the complete statement descriptor. Maximum 17 + characters. On card statements, the *concatenation* of both prefix + and suffix (including separators) will appear truncated to 17 + characters. + maxLength: 5000 + type: string + title: payment_intent_payment_method_options_card + type: object + x-expandableFields: + - installments + - mandate_options + payment_intent_payment_method_options_eps: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_intent_payment_method_options_eps + type: object + x-expandableFields: [] + payment_intent_payment_method_options_link: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: payment_intent_payment_method_options_link + type: object + x-expandableFields: [] + payment_intent_payment_method_options_mandate_options_acss_debit: + description: '' + properties: + custom_mandate_url: + description: A URL for custom mandate text + maxLength: 5000 + type: string + interval_description: + description: >- + Description of the interval. Only required if the 'payment_schedule' + parameter is 'interval' or 'combined'. + maxLength: 5000 + nullable: true + type: string + payment_schedule: + description: Payment schedule for the mandate. + enum: + - combined + - interval + - sporadic + nullable: true + type: string + transaction_type: + description: Transaction type of the mandate. + enum: + - business + - personal + nullable: true + type: string + title: payment_intent_payment_method_options_mandate_options_acss_debit + type: object + x-expandableFields: [] + payment_intent_payment_method_options_mandate_options_bacs_debit: + description: '' + properties: + reference_prefix: + description: >- + Prefix used to generate the Mandate reference. Must be at most 12 + characters long. Must consist of only uppercase letters, numbers, + spaces, or the following special characters: '/', '_', '-', '&', + '.'. Cannot begin with 'DDIC' or 'STRIPE'. + maxLength: 5000 + type: string + title: payment_intent_payment_method_options_mandate_options_bacs_debit + type: object + x-expandableFields: [] + payment_intent_payment_method_options_mandate_options_sepa_debit: + description: '' + properties: + reference_prefix: + description: >- + Prefix used to generate the Mandate reference. Must be at most 12 + characters long. Must consist of only uppercase letters, numbers, + spaces, or the following special characters: '/', '_', '-', '&', + '.'. Cannot begin with 'STRIPE'. + maxLength: 5000 + type: string + title: payment_intent_payment_method_options_mandate_options_sepa_debit + type: object + x-expandableFields: [] + payment_intent_payment_method_options_mobilepay: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_intent_payment_method_options_mobilepay + type: object + x-expandableFields: [] + payment_intent_payment_method_options_nz_bank_account: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + title: payment_intent_payment_method_options_nz_bank_account + type: object + x-expandableFields: [] + payment_intent_payment_method_options_sepa_debit: + description: '' + properties: + mandate_options: + $ref: >- + #/components/schemas/payment_intent_payment_method_options_mandate_options_sepa_debit + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + title: payment_intent_payment_method_options_sepa_debit + type: object + x-expandableFields: + - mandate_options + payment_intent_payment_method_options_swish: + description: '' + properties: + reference: + description: A reference for this payment to be displayed in the Swish app. + maxLength: 35 + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_intent_payment_method_options_swish + type: object + x-expandableFields: [] + payment_intent_payment_method_options_us_bank_account: + description: '' + properties: + financial_connections: + $ref: '#/components/schemas/linked_account_options_common' + mandate_options: + $ref: >- + #/components/schemas/payment_method_options_us_bank_account_mandate_options + preferred_settlement_speed: + description: Preferred transaction settlement speed + enum: + - fastest + - standard + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + target_date: + description: >- + Controls when Stripe will attempt to debit the funds from the + customer's account. The date must be a string in YYYY-MM-DD format. + The date must be in the future and between 3 and 15 calendar days + from now. + maxLength: 5000 + type: string + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: payment_intent_payment_method_options_us_bank_account + type: object + x-expandableFields: + - financial_connections + - mandate_options + payment_intent_processing: + description: '' + properties: + card: + $ref: '#/components/schemas/payment_intent_card_processing' + type: + description: >- + Type of the payment method for which payment is in `processing` + state, one of `card`. + enum: + - card + type: string + required: + - type + title: PaymentIntentProcessing + type: object + x-expandableFields: + - card + payment_intent_processing_customer_notification: + description: '' + properties: + approval_requested: + description: >- + Whether customer approval has been requested for this payment. For + payments greater than INR 15000 or mandate amount, the customer must + provide explicit approval of the payment with their bank. + nullable: true + type: boolean + completes_at: + description: >- + If customer approval is required, they need to provide approval + before this time. + format: unix-time + nullable: true + type: integer + title: PaymentIntentProcessingCustomerNotification + type: object + x-expandableFields: [] + payment_intent_type_specific_payment_method_options_client: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + - manual_preferred + type: string + installments: + $ref: '#/components/schemas/payment_flows_installment_options' + request_incremental_authorization_support: + description: >- + Request ability to + [increment](https://stripe.com/docs/terminal/features/incremental-authorizations) + this PaymentIntent if the combination of MCC and card brand is + eligible. Check + [incremental_authorization_supported](https://stripe.com/docs/api/charges/object#charge_object-payment_method_details-card_present-incremental_authorization_supported) + in the + [Confirm](https://stripe.com/docs/api/payment_intents/confirm) + response to verify support. + type: boolean + require_cvc_recollection: + description: >- + When enabled, using a card that is attached to a customer will + require the CVC to be provided again (i.e. using the cvc_token + parameter). + type: boolean + routing: + $ref: '#/components/schemas/payment_method_options_card_present_routing' + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: PaymentIntentTypeSpecificPaymentMethodOptionsClient + type: object + x-expandableFields: + - installments + - routing + payment_link: + description: >- + A payment link is a shareable URL that will take your customers to a + hosted payment page. A payment link can be shared and used multiple + times. + + + When a customer opens a payment link it will open a new [checkout + session](https://stripe.com/docs/api/checkout/sessions) to render the + payment page. You can use [checkout session + events](https://stripe.com/docs/api/events/types#event_types-checkout.session.completed) + to track payments through payment links. + + + Related guide: [Payment Links + API](https://stripe.com/docs/payment-links) + properties: + active: + description: >- + Whether the payment link's `url` is active. If `false`, customers + visiting the URL will be shown a page saying that the link has been + deactivated. + type: boolean + after_completion: + $ref: '#/components/schemas/payment_links_resource_after_completion' + allow_promotion_codes: + description: Whether user redeemable promotion codes are enabled. + type: boolean + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + description: The ID of the Connect application that created the Payment Link. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + application_fee_amount: + description: >- + The amount of the application fee (if any) that will be requested to + be applied to the payment and transferred to the application owner's + Stripe account. + nullable: true + type: integer + application_fee_percent: + description: >- + This represents the percentage of the subscription invoice total + that will be transferred to the application owner's Stripe account. + nullable: true + type: number + automatic_tax: + $ref: '#/components/schemas/payment_links_resource_automatic_tax' + billing_address_collection: + description: >- + Configuration for collecting the customer's billing address. + Defaults to `auto`. + enum: + - auto + - required + type: string + consent_collection: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_consent_collection' + description: >- + When set, provides configuration to gather active consent from + customers. + nullable: true + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + custom_fields: + description: >- + Collect additional information from your customer using custom + fields. Up to 3 fields are supported. + items: + $ref: '#/components/schemas/payment_links_resource_custom_fields' + type: array + custom_text: + $ref: '#/components/schemas/payment_links_resource_custom_text' + customer_creation: + description: Configuration for Customer creation during checkout. + enum: + - always + - if_required + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + inactive_message: + description: >- + The custom message to be displayed to a customer when a payment link + is no longer active. + maxLength: 5000 + nullable: true + type: string + invoice_creation: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_invoice_creation' + description: Configuration for creating invoice for payment mode payment links. + nullable: true + line_items: + description: The line items representing what is being sold. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: PaymentLinksResourceListLineItems + type: object + x-expandableFields: + - data + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - payment_link + type: string + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account on behalf of which to charge. See the [Connect + documentation](https://support.stripe.com/questions/sending-invoices-on-behalf-of-connected-accounts) + for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + optional_items: + description: The optional items presented to the customer at checkout. + items: + $ref: '#/components/schemas/payment_links_resource_optional_item' + nullable: true + type: array + payment_intent_data: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_payment_intent_data' + description: >- + Indicates the parameters to be passed to PaymentIntent creation + during checkout. + nullable: true + payment_method_collection: + description: >- + Configuration for collecting a payment method during checkout. + Defaults to `always`. + enum: + - always + - if_required + type: string + payment_method_types: + description: >- + The list of payment method types that customers can use. When + `null`, Stripe will dynamically show relevant payment methods you've + enabled in your [payment method + settings](https://dashboard.stripe.com/settings/payment_methods). + items: + enum: + - affirm + - afterpay_clearpay + - alipay + - alma + - au_becs_debit + - bacs_debit + - bancontact + - billie + - blik + - boleto + - card + - cashapp + - eps + - fpx + - giropay + - grabpay + - ideal + - klarna + - konbini + - link + - mobilepay + - multibanco + - oxxo + - p24 + - pay_by_bank + - paynow + - paypal + - pix + - promptpay + - satispay + - sepa_debit + - sofort + - swish + - twint + - us_bank_account + - wechat_pay + - zip + type: string + x-stripeBypassValidation: true + nullable: true + type: array + phone_number_collection: + $ref: '#/components/schemas/payment_links_resource_phone_number_collection' + restrictions: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_restrictions' + description: Settings that restrict the usage of a payment link. + nullable: true + shipping_address_collection: + anyOf: + - $ref: >- + #/components/schemas/payment_links_resource_shipping_address_collection + description: Configuration for collecting the customer's shipping address. + nullable: true + shipping_options: + description: The shipping rate options applied to the session. + items: + $ref: '#/components/schemas/payment_links_resource_shipping_option' + type: array + submit_type: + description: >- + Indicates the type of transaction being performed which customizes + relevant text on the page, such as the submit button. + enum: + - auto + - book + - donate + - pay + - subscribe + type: string + subscription_data: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_subscription_data' + description: >- + When creating a subscription, the specified configuration data will + be used. There must be at least one line item with a recurring price + to use `subscription_data`. + nullable: true + tax_id_collection: + $ref: '#/components/schemas/payment_links_resource_tax_id_collection' + transfer_data: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_transfer_data' + description: >- + The account (if any) the payments will be attributed to for tax + reporting, and where funds from each payment will be transferred to. + nullable: true + url: + description: The public URL that can be shared with customers. + maxLength: 5000 + type: string + required: + - active + - after_completion + - allow_promotion_codes + - automatic_tax + - billing_address_collection + - currency + - custom_fields + - custom_text + - customer_creation + - id + - livemode + - metadata + - object + - payment_method_collection + - phone_number_collection + - shipping_options + - submit_type + - tax_id_collection + - url + title: PaymentLink + type: object + x-expandableFields: + - after_completion + - application + - automatic_tax + - consent_collection + - custom_fields + - custom_text + - invoice_creation + - line_items + - on_behalf_of + - optional_items + - payment_intent_data + - phone_number_collection + - restrictions + - shipping_address_collection + - shipping_options + - subscription_data + - tax_id_collection + - transfer_data + x-resourceId: payment_link + payment_links_resource_after_completion: + description: '' + properties: + hosted_confirmation: + $ref: >- + #/components/schemas/payment_links_resource_completion_behavior_confirmation_page + redirect: + $ref: >- + #/components/schemas/payment_links_resource_completion_behavior_redirect + type: + description: The specified behavior after the purchase is complete. + enum: + - hosted_confirmation + - redirect + type: string + required: + - type + title: PaymentLinksResourceAfterCompletion + type: object + x-expandableFields: + - hosted_confirmation + - redirect + payment_links_resource_automatic_tax: + description: '' + properties: + enabled: + description: >- + If `true`, tax will be calculated automatically using the customer's + location. + type: boolean + liability: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The account that's liable for tax. If set, the business address and + tax registrations required to perform the tax calculation are loaded + from this account. The tax transaction is returned in the report of + the connected account. + nullable: true + required: + - enabled + title: PaymentLinksResourceAutomaticTax + type: object + x-expandableFields: + - liability + payment_links_resource_completed_sessions: + description: '' + properties: + count: + description: >- + The current number of checkout sessions that have been completed on + the payment link which count towards the `completed_sessions` + restriction to be met. + type: integer + limit: + description: >- + The maximum number of checkout sessions that can be completed for + the `completed_sessions` restriction to be met. + type: integer + required: + - count + - limit + title: PaymentLinksResourceCompletedSessions + type: object + x-expandableFields: [] + payment_links_resource_completion_behavior_confirmation_page: + description: '' + properties: + custom_message: + description: >- + The custom message that is displayed to the customer after the + purchase is complete. + maxLength: 5000 + nullable: true + type: string + title: PaymentLinksResourceCompletionBehaviorConfirmationPage + type: object + x-expandableFields: [] + payment_links_resource_completion_behavior_redirect: + description: '' + properties: + url: + description: >- + The URL the customer will be redirected to after the purchase is + complete. + maxLength: 5000 + type: string + required: + - url + title: PaymentLinksResourceCompletionBehaviorRedirect + type: object + x-expandableFields: [] + payment_links_resource_consent_collection: + description: '' + properties: + payment_method_reuse_agreement: + anyOf: + - $ref: >- + #/components/schemas/payment_links_resource_payment_method_reuse_agreement + description: >- + Settings related to the payment method reuse text shown in the + Checkout UI. + nullable: true + promotions: + description: >- + If set to `auto`, enables the collection of customer consent for + promotional communications. + enum: + - auto + - none + nullable: true + type: string + terms_of_service: + description: >- + If set to `required`, it requires cutomers to accept the terms of + service before being able to pay. If set to `none`, customers won't + be shown a checkbox to accept the terms of service. + enum: + - none + - required + nullable: true + type: string + title: PaymentLinksResourceConsentCollection + type: object + x-expandableFields: + - payment_method_reuse_agreement + payment_links_resource_custom_fields: + description: '' + properties: + dropdown: + $ref: '#/components/schemas/payment_links_resource_custom_fields_dropdown' + key: + description: >- + String of your choice that your integration can use to reconcile + this field. Must be unique to this field, alphanumeric, and up to + 200 characters. + maxLength: 5000 + type: string + label: + $ref: '#/components/schemas/payment_links_resource_custom_fields_label' + numeric: + $ref: '#/components/schemas/payment_links_resource_custom_fields_numeric' + optional: + description: >- + Whether the customer is required to complete the field before + completing the Checkout Session. Defaults to `false`. + type: boolean + text: + $ref: '#/components/schemas/payment_links_resource_custom_fields_text' + type: + description: The type of the field. + enum: + - dropdown + - numeric + - text + type: string + required: + - key + - label + - optional + - type + title: PaymentLinksResourceCustomFields + type: object + x-expandableFields: + - dropdown + - label + - numeric + - text + payment_links_resource_custom_fields_dropdown: + description: '' + properties: + default_value: + description: The value that will pre-fill on the payment page. + maxLength: 5000 + nullable: true + type: string + options: + description: >- + The options available for the customer to select. Up to 200 options + allowed. + items: + $ref: >- + #/components/schemas/payment_links_resource_custom_fields_dropdown_option + type: array + required: + - options + title: PaymentLinksResourceCustomFieldsDropdown + type: object + x-expandableFields: + - options + payment_links_resource_custom_fields_dropdown_option: + description: '' + properties: + label: + description: >- + The label for the option, displayed to the customer. Up to 100 + characters. + maxLength: 5000 + type: string + value: + description: >- + The value for this option, not displayed to the customer, used by + your integration to reconcile the option selected by the customer. + Must be unique to this option, alphanumeric, and up to 100 + characters. + maxLength: 5000 + type: string + required: + - label + - value + title: PaymentLinksResourceCustomFieldsDropdownOption + type: object + x-expandableFields: [] + payment_links_resource_custom_fields_label: + description: '' + properties: + custom: + description: >- + Custom text for the label, displayed to the customer. Up to 50 + characters. + maxLength: 5000 + nullable: true + type: string + type: + description: The type of the label. + enum: + - custom + type: string + required: + - type + title: PaymentLinksResourceCustomFieldsLabel + type: object + x-expandableFields: [] + payment_links_resource_custom_fields_numeric: + description: '' + properties: + default_value: + description: The value that will pre-fill the field on the payment page. + maxLength: 5000 + nullable: true + type: string + maximum_length: + description: The maximum character length constraint for the customer's input. + nullable: true + type: integer + minimum_length: + description: The minimum character length requirement for the customer's input. + nullable: true + type: integer + title: PaymentLinksResourceCustomFieldsNumeric + type: object + x-expandableFields: [] + payment_links_resource_custom_fields_text: + description: '' + properties: + default_value: + description: The value that will pre-fill the field on the payment page. + maxLength: 5000 + nullable: true + type: string + maximum_length: + description: The maximum character length constraint for the customer's input. + nullable: true + type: integer + minimum_length: + description: The minimum character length requirement for the customer's input. + nullable: true + type: integer + title: PaymentLinksResourceCustomFieldsText + type: object + x-expandableFields: [] + payment_links_resource_custom_text: + description: '' + properties: + after_submit: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_custom_text_position' + description: >- + Custom text that should be displayed after the payment confirmation + button. + nullable: true + shipping_address: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_custom_text_position' + description: >- + Custom text that should be displayed alongside shipping address + collection. + nullable: true + submit: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_custom_text_position' + description: >- + Custom text that should be displayed alongside the payment + confirmation button. + nullable: true + terms_of_service_acceptance: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_custom_text_position' + description: >- + Custom text that should be displayed in place of the default terms + of service agreement text. + nullable: true + title: PaymentLinksResourceCustomText + type: object + x-expandableFields: + - after_submit + - shipping_address + - submit + - terms_of_service_acceptance + payment_links_resource_custom_text_position: + description: '' + properties: + message: + description: Text may be up to 1200 characters in length. + maxLength: 500 + type: string + required: + - message + title: PaymentLinksResourceCustomTextPosition + type: object + x-expandableFields: [] + payment_links_resource_invoice_creation: + description: '' + properties: + enabled: + description: Enable creating an invoice on successful payment. + type: boolean + invoice_data: + anyOf: + - $ref: '#/components/schemas/payment_links_resource_invoice_settings' + description: >- + Configuration for the invoice. Default invoice values will be used + if unspecified. + nullable: true + required: + - enabled + title: PaymentLinksResourceInvoiceCreation + type: object + x-expandableFields: + - invoice_data + payment_links_resource_invoice_settings: + description: '' + properties: + account_tax_ids: + description: The account tax IDs associated with the invoice. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + nullable: true + type: array + custom_fields: + description: A list of up to 4 custom fields to be displayed on the invoice. + items: + $ref: '#/components/schemas/invoice_setting_custom_field' + nullable: true + type: array + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + footer: + description: Footer to be displayed on the invoice. + maxLength: 5000 + nullable: true + type: string + issuer: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The connected account that issues the invoice. The invoice is + presented with the branding and support information of the specified + account. + nullable: true + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + rendering_options: + anyOf: + - $ref: '#/components/schemas/invoice_setting_checkout_rendering_options' + description: Options for invoice PDF rendering. + nullable: true + title: PaymentLinksResourceInvoiceSettings + type: object + x-expandableFields: + - account_tax_ids + - custom_fields + - issuer + - rendering_options + payment_links_resource_optional_item: + description: '' + properties: + adjustable_quantity: + anyOf: + - $ref: >- + #/components/schemas/payment_links_resource_optional_item_adjustable_quantity + nullable: true + price: + maxLength: 5000 + type: string + quantity: + type: integer + required: + - price + - quantity + title: PaymentLinksResourceOptionalItem + type: object + x-expandableFields: + - adjustable_quantity + payment_links_resource_optional_item_adjustable_quantity: + description: '' + properties: + enabled: + description: >- + Set to true if the quantity can be adjusted to any non-negative + integer. + type: boolean + maximum: + description: >- + The maximum quantity of this item the customer can purchase. By + default this value is 99. + nullable: true + type: integer + minimum: + description: >- + The minimum quantity of this item the customer must purchase, if + they choose to purchase it. Because this item is optional, the + customer will always be able to remove it from their order, even if + the `minimum` configured here is greater than 0. By default this + value is 0. + nullable: true + type: integer + required: + - enabled + title: PaymentLinksResourceOptionalItemAdjustableQuantity + type: object + x-expandableFields: [] + payment_links_resource_payment_intent_data: + description: '' + properties: + capture_method: + description: >- + Indicates when the funds will be captured from the customer's + account. + enum: + - automatic + - automatic_async + - manual + nullable: true + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + will set metadata on [Payment + Intents](https://stripe.com/docs/api/payment_intents) generated from + this payment link. + type: object + setup_future_usage: + description: >- + Indicates that you intend to make future payments with the payment + method collected during checkout. + enum: + - off_session + - on_session + nullable: true + type: string + statement_descriptor: + description: >- + For a non-card payment, information about the charge that appears on + the customer's statement when this payment succeeds in creating a + charge. + maxLength: 5000 + nullable: true + type: string + statement_descriptor_suffix: + description: >- + For a card payment, information about the charge that appears on the + customer's statement when this payment succeeds in creating a + charge. Concatenated with the account's statement descriptor prefix + to form the complete statement descriptor. + maxLength: 5000 + nullable: true + type: string + transfer_group: + description: >- + A string that identifies the resulting payment as part of a group. + See the PaymentIntents [use case for connected + accounts](https://stripe.com/docs/connect/separate-charges-and-transfers) + for details. + maxLength: 5000 + nullable: true + type: string + required: + - metadata + title: PaymentLinksResourcePaymentIntentData + type: object + x-expandableFields: [] + payment_links_resource_payment_method_reuse_agreement: + description: '' + properties: + position: + description: >- + Determines the position and visibility of the payment method reuse + agreement in the UI. When set to `auto`, Stripe's defaults will be + used. + + + When set to `hidden`, the payment method reuse agreement text will + always be hidden in the UI. + enum: + - auto + - hidden + type: string + required: + - position + title: PaymentLinksResourcePaymentMethodReuseAgreement + type: object + x-expandableFields: [] + payment_links_resource_phone_number_collection: + description: '' + properties: + enabled: + description: 'If `true`, a phone number will be collected during checkout.' + type: boolean + required: + - enabled + title: PaymentLinksResourcePhoneNumberCollection + type: object + x-expandableFields: [] + payment_links_resource_restrictions: + description: '' + properties: + completed_sessions: + $ref: '#/components/schemas/payment_links_resource_completed_sessions' + required: + - completed_sessions + title: PaymentLinksResourceRestrictions + type: object + x-expandableFields: + - completed_sessions + payment_links_resource_shipping_address_collection: + description: '' + properties: + allowed_countries: + description: >- + An array of two-letter ISO country codes representing which + countries Checkout should provide as options for shipping locations. + Unsupported country codes: `AS, CX, CC, CU, HM, IR, KP, MH, FM, NF, + MP, PW, SD, SY, UM, VI`. + items: + enum: + - AC + - AD + - AE + - AF + - AG + - AI + - AL + - AM + - AO + - AQ + - AR + - AT + - AU + - AW + - AX + - AZ + - BA + - BB + - BD + - BE + - BF + - BG + - BH + - BI + - BJ + - BL + - BM + - BN + - BO + - BQ + - BR + - BS + - BT + - BV + - BW + - BY + - BZ + - CA + - CD + - CF + - CG + - CH + - CI + - CK + - CL + - CM + - CN + - CO + - CR + - CV + - CW + - CY + - CZ + - DE + - DJ + - DK + - DM + - DO + - DZ + - EC + - EE + - EG + - EH + - ER + - ES + - ET + - FI + - FJ + - FK + - FO + - FR + - GA + - GB + - GD + - GE + - GF + - GG + - GH + - GI + - GL + - GM + - GN + - GP + - GQ + - GR + - GS + - GT + - GU + - GW + - GY + - HK + - HN + - HR + - HT + - HU + - ID + - IE + - IL + - IM + - IN + - IO + - IQ + - IS + - IT + - JE + - JM + - JO + - JP + - KE + - KG + - KH + - KI + - KM + - KN + - KR + - KW + - KY + - KZ + - LA + - LB + - LC + - LI + - LK + - LR + - LS + - LT + - LU + - LV + - LY + - MA + - MC + - MD + - ME + - MF + - MG + - MK + - ML + - MM + - MN + - MO + - MQ + - MR + - MS + - MT + - MU + - MV + - MW + - MX + - MY + - MZ + - NA + - NC + - NE + - NG + - NI + - NL + - 'NO' + - NP + - NR + - NU + - NZ + - OM + - PA + - PE + - PF + - PG + - PH + - PK + - PL + - PM + - PN + - PR + - PS + - PT + - PY + - QA + - RE + - RO + - RS + - RU + - RW + - SA + - SB + - SC + - SD + - SE + - SG + - SH + - SI + - SJ + - SK + - SL + - SM + - SN + - SO + - SR + - SS + - ST + - SV + - SX + - SZ + - TA + - TC + - TD + - TF + - TG + - TH + - TJ + - TK + - TL + - TM + - TN + - TO + - TR + - TT + - TV + - TW + - TZ + - UA + - UG + - US + - UY + - UZ + - VA + - VC + - VE + - VG + - VN + - VU + - WF + - WS + - XK + - YE + - YT + - ZA + - ZM + - ZW + - ZZ + type: string + type: array + required: + - allowed_countries + title: PaymentLinksResourceShippingAddressCollection + type: object + x-expandableFields: [] + payment_links_resource_shipping_option: + description: '' + properties: + shipping_amount: + description: A non-negative integer in cents representing how much to charge. + type: integer + shipping_rate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/shipping_rate' + description: The ID of the Shipping Rate to use for this shipping option. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/shipping_rate' + required: + - shipping_amount + - shipping_rate + title: PaymentLinksResourceShippingOption + type: object + x-expandableFields: + - shipping_rate + payment_links_resource_subscription_data: + description: '' + properties: + description: + description: >- + The subscription's description, meant to be displayable to the + customer. Use this field to optionally store an explanation of the + subscription for rendering in Stripe surfaces and certain local + payment methods UIs. + maxLength: 5000 + nullable: true + type: string + invoice_settings: + $ref: >- + #/components/schemas/payment_links_resource_subscription_data_invoice_settings + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + will set metadata on + [Subscriptions](https://stripe.com/docs/api/subscriptions) generated + from this payment link. + type: object + trial_period_days: + description: >- + Integer representing the number of trial period days before the + customer is charged for the first time. + nullable: true + type: integer + trial_settings: + anyOf: + - $ref: >- + #/components/schemas/subscriptions_trials_resource_trial_settings + description: Settings related to subscription trials. + nullable: true + required: + - invoice_settings + - metadata + title: PaymentLinksResourceSubscriptionData + type: object + x-expandableFields: + - invoice_settings + - trial_settings + payment_links_resource_subscription_data_invoice_settings: + description: '' + properties: + issuer: + $ref: '#/components/schemas/connect_account_reference' + required: + - issuer + title: PaymentLinksResourceSubscriptionDataInvoiceSettings + type: object + x-expandableFields: + - issuer + payment_links_resource_tax_id_collection: + description: '' + properties: + enabled: + description: Indicates whether tax ID collection is enabled for the session. + type: boolean + required: + enum: + - if_supported + - never + type: string + required: + - enabled + - required + title: PaymentLinksResourceTaxIdCollection + type: object + x-expandableFields: [] + payment_links_resource_transfer_data: + description: '' + properties: + amount: + description: >- + The amount in cents (or local equivalent) that will be transferred + to the destination account. By default, the entire amount is + transferred to the destination. + nullable: true + type: integer + destination: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: The connected account receiving the transfer. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + required: + - destination + title: PaymentLinksResourceTransferData + type: object + x-expandableFields: + - destination + payment_method: + description: >- + PaymentMethod objects represent your customer's payment instruments. + + You can use them with + [PaymentIntents](https://stripe.com/docs/payments/payment-intents) to + collect payments or save them to + + Customer objects to store instrument details for future payments. + + + Related guides: [Payment + Methods](https://stripe.com/docs/payments/payment-methods) and [More + Payment + Scenarios](https://stripe.com/docs/payments/more-payment-scenarios). + properties: + acss_debit: + $ref: '#/components/schemas/payment_method_acss_debit' + affirm: + $ref: '#/components/schemas/payment_method_affirm' + afterpay_clearpay: + $ref: '#/components/schemas/payment_method_afterpay_clearpay' + alipay: + $ref: '#/components/schemas/payment_flows_private_payment_methods_alipay' + allow_redisplay: + description: >- + This field indicates whether this payment method can be shown again + to its customer in a checkout flow. Stripe products such as Checkout + and Elements use this field to determine whether a payment method + can be shown as a saved payment method in a checkout flow. The field + defaults to “unspecified”. + enum: + - always + - limited + - unspecified + type: string + alma: + $ref: '#/components/schemas/payment_method_alma' + amazon_pay: + $ref: '#/components/schemas/payment_method_amazon_pay' + au_becs_debit: + $ref: '#/components/schemas/payment_method_au_becs_debit' + bacs_debit: + $ref: '#/components/schemas/payment_method_bacs_debit' + bancontact: + $ref: '#/components/schemas/payment_method_bancontact' + billie: + $ref: '#/components/schemas/payment_method_billie' + billing_details: + $ref: '#/components/schemas/billing_details' + blik: + $ref: '#/components/schemas/payment_method_blik' + boleto: + $ref: '#/components/schemas/payment_method_boleto' + card: + $ref: '#/components/schemas/payment_method_card' + card_present: + $ref: '#/components/schemas/payment_method_card_present' + cashapp: + $ref: '#/components/schemas/payment_method_cashapp' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: >- + The ID of the Customer to which this PaymentMethod is saved. This + will not be set when the PaymentMethod has not been saved to a + Customer. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + customer_balance: + $ref: '#/components/schemas/payment_method_customer_balance' + eps: + $ref: '#/components/schemas/payment_method_eps' + fpx: + $ref: '#/components/schemas/payment_method_fpx' + giropay: + $ref: '#/components/schemas/payment_method_giropay' + grabpay: + $ref: '#/components/schemas/payment_method_grabpay' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + ideal: + $ref: '#/components/schemas/payment_method_ideal' + interac_present: + $ref: '#/components/schemas/payment_method_interac_present' + kakao_pay: + $ref: '#/components/schemas/payment_method_kakao_pay' + klarna: + $ref: '#/components/schemas/payment_method_klarna' + konbini: + $ref: '#/components/schemas/payment_method_konbini' + kr_card: + $ref: '#/components/schemas/payment_method_kr_card' + link: + $ref: '#/components/schemas/payment_method_link' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + mobilepay: + $ref: '#/components/schemas/payment_method_mobilepay' + multibanco: + $ref: '#/components/schemas/payment_method_multibanco' + naver_pay: + $ref: '#/components/schemas/payment_method_naver_pay' + nz_bank_account: + $ref: '#/components/schemas/payment_method_nz_bank_account' + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - payment_method + type: string + oxxo: + $ref: '#/components/schemas/payment_method_oxxo' + p24: + $ref: '#/components/schemas/payment_method_p24' + pay_by_bank: + $ref: '#/components/schemas/payment_method_pay_by_bank' + payco: + $ref: '#/components/schemas/payment_method_payco' + paynow: + $ref: '#/components/schemas/payment_method_paynow' + paypal: + $ref: '#/components/schemas/payment_method_paypal' + pix: + $ref: '#/components/schemas/payment_method_pix' + promptpay: + $ref: '#/components/schemas/payment_method_promptpay' + radar_options: + $ref: '#/components/schemas/radar_radar_options' + revolut_pay: + $ref: '#/components/schemas/payment_method_revolut_pay' + samsung_pay: + $ref: '#/components/schemas/payment_method_samsung_pay' + satispay: + $ref: '#/components/schemas/payment_method_satispay' + sepa_debit: + $ref: '#/components/schemas/payment_method_sepa_debit' + sofort: + $ref: '#/components/schemas/payment_method_sofort' + swish: + $ref: '#/components/schemas/payment_method_swish' + twint: + $ref: '#/components/schemas/payment_method_twint' + type: + description: >- + The type of the PaymentMethod. An additional hash is included on the + PaymentMethod with a name matching this value. It contains + additional information specific to the PaymentMethod type. + enum: + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - billie + - blik + - boleto + - card + - card_present + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - interac_present + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - nz_bank_account + - oxxo + - p24 + - pay_by_bank + - payco + - paynow + - paypal + - pix + - promptpay + - revolut_pay + - samsung_pay + - satispay + - sepa_debit + - sofort + - swish + - twint + - us_bank_account + - wechat_pay + - zip + type: string + x-stripeBypassValidation: true + us_bank_account: + $ref: '#/components/schemas/payment_method_us_bank_account' + wechat_pay: + $ref: '#/components/schemas/payment_method_wechat_pay' + zip: + $ref: '#/components/schemas/payment_method_zip' + required: + - billing_details + - created + - id + - livemode + - object + - type + title: PaymentMethod + type: object + x-expandableFields: + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - billie + - billing_details + - blik + - boleto + - card + - card_present + - cashapp + - customer + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - interac_present + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - nz_bank_account + - oxxo + - p24 + - pay_by_bank + - payco + - paynow + - paypal + - pix + - promptpay + - radar_options + - revolut_pay + - samsung_pay + - satispay + - sepa_debit + - sofort + - swish + - twint + - us_bank_account + - wechat_pay + - zip + x-resourceId: payment_method + payment_method_acss_debit: + description: '' + properties: + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + institution_number: + description: Institution number of the bank account. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + transit_number: + description: Transit number of the bank account. + maxLength: 5000 + nullable: true + type: string + title: payment_method_acss_debit + type: object + x-expandableFields: [] + payment_method_affirm: + description: '' + properties: {} + title: payment_method_affirm + type: object + x-expandableFields: [] + payment_method_afterpay_clearpay: + description: '' + properties: {} + title: payment_method_afterpay_clearpay + type: object + x-expandableFields: [] + payment_method_alma: + description: '' + properties: {} + title: payment_method_alma + type: object + x-expandableFields: [] + payment_method_amazon_pay: + description: '' + properties: {} + title: payment_method_amazon_pay + type: object + x-expandableFields: [] + payment_method_au_becs_debit: + description: '' + properties: + bsb_number: + description: >- + Six-digit number identifying bank and branch associated with this + bank account. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + title: payment_method_au_becs_debit + type: object + x-expandableFields: [] + payment_method_bacs_debit: + description: '' + properties: + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + sort_code: + description: 'Sort code of the bank account. (e.g., `10-20-30`)' + maxLength: 5000 + nullable: true + type: string + title: payment_method_bacs_debit + type: object + x-expandableFields: [] + payment_method_bancontact: + description: '' + properties: {} + title: payment_method_bancontact + type: object + x-expandableFields: [] + payment_method_billie: + description: '' + properties: {} + title: payment_method_billie + type: object + x-expandableFields: [] + payment_method_blik: + description: '' + properties: {} + title: payment_method_blik + type: object + x-expandableFields: [] + payment_method_boleto: + description: '' + properties: + tax_id: + description: Uniquely identifies the customer tax id (CNPJ or CPF) + maxLength: 5000 + type: string + required: + - tax_id + title: payment_method_boleto + type: object + x-expandableFields: [] + payment_method_card: + description: '' + properties: + brand: + description: >- + Card brand. Can be `amex`, `diners`, `discover`, `eftpos_au`, `jcb`, + `link`, `mastercard`, `unionpay`, `visa`, or `unknown`. + maxLength: 5000 + type: string + checks: + anyOf: + - $ref: '#/components/schemas/payment_method_card_checks' + description: Checks on Card address and CVC if provided. + nullable: true + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + display_brand: + description: >- + The brand to use when displaying the card, this accounts for + customer's brand choice on dual-branded cards. Can be + `american_express`, `cartes_bancaires`, `diners_club`, `discover`, + `eftpos_australia`, `interac`, `jcb`, `mastercard`, `union_pay`, + `visa`, or `other` and may contain more values in the future. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + type: integer + fingerprint: + description: >- + Uniquely identifies this particular card number. You can use this + attribute to check whether two customers who’ve signed up with you + are using the same card number, for example. For payment methods + that tokenize card information (Apple Pay, Google Pay), the + tokenized number might be provided instead of the underlying card + number. + + + *As of May 1, 2021, card fingerprint in India for Connect changed to + allow two fingerprints for the same card---one for India and one for + the rest of the world.* + maxLength: 5000 + nullable: true + type: string + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + type: string + generated_from: + anyOf: + - $ref: '#/components/schemas/payment_method_card_generated_card' + description: Details of the original PaymentMethod that created this object. + nullable: true + last4: + description: The last four digits of the card. + maxLength: 5000 + type: string + networks: + anyOf: + - $ref: '#/components/schemas/networks' + description: >- + Contains information about card networks that can be used to process + the payment. + nullable: true + regulated_status: + description: Status of a card based on the card issuer. + enum: + - regulated + - unregulated + nullable: true + type: string + three_d_secure_usage: + anyOf: + - $ref: '#/components/schemas/three_d_secure_usage' + description: >- + Contains details on how this Card may be used for 3D Secure + authentication. + nullable: true + wallet: + anyOf: + - $ref: '#/components/schemas/payment_method_card_wallet' + description: >- + If this Card is part of a card wallet, this contains the details of + the card wallet. + nullable: true + required: + - brand + - exp_month + - exp_year + - funding + - last4 + title: payment_method_card + type: object + x-expandableFields: + - checks + - generated_from + - networks + - three_d_secure_usage + - wallet + payment_method_card_checks: + description: '' + properties: + address_line1_check: + description: >- + If a address line1 was provided, results of the check, one of + `pass`, `fail`, `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + address_postal_code_check: + description: >- + If a address postal code was provided, results of the check, one of + `pass`, `fail`, `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + cvc_check: + description: >- + If a CVC was provided, results of the check, one of `pass`, `fail`, + `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + title: payment_method_card_checks + type: object + x-expandableFields: [] + payment_method_card_generated_card: + description: '' + properties: + charge: + description: The charge that created this object. + maxLength: 5000 + nullable: true + type: string + payment_method_details: + anyOf: + - $ref: '#/components/schemas/card_generated_from_payment_method_details' + description: >- + Transaction-specific details of the payment method used in the + payment. + nullable: true + setup_attempt: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/setup_attempt' + description: >- + The ID of the SetupAttempt that generated this PaymentMethod, if + any. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/setup_attempt' + title: payment_method_card_generated_card + type: object + x-expandableFields: + - payment_method_details + - setup_attempt + payment_method_card_present: + description: '' + properties: + brand: + description: >- + Card brand. Can be `amex`, `diners`, `discover`, `eftpos_au`, `jcb`, + `link`, `mastercard`, `unionpay`, `visa`, or `unknown`. + maxLength: 5000 + nullable: true + type: string + brand_product: + description: >- + The [product code](https://stripe.com/docs/card-product-codes) that + identifies the specific program or product associated with a card. + maxLength: 5000 + nullable: true + type: string + cardholder_name: + description: >- + The cardholder name as read from the card, in [ISO + 7813](https://en.wikipedia.org/wiki/ISO/IEC_7813) format. May + include alphanumeric characters, special characters and first/last + name separator (`/`). In some cases, the cardholder name may not be + available depending on how the issuer has configured the card. + Cardholder name is typically not available on swipe or contactless + payments, such as those made with Apple Pay and Google Pay. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + description: + description: A high-level description of the type of cards issued in this range. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + type: integer + fingerprint: + description: >- + Uniquely identifies this particular card number. You can use this + attribute to check whether two customers who’ve signed up with you + are using the same card number, for example. For payment methods + that tokenize card information (Apple Pay, Google Pay), the + tokenized number might be provided instead of the underlying card + number. + + + *As of May 1, 2021, card fingerprint in India for Connect changed to + allow two fingerprints for the same card---one for India and one for + the rest of the world.* + maxLength: 5000 + nullable: true + type: string + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + issuer: + description: The name of the card's issuing bank. + maxLength: 5000 + nullable: true + type: string + last4: + description: The last four digits of the card. + maxLength: 5000 + nullable: true + type: string + networks: + anyOf: + - $ref: '#/components/schemas/payment_method_card_present_networks' + description: >- + Contains information about card networks that can be used to process + the payment. + nullable: true + offline: + anyOf: + - $ref: '#/components/schemas/payment_method_details_card_present_offline' + description: Details about payment methods collected offline. + nullable: true + preferred_locales: + description: >- + EMV tag 5F2D. Preferred languages specified by the integrated + circuit chip. + items: + maxLength: 5000 + type: string + nullable: true + type: array + read_method: + description: How card details were read in this transaction. + enum: + - contact_emv + - contactless_emv + - contactless_magstripe_mode + - magnetic_stripe_fallback + - magnetic_stripe_track2 + nullable: true + type: string + wallet: + $ref: >- + #/components/schemas/payment_flows_private_payment_methods_card_present_common_wallet + required: + - exp_month + - exp_year + title: payment_method_card_present + type: object + x-expandableFields: + - networks + - offline + - wallet + payment_method_card_present_networks: + description: '' + properties: + available: + description: >- + All networks available for selection via + [payment_method_options.card.network](/api/payment_intents/confirm#confirm_payment_intent-payment_method_options-card-network). + items: + maxLength: 5000 + type: string + type: array + preferred: + description: The preferred network for the card. + maxLength: 5000 + nullable: true + type: string + required: + - available + title: payment_method_card_present_networks + type: object + x-expandableFields: [] + payment_method_card_wallet: + description: '' + properties: + amex_express_checkout: + $ref: >- + #/components/schemas/payment_method_card_wallet_amex_express_checkout + apple_pay: + $ref: '#/components/schemas/payment_method_card_wallet_apple_pay' + dynamic_last4: + description: >- + (For tokenized numbers only.) The last four digits of the device + account number. + maxLength: 5000 + nullable: true + type: string + google_pay: + $ref: '#/components/schemas/payment_method_card_wallet_google_pay' + link: + $ref: '#/components/schemas/payment_method_card_wallet_link' + masterpass: + $ref: '#/components/schemas/payment_method_card_wallet_masterpass' + samsung_pay: + $ref: '#/components/schemas/payment_method_card_wallet_samsung_pay' + type: + description: >- + The type of the card wallet, one of `amex_express_checkout`, + `apple_pay`, `google_pay`, `masterpass`, `samsung_pay`, + `visa_checkout`, or `link`. An additional hash is included on the + Wallet subhash with a name matching this value. It contains + additional information specific to the card wallet type. + enum: + - amex_express_checkout + - apple_pay + - google_pay + - link + - masterpass + - samsung_pay + - visa_checkout + type: string + visa_checkout: + $ref: '#/components/schemas/payment_method_card_wallet_visa_checkout' + required: + - type + title: payment_method_card_wallet + type: object + x-expandableFields: + - amex_express_checkout + - apple_pay + - google_pay + - link + - masterpass + - samsung_pay + - visa_checkout + payment_method_card_wallet_amex_express_checkout: + description: '' + properties: {} + title: payment_method_card_wallet_amex_express_checkout + type: object + x-expandableFields: [] + payment_method_card_wallet_apple_pay: + description: '' + properties: {} + title: payment_method_card_wallet_apple_pay + type: object + x-expandableFields: [] + payment_method_card_wallet_google_pay: + description: '' + properties: {} + title: payment_method_card_wallet_google_pay + type: object + x-expandableFields: [] + payment_method_card_wallet_link: + description: '' + properties: {} + title: payment_method_card_wallet_link + type: object + x-expandableFields: [] + payment_method_card_wallet_masterpass: + description: '' + properties: + billing_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Owner's verified billing address. Values are verified or provided by + the wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + nullable: true + email: + description: >- + Owner's verified email. Values are verified or provided by the + wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + name: + description: >- + Owner's verified full name. Values are verified or provided by the + wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + shipping_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Owner's verified shipping address. Values are verified or provided + by the wallet directly (if supported) at the time of authorization + or settlement. They cannot be set or mutated. + nullable: true + title: payment_method_card_wallet_masterpass + type: object + x-expandableFields: + - billing_address + - shipping_address + payment_method_card_wallet_samsung_pay: + description: '' + properties: {} + title: payment_method_card_wallet_samsung_pay + type: object + x-expandableFields: [] + payment_method_card_wallet_visa_checkout: + description: '' + properties: + billing_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Owner's verified billing address. Values are verified or provided by + the wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + nullable: true + email: + description: >- + Owner's verified email. Values are verified or provided by the + wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + name: + description: >- + Owner's verified full name. Values are verified or provided by the + wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + shipping_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Owner's verified shipping address. Values are verified or provided + by the wallet directly (if supported) at the time of authorization + or settlement. They cannot be set or mutated. + nullable: true + title: payment_method_card_wallet_visa_checkout + type: object + x-expandableFields: + - billing_address + - shipping_address + payment_method_cashapp: + description: '' + properties: + buyer_id: + description: >- + A unique and immutable identifier assigned by Cash App to every + buyer. + maxLength: 5000 + nullable: true + type: string + cashtag: + description: A public identifier for buyers using Cash App. + maxLength: 5000 + nullable: true + type: string + title: payment_method_cashapp + type: object + x-expandableFields: [] + payment_method_config_biz_payment_method_configuration_details: + description: '' + properties: + id: + description: ID of the payment method configuration used. + maxLength: 5000 + type: string + parent: + description: ID of the parent payment method configuration used. + maxLength: 5000 + nullable: true + type: string + required: + - id + title: PaymentMethodConfigBizPaymentMethodConfigurationDetails + type: object + x-expandableFields: [] + payment_method_config_resource_display_preference: + description: '' + properties: + overridable: + description: >- + For child configs, whether or not the account's preference will be + observed. If `false`, the parent configuration's default is used. + nullable: true + type: boolean + preference: + description: The account's display preference. + enum: + - none + - 'off' + - 'on' + type: string + value: + description: The effective display preference value. + enum: + - 'off' + - 'on' + type: string + required: + - preference + - value + title: PaymentMethodConfigResourceDisplayPreference + type: object + x-expandableFields: [] + payment_method_config_resource_payment_method_properties: + description: '' + properties: + available: + description: >- + Whether this payment method may be offered at checkout. True if + `display_preference` is `on` and the payment method's capability is + active. + type: boolean + display_preference: + $ref: >- + #/components/schemas/payment_method_config_resource_display_preference + required: + - available + - display_preference + title: PaymentMethodConfigResourcePaymentMethodProperties + type: object + x-expandableFields: + - display_preference + payment_method_configuration: + description: >- + PaymentMethodConfigurations control which payment methods are displayed + to your customers when you don't explicitly specify payment method + types. You can have multiple configurations with different sets of + payment methods for different scenarios. + + + There are two types of PaymentMethodConfigurations. Which is used + depends on the [charge type](https://stripe.com/docs/connect/charges): + + + **Direct** configurations apply to payments created on your account, + including Connect destination charges, Connect separate charges and + transfers, and payments not involving Connect. + + + **Child** configurations apply to payments created on your connected + accounts using direct charges, and charges with the on_behalf_of + parameter. + + + Child configurations have a `parent` that sets default values and + controls which settings connected accounts may override. You can specify + a parent ID at payment time, and Stripe will automatically resolve the + connected account’s associated child configuration. Parent + configurations are [managed in the + dashboard](https://dashboard.stripe.com/settings/payment_methods/connected_accounts) + and are not available in this API. + + + Related guides: + + - [Payment Method Configurations + API](https://stripe.com/docs/connect/payment-method-configurations) + + - [Multiple configurations on dynamic payment + methods](https://stripe.com/docs/payments/multiple-payment-method-configs) + + - [Multiple configurations for your Connect + accounts](https://stripe.com/docs/connect/multiple-payment-method-configurations) + properties: + acss_debit: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + active: + description: Whether the configuration can be used for new payments. + type: boolean + affirm: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + afterpay_clearpay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + alipay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + alma: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + amazon_pay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + apple_pay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + application: + description: >- + For child configs, the Connect application associated with the + configuration. + maxLength: 5000 + nullable: true + type: string + au_becs_debit: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + bacs_debit: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + bancontact: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + billie: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + blik: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + boleto: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + card: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + cartes_bancaires: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + cashapp: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + customer_balance: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + eps: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + fpx: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + giropay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + google_pay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + grabpay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + ideal: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + is_default: + description: >- + The default configuration is used whenever a payment method + configuration is not specified. + type: boolean + jcb: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + kakao_pay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + klarna: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + konbini: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + kr_card: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + link: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + mobilepay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + multibanco: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + name: + description: The configuration's name. + maxLength: 5000 + type: string + naver_pay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + nz_bank_account: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - payment_method_configuration + type: string + oxxo: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + p24: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + parent: + description: 'For child configs, the configuration''s parent configuration.' + maxLength: 5000 + nullable: true + type: string + pay_by_bank: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + payco: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + paynow: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + paypal: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + pix: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + promptpay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + revolut_pay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + samsung_pay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + satispay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + sepa_debit: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + sofort: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + swish: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + twint: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + us_bank_account: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + wechat_pay: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + zip: + $ref: >- + #/components/schemas/payment_method_config_resource_payment_method_properties + required: + - active + - id + - is_default + - livemode + - name + - object + title: PaymentMethodConfigResourcePaymentMethodConfiguration + type: object + x-expandableFields: + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - apple_pay + - au_becs_debit + - bacs_debit + - bancontact + - billie + - blik + - boleto + - card + - cartes_bancaires + - cashapp + - customer_balance + - eps + - fpx + - giropay + - google_pay + - grabpay + - ideal + - jcb + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - nz_bank_account + - oxxo + - p24 + - pay_by_bank + - payco + - paynow + - paypal + - pix + - promptpay + - revolut_pay + - samsung_pay + - satispay + - sepa_debit + - sofort + - swish + - twint + - us_bank_account + - wechat_pay + - zip + x-resourceId: payment_method_configuration + payment_method_customer_balance: + description: '' + properties: {} + title: payment_method_customer_balance + type: object + x-expandableFields: [] + payment_method_details: + description: '' + properties: + ach_credit_transfer: + $ref: '#/components/schemas/payment_method_details_ach_credit_transfer' + ach_debit: + $ref: '#/components/schemas/payment_method_details_ach_debit' + acss_debit: + $ref: '#/components/schemas/payment_method_details_acss_debit' + affirm: + $ref: '#/components/schemas/payment_method_details_affirm' + afterpay_clearpay: + $ref: '#/components/schemas/payment_method_details_afterpay_clearpay' + alipay: + $ref: >- + #/components/schemas/payment_flows_private_payment_methods_alipay_details + alma: + $ref: '#/components/schemas/payment_method_details_alma' + amazon_pay: + $ref: '#/components/schemas/payment_method_details_amazon_pay' + au_becs_debit: + $ref: '#/components/schemas/payment_method_details_au_becs_debit' + bacs_debit: + $ref: '#/components/schemas/payment_method_details_bacs_debit' + bancontact: + $ref: '#/components/schemas/payment_method_details_bancontact' + billie: + $ref: '#/components/schemas/payment_method_details_billie' + blik: + $ref: '#/components/schemas/payment_method_details_blik' + boleto: + $ref: '#/components/schemas/payment_method_details_boleto' + card: + $ref: '#/components/schemas/payment_method_details_card' + card_present: + $ref: '#/components/schemas/payment_method_details_card_present' + cashapp: + $ref: '#/components/schemas/payment_method_details_cashapp' + customer_balance: + $ref: '#/components/schemas/payment_method_details_customer_balance' + eps: + $ref: '#/components/schemas/payment_method_details_eps' + fpx: + $ref: '#/components/schemas/payment_method_details_fpx' + giropay: + $ref: '#/components/schemas/payment_method_details_giropay' + grabpay: + $ref: '#/components/schemas/payment_method_details_grabpay' + ideal: + $ref: '#/components/schemas/payment_method_details_ideal' + interac_present: + $ref: '#/components/schemas/payment_method_details_interac_present' + kakao_pay: + $ref: '#/components/schemas/payment_method_details_kakao_pay' + klarna: + $ref: '#/components/schemas/payment_method_details_klarna' + konbini: + $ref: '#/components/schemas/payment_method_details_konbini' + kr_card: + $ref: '#/components/schemas/payment_method_details_kr_card' + link: + $ref: '#/components/schemas/payment_method_details_link' + mobilepay: + $ref: '#/components/schemas/payment_method_details_mobilepay' + multibanco: + $ref: '#/components/schemas/payment_method_details_multibanco' + naver_pay: + $ref: '#/components/schemas/payment_method_details_naver_pay' + nz_bank_account: + $ref: '#/components/schemas/payment_method_details_nz_bank_account' + oxxo: + $ref: '#/components/schemas/payment_method_details_oxxo' + p24: + $ref: '#/components/schemas/payment_method_details_p24' + pay_by_bank: + $ref: '#/components/schemas/payment_method_details_pay_by_bank' + payco: + $ref: '#/components/schemas/payment_method_details_payco' + paynow: + $ref: '#/components/schemas/payment_method_details_paynow' + paypal: + $ref: '#/components/schemas/payment_method_details_paypal' + pix: + $ref: '#/components/schemas/payment_method_details_pix' + promptpay: + $ref: '#/components/schemas/payment_method_details_promptpay' + revolut_pay: + $ref: '#/components/schemas/payment_method_details_revolut_pay' + samsung_pay: + $ref: '#/components/schemas/payment_method_details_samsung_pay' + satispay: + $ref: '#/components/schemas/payment_method_details_satispay' + sepa_debit: + $ref: '#/components/schemas/payment_method_details_sepa_debit' + sofort: + $ref: '#/components/schemas/payment_method_details_sofort' + stripe_account: + $ref: '#/components/schemas/payment_method_details_stripe_account' + swish: + $ref: '#/components/schemas/payment_method_details_swish' + twint: + $ref: '#/components/schemas/payment_method_details_twint' + type: + description: >- + The type of transaction-specific details of the payment method used + in the payment. See + [PaymentMethod.type](https://stripe.com/docs/api/payment_methods/object#payment_method_object-type) + for the full list of possible types. + + An additional hash is included on `payment_method_details` with a + name matching this value. + + It contains information specific to the payment method. + maxLength: 5000 + type: string + us_bank_account: + $ref: '#/components/schemas/payment_method_details_us_bank_account' + wechat: + $ref: '#/components/schemas/payment_method_details_wechat' + wechat_pay: + $ref: '#/components/schemas/payment_method_details_wechat_pay' + zip: + $ref: '#/components/schemas/payment_method_details_zip' + required: + - type + title: payment_method_details + type: object + x-expandableFields: + - ach_credit_transfer + - ach_debit + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - billie + - blik + - boleto + - card + - card_present + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - interac_present + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - nz_bank_account + - oxxo + - p24 + - pay_by_bank + - payco + - paynow + - paypal + - pix + - promptpay + - revolut_pay + - samsung_pay + - satispay + - sepa_debit + - sofort + - stripe_account + - swish + - twint + - us_bank_account + - wechat + - wechat_pay + - zip + payment_method_details_ach_credit_transfer: + description: '' + properties: + account_number: + description: Account number to transfer funds to. + maxLength: 5000 + nullable: true + type: string + bank_name: + description: Name of the bank associated with the routing number. + maxLength: 5000 + nullable: true + type: string + routing_number: + description: Routing transit number for the bank account to transfer funds to. + maxLength: 5000 + nullable: true + type: string + swift_code: + description: SWIFT code of the bank associated with the routing number. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_ach_credit_transfer + type: object + x-expandableFields: [] + payment_method_details_ach_debit: + description: '' + properties: + account_holder_type: + description: >- + Type of entity that holds the account. This can be either + `individual` or `company`. + enum: + - company + - individual + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country the bank account is + located in. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + routing_number: + description: Routing transit number of the bank account. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_ach_debit + type: object + x-expandableFields: [] + payment_method_details_acss_debit: + description: '' + properties: + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + institution_number: + description: Institution number of the bank account + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + mandate: + description: ID of the mandate used to make this payment. + maxLength: 5000 + type: string + transit_number: + description: Transit number of the bank account. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_acss_debit + type: object + x-expandableFields: [] + payment_method_details_affirm: + description: '' + properties: + location: + description: >- + ID of the [location](https://stripe.com/docs/api/terminal/locations) + that this transaction's reader is assigned to. + maxLength: 5000 + type: string + reader: + description: >- + ID of the [reader](https://stripe.com/docs/api/terminal/readers) + this transaction was made on. + maxLength: 5000 + type: string + transaction_id: + description: The Affirm transaction ID associated with this payment. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_affirm + type: object + x-expandableFields: [] + payment_method_details_afterpay_clearpay: + description: '' + properties: + order_id: + description: The Afterpay order ID associated with this payment intent. + maxLength: 5000 + nullable: true + type: string + reference: + description: Order identifier shown to the merchant in Afterpay’s online portal. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_afterpay_clearpay + type: object + x-expandableFields: [] + payment_method_details_alma: + description: '' + properties: {} + title: payment_method_details_alma + type: object + x-expandableFields: [] + payment_method_details_amazon_pay: + description: '' + properties: + funding: + $ref: >- + #/components/schemas/amazon_pay_underlying_payment_method_funding_details + title: payment_method_details_amazon_pay + type: object + x-expandableFields: + - funding + payment_method_details_au_becs_debit: + description: '' + properties: + bsb_number: + description: Bank-State-Branch number of the bank account. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + mandate: + description: ID of the mandate used to make this payment. + maxLength: 5000 + type: string + title: payment_method_details_au_becs_debit + type: object + x-expandableFields: [] + payment_method_details_bacs_debit: + description: '' + properties: + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + mandate: + description: ID of the mandate used to make this payment. + maxLength: 5000 + nullable: true + type: string + sort_code: + description: 'Sort code of the bank account. (e.g., `10-20-30`)' + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_bacs_debit + type: object + x-expandableFields: [] + payment_method_details_bancontact: + description: '' + properties: + bank_code: + description: Bank code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bic: + description: Bank Identifier Code of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + generated_sepa_debit: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + The ID of the SEPA Direct Debit PaymentMethod which was generated by + this Charge. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + generated_sepa_debit_mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: >- + The mandate for the SEPA Direct Debit PaymentMethod which was + generated by this Charge. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + iban_last4: + description: Last four characters of the IBAN. + maxLength: 5000 + nullable: true + type: string + preferred_language: + description: >- + Preferred language of the Bancontact authorization page that the + customer is redirected to. + + Can be one of `en`, `de`, `fr`, or `nl` + enum: + - de + - en + - fr + - nl + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by + Bancontact directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_bancontact + type: object + x-expandableFields: + - generated_sepa_debit + - generated_sepa_debit_mandate + payment_method_details_billie: + description: '' + properties: {} + title: payment_method_details_billie + type: object + x-expandableFields: [] + payment_method_details_blik: + description: '' + properties: + buyer_id: + description: A unique and immutable identifier assigned by BLIK to every buyer. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_blik + type: object + x-expandableFields: [] + payment_method_details_boleto: + description: '' + properties: + tax_id: + description: >- + The tax ID of the customer (CPF for individuals consumers or CNPJ + for businesses consumers) + maxLength: 5000 + type: string + required: + - tax_id + title: payment_method_details_boleto + type: object + x-expandableFields: [] + payment_method_details_card: + description: '' + properties: + amount_authorized: + description: The authorized amount. + nullable: true + type: integer + authorization_code: + description: Authorization code on the charge. + maxLength: 5000 + nullable: true + type: string + brand: + description: >- + Card brand. Can be `amex`, `diners`, `discover`, `eftpos_au`, `jcb`, + `link`, `mastercard`, `unionpay`, `visa`, or `unknown`. + maxLength: 5000 + nullable: true + type: string + capture_before: + description: >- + When using manual capture, a future timestamp at which the charge + will be automatically refunded if uncaptured. + format: unix-time + type: integer + checks: + anyOf: + - $ref: '#/components/schemas/payment_method_details_card_checks' + description: >- + Check results by Card networks on Card address and CVC at time of + payment. + nullable: true + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + type: integer + extended_authorization: + $ref: >- + #/components/schemas/payment_flows_private_payment_methods_card_details_api_resource_enterprise_features_extended_authorization_extended_authorization + fingerprint: + description: >- + Uniquely identifies this particular card number. You can use this + attribute to check whether two customers who’ve signed up with you + are using the same card number, for example. For payment methods + that tokenize card information (Apple Pay, Google Pay), the + tokenized number might be provided instead of the underlying card + number. + + + *As of May 1, 2021, card fingerprint in India for Connect changed to + allow two fingerprints for the same card---one for India and one for + the rest of the world.* + maxLength: 5000 + nullable: true + type: string + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + incremental_authorization: + $ref: >- + #/components/schemas/payment_flows_private_payment_methods_card_details_api_resource_enterprise_features_incremental_authorization_incremental_authorization + installments: + anyOf: + - $ref: '#/components/schemas/payment_method_details_card_installments' + description: >- + Installment details for this payment (Mexico only). + + + For more information, see the [installments integration + guide](https://stripe.com/docs/payments/installments). + nullable: true + last4: + description: The last four digits of the card. + maxLength: 5000 + nullable: true + type: string + mandate: + description: ID of the mandate used to make this payment or created by it. + maxLength: 5000 + nullable: true + type: string + multicapture: + $ref: >- + #/components/schemas/payment_flows_private_payment_methods_card_details_api_resource_multicapture + network: + description: >- + Identifies which network this charge was processed on. Can be + `amex`, `cartes_bancaires`, `diners`, `discover`, `eftpos_au`, + `interac`, `jcb`, `link`, `mastercard`, `unionpay`, `visa`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + network_token: + anyOf: + - $ref: '#/components/schemas/payment_method_details_card_network_token' + description: >- + If this card has network token credentials, this contains the + details of the network token credentials. + nullable: true + network_transaction_id: + description: >- + This is used by the financial networks to identify a transaction. + Visa calls this the Transaction ID, Mastercard calls this the Trace + ID, and American Express calls this the Acquirer Reference Data. + This value will be present if it is returned by the financial + network in the authorization response, and null otherwise. + maxLength: 5000 + nullable: true + type: string + overcapture: + $ref: >- + #/components/schemas/payment_flows_private_payment_methods_card_details_api_resource_enterprise_features_overcapture_overcapture + regulated_status: + description: Status of a card based on the card issuer. + enum: + - regulated + - unregulated + nullable: true + type: string + three_d_secure: + anyOf: + - $ref: '#/components/schemas/three_d_secure_details_charge' + description: Populated if this transaction used 3D Secure authentication. + nullable: true + wallet: + anyOf: + - $ref: '#/components/schemas/payment_method_details_card_wallet' + description: >- + If this Card is part of a card wallet, this contains the details of + the card wallet. + nullable: true + required: + - exp_month + - exp_year + title: payment_method_details_card + type: object + x-expandableFields: + - checks + - extended_authorization + - incremental_authorization + - installments + - multicapture + - network_token + - overcapture + - three_d_secure + - wallet + payment_method_details_card_checks: + description: '' + properties: + address_line1_check: + description: >- + If a address line1 was provided, results of the check, one of + `pass`, `fail`, `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + address_postal_code_check: + description: >- + If a address postal code was provided, results of the check, one of + `pass`, `fail`, `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + cvc_check: + description: >- + If a CVC was provided, results of the check, one of `pass`, `fail`, + `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_card_checks + type: object + x-expandableFields: [] + payment_method_details_card_installments: + description: '' + properties: + plan: + anyOf: + - $ref: >- + #/components/schemas/payment_method_details_card_installments_plan + description: Installment plan selected for the payment. + nullable: true + title: payment_method_details_card_installments + type: object + x-expandableFields: + - plan + payment_method_details_card_installments_plan: + description: '' + properties: + count: + description: >- + For `fixed_count` installment plans, this is the number of + installment payments your customer will make to their credit card. + nullable: true + type: integer + interval: + description: >- + For `fixed_count` installment plans, this is the interval between + installment payments your customer will make to their credit card. + + One of `month`. + enum: + - month + nullable: true + type: string + type: + description: 'Type of installment plan, one of `fixed_count`.' + enum: + - fixed_count + type: string + x-stripeBypassValidation: true + required: + - type + title: payment_method_details_card_installments_plan + type: object + x-expandableFields: [] + payment_method_details_card_network_token: + description: '' + properties: + used: + description: >- + Indicates if Stripe used a network token, either user provided or + Stripe managed when processing the transaction. + type: boolean + required: + - used + title: payment_method_details_card_network_token + type: object + x-expandableFields: [] + payment_method_details_card_present: + description: '' + properties: + amount_authorized: + description: The authorized amount + nullable: true + type: integer + brand: + description: >- + Card brand. Can be `amex`, `diners`, `discover`, `eftpos_au`, `jcb`, + `link`, `mastercard`, `unionpay`, `visa`, or `unknown`. + maxLength: 5000 + nullable: true + type: string + brand_product: + description: >- + The [product code](https://stripe.com/docs/card-product-codes) that + identifies the specific program or product associated with a card. + maxLength: 5000 + nullable: true + type: string + capture_before: + description: >- + When using manual capture, a future timestamp after which the charge + will be automatically refunded if uncaptured. + format: unix-time + type: integer + cardholder_name: + description: >- + The cardholder name as read from the card, in [ISO + 7813](https://en.wikipedia.org/wiki/ISO/IEC_7813) format. May + include alphanumeric characters, special characters and first/last + name separator (`/`). In some cases, the cardholder name may not be + available depending on how the issuer has configured the card. + Cardholder name is typically not available on swipe or contactless + payments, such as those made with Apple Pay and Google Pay. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + description: + description: A high-level description of the type of cards issued in this range. + maxLength: 5000 + nullable: true + type: string + emv_auth_data: + description: Authorization response cryptogram. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + type: integer + fingerprint: + description: >- + Uniquely identifies this particular card number. You can use this + attribute to check whether two customers who’ve signed up with you + are using the same card number, for example. For payment methods + that tokenize card information (Apple Pay, Google Pay), the + tokenized number might be provided instead of the underlying card + number. + + + *As of May 1, 2021, card fingerprint in India for Connect changed to + allow two fingerprints for the same card---one for India and one for + the rest of the world.* + maxLength: 5000 + nullable: true + type: string + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + generated_card: + description: >- + ID of a card PaymentMethod generated from the card_present + PaymentMethod that may be attached to a Customer for future + transactions. Only present if it was possible to generate a card + PaymentMethod. + maxLength: 5000 + nullable: true + type: string + incremental_authorization_supported: + description: >- + Whether this + [PaymentIntent](https://stripe.com/docs/api/payment_intents) is + eligible for incremental authorizations. Request support using + [request_incremental_authorization_support](https://stripe.com/docs/api/payment_intents/create#create_payment_intent-payment_method_options-card_present-request_incremental_authorization_support). + type: boolean + issuer: + description: The name of the card's issuing bank. + maxLength: 5000 + nullable: true + type: string + last4: + description: The last four digits of the card. + maxLength: 5000 + nullable: true + type: string + network: + description: >- + Identifies which network this charge was processed on. Can be + `amex`, `cartes_bancaires`, `diners`, `discover`, `eftpos_au`, + `interac`, `jcb`, `link`, `mastercard`, `unionpay`, `visa`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + network_transaction_id: + description: >- + This is used by the financial networks to identify a transaction. + Visa calls this the Transaction ID, Mastercard calls this the Trace + ID, and American Express calls this the Acquirer Reference Data. + This value will be present if it is returned by the financial + network in the authorization response, and null otherwise. + maxLength: 5000 + nullable: true + type: string + offline: + anyOf: + - $ref: '#/components/schemas/payment_method_details_card_present_offline' + description: Details about payments collected offline. + nullable: true + overcapture_supported: + description: Defines whether the authorized amount can be over-captured or not + type: boolean + preferred_locales: + description: >- + EMV tag 5F2D. Preferred languages specified by the integrated + circuit chip. + items: + maxLength: 5000 + type: string + nullable: true + type: array + read_method: + description: How card details were read in this transaction. + enum: + - contact_emv + - contactless_emv + - contactless_magstripe_mode + - magnetic_stripe_fallback + - magnetic_stripe_track2 + nullable: true + type: string + receipt: + anyOf: + - $ref: '#/components/schemas/payment_method_details_card_present_receipt' + description: >- + A collection of fields required to be displayed on receipts. Only + required for EMV transactions. + nullable: true + wallet: + $ref: >- + #/components/schemas/payment_flows_private_payment_methods_card_present_common_wallet + required: + - exp_month + - exp_year + - incremental_authorization_supported + - overcapture_supported + title: payment_method_details_card_present + type: object + x-expandableFields: + - offline + - receipt + - wallet + payment_method_details_card_present_offline: + description: '' + properties: + stored_at: + description: Time at which the payment was collected while offline + format: unix-time + nullable: true + type: integer + type: + description: >- + The method used to process this payment method offline. Only + deferred is allowed. + enum: + - deferred + nullable: true + type: string + title: payment_method_details_card_present_offline + type: object + x-expandableFields: [] + payment_method_details_card_present_receipt: + description: '' + properties: + account_type: + description: The type of account being debited or credited + enum: + - checking + - credit + - prepaid + - unknown + type: string + x-stripeBypassValidation: true + application_cryptogram: + description: 'EMV tag 9F26, cryptogram generated by the integrated circuit chip.' + maxLength: 5000 + nullable: true + type: string + application_preferred_name: + description: Mnenomic of the Application Identifier. + maxLength: 5000 + nullable: true + type: string + authorization_code: + description: Identifier for this transaction. + maxLength: 5000 + nullable: true + type: string + authorization_response_code: + description: EMV tag 8A. A code returned by the card issuer. + maxLength: 5000 + nullable: true + type: string + cardholder_verification_method: + description: >- + Describes the method used by the cardholder to verify ownership of + the card. One of the following: `approval`, `failure`, `none`, + `offline_pin`, `offline_pin_and_signature`, `online_pin`, or + `signature`. + maxLength: 5000 + nullable: true + type: string + dedicated_file_name: + description: >- + EMV tag 84. Similar to the application identifier stored on the + integrated circuit chip. + maxLength: 5000 + nullable: true + type: string + terminal_verification_results: + description: >- + The outcome of a series of EMV functions performed by the card + reader. + maxLength: 5000 + nullable: true + type: string + transaction_status_information: + description: >- + An indication of various EMV functions performed during the + transaction. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_card_present_receipt + type: object + x-expandableFields: [] + payment_method_details_card_wallet: + description: '' + properties: + amex_express_checkout: + $ref: >- + #/components/schemas/payment_method_details_card_wallet_amex_express_checkout + apple_pay: + $ref: '#/components/schemas/payment_method_details_card_wallet_apple_pay' + dynamic_last4: + description: >- + (For tokenized numbers only.) The last four digits of the device + account number. + maxLength: 5000 + nullable: true + type: string + google_pay: + $ref: '#/components/schemas/payment_method_details_card_wallet_google_pay' + link: + $ref: '#/components/schemas/payment_method_details_card_wallet_link' + masterpass: + $ref: '#/components/schemas/payment_method_details_card_wallet_masterpass' + samsung_pay: + $ref: '#/components/schemas/payment_method_details_card_wallet_samsung_pay' + type: + description: >- + The type of the card wallet, one of `amex_express_checkout`, + `apple_pay`, `google_pay`, `masterpass`, `samsung_pay`, + `visa_checkout`, or `link`. An additional hash is included on the + Wallet subhash with a name matching this value. It contains + additional information specific to the card wallet type. + enum: + - amex_express_checkout + - apple_pay + - google_pay + - link + - masterpass + - samsung_pay + - visa_checkout + type: string + visa_checkout: + $ref: >- + #/components/schemas/payment_method_details_card_wallet_visa_checkout + required: + - type + title: payment_method_details_card_wallet + type: object + x-expandableFields: + - amex_express_checkout + - apple_pay + - google_pay + - link + - masterpass + - samsung_pay + - visa_checkout + payment_method_details_card_wallet_amex_express_checkout: + description: '' + properties: {} + title: payment_method_details_card_wallet_amex_express_checkout + type: object + x-expandableFields: [] + payment_method_details_card_wallet_apple_pay: + description: '' + properties: {} + title: payment_method_details_card_wallet_apple_pay + type: object + x-expandableFields: [] + payment_method_details_card_wallet_google_pay: + description: '' + properties: {} + title: payment_method_details_card_wallet_google_pay + type: object + x-expandableFields: [] + payment_method_details_card_wallet_link: + description: '' + properties: {} + title: payment_method_details_card_wallet_link + type: object + x-expandableFields: [] + payment_method_details_card_wallet_masterpass: + description: '' + properties: + billing_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Owner's verified billing address. Values are verified or provided by + the wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + nullable: true + email: + description: >- + Owner's verified email. Values are verified or provided by the + wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + name: + description: >- + Owner's verified full name. Values are verified or provided by the + wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + shipping_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Owner's verified shipping address. Values are verified or provided + by the wallet directly (if supported) at the time of authorization + or settlement. They cannot be set or mutated. + nullable: true + title: payment_method_details_card_wallet_masterpass + type: object + x-expandableFields: + - billing_address + - shipping_address + payment_method_details_card_wallet_samsung_pay: + description: '' + properties: {} + title: payment_method_details_card_wallet_samsung_pay + type: object + x-expandableFields: [] + payment_method_details_card_wallet_visa_checkout: + description: '' + properties: + billing_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Owner's verified billing address. Values are verified or provided by + the wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + nullable: true + email: + description: >- + Owner's verified email. Values are verified or provided by the + wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + name: + description: >- + Owner's verified full name. Values are verified or provided by the + wallet directly (if supported) at the time of authorization or + settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + shipping_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Owner's verified shipping address. Values are verified or provided + by the wallet directly (if supported) at the time of authorization + or settlement. They cannot be set or mutated. + nullable: true + title: payment_method_details_card_wallet_visa_checkout + type: object + x-expandableFields: + - billing_address + - shipping_address + payment_method_details_cashapp: + description: '' + properties: + buyer_id: + description: >- + A unique and immutable identifier assigned by Cash App to every + buyer. + maxLength: 5000 + nullable: true + type: string + cashtag: + description: A public identifier for buyers using Cash App. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_cashapp + type: object + x-expandableFields: [] + payment_method_details_customer_balance: + description: '' + properties: {} + title: payment_method_details_customer_balance + type: object + x-expandableFields: [] + payment_method_details_eps: + description: '' + properties: + bank: + description: >- + The customer's bank. Should be one of `arzte_und_apotheker_bank`, + `austrian_anadi_bank_ag`, `bank_austria`, `bankhaus_carl_spangler`, + `bankhaus_schelhammer_und_schattera_ag`, `bawag_psk_ag`, + `bks_bank_ag`, `brull_kallmus_bank_ag`, `btv_vier_lander_bank`, + `capital_bank_grawe_gruppe_ag`, `deutsche_bank_ag`, `dolomitenbank`, + `easybank_ag`, `erste_bank_und_sparkassen`, + `hypo_alpeadriabank_international_ag`, + `hypo_noe_lb_fur_niederosterreich_u_wien`, + `hypo_oberosterreich_salzburg_steiermark`, `hypo_tirol_bank_ag`, + `hypo_vorarlberg_bank_ag`, + `hypo_bank_burgenland_aktiengesellschaft`, `marchfelder_bank`, + `oberbank_ag`, `raiffeisen_bankengruppe_osterreich`, + `schoellerbank_ag`, `sparda_bank_wien`, `volksbank_gruppe`, + `volkskreditbank_ag`, or `vr_bank_braunau`. + enum: + - arzte_und_apotheker_bank + - austrian_anadi_bank_ag + - bank_austria + - bankhaus_carl_spangler + - bankhaus_schelhammer_und_schattera_ag + - bawag_psk_ag + - bks_bank_ag + - brull_kallmus_bank_ag + - btv_vier_lander_bank + - capital_bank_grawe_gruppe_ag + - deutsche_bank_ag + - dolomitenbank + - easybank_ag + - erste_bank_und_sparkassen + - hypo_alpeadriabank_international_ag + - hypo_bank_burgenland_aktiengesellschaft + - hypo_noe_lb_fur_niederosterreich_u_wien + - hypo_oberosterreich_salzburg_steiermark + - hypo_tirol_bank_ag + - hypo_vorarlberg_bank_ag + - marchfelder_bank + - oberbank_ag + - raiffeisen_bankengruppe_osterreich + - schoellerbank_ag + - sparda_bank_wien + - volksbank_gruppe + - volkskreditbank_ag + - vr_bank_braunau + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by EPS + directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + + EPS rarely provides this information so the attribute is usually + empty. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_eps + type: object + x-expandableFields: [] + payment_method_details_fpx: + description: '' + properties: + bank: + description: >- + The customer's bank. Can be one of `affin_bank`, `agrobank`, + `alliance_bank`, `ambank`, `bank_islam`, `bank_muamalat`, + `bank_rakyat`, `bsn`, `cimb`, `hong_leong_bank`, `hsbc`, `kfh`, + `maybank2u`, `ocbc`, `public_bank`, `rhb`, `standard_chartered`, + `uob`, `deutsche_bank`, `maybank2e`, `pb_enterprise`, or + `bank_of_china`. + enum: + - affin_bank + - agrobank + - alliance_bank + - ambank + - bank_islam + - bank_muamalat + - bank_of_china + - bank_rakyat + - bsn + - cimb + - deutsche_bank + - hong_leong_bank + - hsbc + - kfh + - maybank2e + - maybank2u + - ocbc + - pb_enterprise + - public_bank + - rhb + - standard_chartered + - uob + type: string + transaction_id: + description: >- + Unique transaction id generated by FPX for every request from the + merchant + maxLength: 5000 + nullable: true + type: string + required: + - bank + title: payment_method_details_fpx + type: object + x-expandableFields: [] + payment_method_details_giropay: + description: '' + properties: + bank_code: + description: Bank code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bic: + description: Bank Identifier Code of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by + Giropay directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + + Giropay rarely provides this information so the attribute is usually + empty. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_giropay + type: object + x-expandableFields: [] + payment_method_details_grabpay: + description: '' + properties: + transaction_id: + description: Unique transaction id generated by GrabPay + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_grabpay + type: object + x-expandableFields: [] + payment_method_details_ideal: + description: '' + properties: + bank: + description: >- + The customer's bank. Can be one of `abn_amro`, `asn_bank`, `bunq`, + `handelsbanken`, `ing`, `knab`, `moneyou`, `n26`, `nn`, `rabobank`, + `regiobank`, `revolut`, `sns_bank`, `triodos_bank`, `van_lanschot`, + or `yoursafe`. + enum: + - abn_amro + - asn_bank + - bunq + - handelsbanken + - ing + - knab + - moneyou + - n26 + - nn + - rabobank + - regiobank + - revolut + - sns_bank + - triodos_bank + - van_lanschot + - yoursafe + nullable: true + type: string + bic: + description: The Bank Identifier Code of the customer's bank. + enum: + - ABNANL2A + - ASNBNL21 + - BITSNL2A + - BUNQNL2A + - FVLBNL22 + - HANDNL2A + - INGBNL2A + - KNABNL2H + - MOYONL21 + - NNBANL2G + - NTSBDEB1 + - RABONL2U + - RBRBNL21 + - REVOIE23 + - REVOLT21 + - SNSBNL2A + - TRIONL2U + nullable: true + type: string + generated_sepa_debit: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + The ID of the SEPA Direct Debit PaymentMethod which was generated by + this Charge. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + generated_sepa_debit_mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: >- + The mandate for the SEPA Direct Debit PaymentMethod which was + generated by this Charge. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + iban_last4: + description: Last four characters of the IBAN. + maxLength: 5000 + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by iDEAL + directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_ideal + type: object + x-expandableFields: + - generated_sepa_debit + - generated_sepa_debit_mandate + payment_method_details_interac_present: + description: '' + properties: + brand: + description: 'Card brand. Can be `interac`, `mastercard` or `visa`.' + maxLength: 5000 + nullable: true + type: string + cardholder_name: + description: >- + The cardholder name as read from the card, in [ISO + 7813](https://en.wikipedia.org/wiki/ISO/IEC_7813) format. May + include alphanumeric characters, special characters and first/last + name separator (`/`). In some cases, the cardholder name may not be + available depending on how the issuer has configured the card. + Cardholder name is typically not available on swipe or contactless + payments, such as those made with Apple Pay and Google Pay. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + description: + description: A high-level description of the type of cards issued in this range. + maxLength: 5000 + nullable: true + type: string + emv_auth_data: + description: Authorization response cryptogram. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + type: integer + fingerprint: + description: >- + Uniquely identifies this particular card number. You can use this + attribute to check whether two customers who’ve signed up with you + are using the same card number, for example. For payment methods + that tokenize card information (Apple Pay, Google Pay), the + tokenized number might be provided instead of the underlying card + number. + + + *As of May 1, 2021, card fingerprint in India for Connect changed to + allow two fingerprints for the same card---one for India and one for + the rest of the world.* + maxLength: 5000 + nullable: true + type: string + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + generated_card: + description: >- + ID of a card PaymentMethod generated from the card_present + PaymentMethod that may be attached to a Customer for future + transactions. Only present if it was possible to generate a card + PaymentMethod. + maxLength: 5000 + nullable: true + type: string + issuer: + description: The name of the card's issuing bank. + maxLength: 5000 + nullable: true + type: string + last4: + description: The last four digits of the card. + maxLength: 5000 + nullable: true + type: string + network: + description: >- + Identifies which network this charge was processed on. Can be + `amex`, `cartes_bancaires`, `diners`, `discover`, `eftpos_au`, + `interac`, `jcb`, `link`, `mastercard`, `unionpay`, `visa`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + network_transaction_id: + description: >- + This is used by the financial networks to identify a transaction. + Visa calls this the Transaction ID, Mastercard calls this the Trace + ID, and American Express calls this the Acquirer Reference Data. + This value will be present if it is returned by the financial + network in the authorization response, and null otherwise. + maxLength: 5000 + nullable: true + type: string + preferred_locales: + description: >- + EMV tag 5F2D. Preferred languages specified by the integrated + circuit chip. + items: + maxLength: 5000 + type: string + nullable: true + type: array + read_method: + description: How card details were read in this transaction. + enum: + - contact_emv + - contactless_emv + - contactless_magstripe_mode + - magnetic_stripe_fallback + - magnetic_stripe_track2 + nullable: true + type: string + receipt: + anyOf: + - $ref: >- + #/components/schemas/payment_method_details_interac_present_receipt + description: >- + A collection of fields required to be displayed on receipts. Only + required for EMV transactions. + nullable: true + required: + - exp_month + - exp_year + title: payment_method_details_interac_present + type: object + x-expandableFields: + - receipt + payment_method_details_interac_present_receipt: + description: '' + properties: + account_type: + description: The type of account being debited or credited + enum: + - checking + - savings + - unknown + type: string + x-stripeBypassValidation: true + application_cryptogram: + description: 'EMV tag 9F26, cryptogram generated by the integrated circuit chip.' + maxLength: 5000 + nullable: true + type: string + application_preferred_name: + description: Mnenomic of the Application Identifier. + maxLength: 5000 + nullable: true + type: string + authorization_code: + description: Identifier for this transaction. + maxLength: 5000 + nullable: true + type: string + authorization_response_code: + description: EMV tag 8A. A code returned by the card issuer. + maxLength: 5000 + nullable: true + type: string + cardholder_verification_method: + description: >- + Describes the method used by the cardholder to verify ownership of + the card. One of the following: `approval`, `failure`, `none`, + `offline_pin`, `offline_pin_and_signature`, `online_pin`, or + `signature`. + maxLength: 5000 + nullable: true + type: string + dedicated_file_name: + description: >- + EMV tag 84. Similar to the application identifier stored on the + integrated circuit chip. + maxLength: 5000 + nullable: true + type: string + terminal_verification_results: + description: >- + The outcome of a series of EMV functions performed by the card + reader. + maxLength: 5000 + nullable: true + type: string + transaction_status_information: + description: >- + An indication of various EMV functions performed during the + transaction. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_interac_present_receipt + type: object + x-expandableFields: [] + payment_method_details_kakao_pay: + description: '' + properties: + buyer_id: + description: >- + A unique identifier for the buyer as determined by the local payment + processor. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_kakao_pay + type: object + x-expandableFields: [] + payment_method_details_klarna: + description: '' + properties: + payer_details: + anyOf: + - $ref: '#/components/schemas/klarna_payer_details' + description: The payer details for this transaction. + nullable: true + payment_method_category: + description: >- + The Klarna payment method used for this transaction. + + Can be one of `pay_later`, `pay_now`, `pay_with_financing`, or + `pay_in_installments` + maxLength: 5000 + nullable: true + type: string + preferred_locale: + description: >- + Preferred language of the Klarna authorization page that the + customer is redirected to. + + Can be one of `de-AT`, `en-AT`, `nl-BE`, `fr-BE`, `en-BE`, `de-DE`, + `en-DE`, `da-DK`, `en-DK`, `es-ES`, `en-ES`, `fi-FI`, `sv-FI`, + `en-FI`, `en-GB`, `en-IE`, `it-IT`, `en-IT`, `nl-NL`, `en-NL`, + `nb-NO`, `en-NO`, `sv-SE`, `en-SE`, `en-US`, `es-US`, `fr-FR`, + `en-FR`, `cs-CZ`, `en-CZ`, `ro-RO`, `en-RO`, `el-GR`, `en-GR`, + `en-AU`, `en-NZ`, `en-CA`, `fr-CA`, `pl-PL`, `en-PL`, `pt-PT`, + `en-PT`, `de-CH`, `fr-CH`, `it-CH`, or `en-CH` + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_klarna + type: object + x-expandableFields: + - payer_details + payment_method_details_konbini: + description: '' + properties: + store: + anyOf: + - $ref: '#/components/schemas/payment_method_details_konbini_store' + description: >- + If the payment succeeded, this contains the details of the + convenience store where the payment was completed. + nullable: true + title: payment_method_details_konbini + type: object + x-expandableFields: + - store + payment_method_details_konbini_store: + description: '' + properties: + chain: + description: >- + The name of the convenience store chain where the payment was + completed. + enum: + - familymart + - lawson + - ministop + - seicomart + nullable: true + type: string + title: payment_method_details_konbini_store + type: object + x-expandableFields: [] + payment_method_details_kr_card: + description: '' + properties: + brand: + description: The local credit or debit card brand. + enum: + - bc + - citi + - hana + - hyundai + - jeju + - jeonbuk + - kakaobank + - kbank + - kdbbank + - kookmin + - kwangju + - lotte + - mg + - nh + - post + - samsung + - savingsbank + - shinhan + - shinhyup + - suhyup + - tossbank + - woori + nullable: true + type: string + buyer_id: + description: >- + A unique identifier for the buyer as determined by the local payment + processor. + maxLength: 5000 + nullable: true + type: string + last4: + description: >- + The last four digits of the card. This may not be present for + American Express cards. + maxLength: 4 + nullable: true + type: string + title: payment_method_details_kr_card + type: object + x-expandableFields: [] + payment_method_details_link: + description: '' + properties: + country: + description: >- + Two-letter ISO code representing the funding source country beneath + the Link payment. + + You could use this attribute to get a sense of international fees. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_link + type: object + x-expandableFields: [] + payment_method_details_mobilepay: + description: '' + properties: + card: + anyOf: + - $ref: '#/components/schemas/internal_card' + description: Internal card details + nullable: true + title: payment_method_details_mobilepay + type: object + x-expandableFields: + - card + payment_method_details_multibanco: + description: '' + properties: + entity: + description: Entity number associated with this Multibanco payment. + maxLength: 5000 + nullable: true + type: string + reference: + description: Reference number associated with this Multibanco payment. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_multibanco + type: object + x-expandableFields: [] + payment_method_details_naver_pay: + description: '' + properties: + buyer_id: + description: >- + A unique identifier for the buyer as determined by the local payment + processor. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_naver_pay + type: object + x-expandableFields: [] + payment_method_details_nz_bank_account: + description: '' + properties: + account_holder_name: + description: >- + The name on the bank account. Only present if the account holder + name is different from the name of the authorized signatory + collected in the PaymentMethod’s billing details. + maxLength: 5000 + nullable: true + type: string + bank_code: + description: The numeric code for the bank account's bank. + maxLength: 5000 + type: string + bank_name: + description: The name of the bank. + maxLength: 5000 + type: string + branch_code: + description: The numeric code for the bank account's bank branch. + maxLength: 5000 + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + type: string + suffix: + description: The suffix of the bank account number. + maxLength: 5000 + nullable: true + type: string + required: + - bank_code + - bank_name + - branch_code + - last4 + title: payment_method_details_nz_bank_account + type: object + x-expandableFields: [] + payment_method_details_oxxo: + description: '' + properties: + number: + description: OXXO reference number + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_oxxo + type: object + x-expandableFields: [] + payment_method_details_p24: + description: '' + properties: + bank: + description: >- + The customer's bank. Can be one of `ing`, `citi_handlowy`, + `tmobile_usbugi_bankowe`, `plus_bank`, `etransfer_pocztowy24`, + `banki_spbdzielcze`, `bank_nowy_bfg_sa`, `getin_bank`, `velobank`, + `blik`, `noble_pay`, `ideabank`, `envelobank`, + `santander_przelew24`, `nest_przelew`, `mbank_mtransfer`, + `inteligo`, `pbac_z_ipko`, `bnp_paribas`, `credit_agricole`, + `toyota_bank`, `bank_pekao_sa`, `volkswagen_bank`, + `bank_millennium`, `alior_bank`, or `boz`. + enum: + - alior_bank + - bank_millennium + - bank_nowy_bfg_sa + - bank_pekao_sa + - banki_spbdzielcze + - blik + - bnp_paribas + - boz + - citi_handlowy + - credit_agricole + - envelobank + - etransfer_pocztowy24 + - getin_bank + - ideabank + - ing + - inteligo + - mbank_mtransfer + - nest_przelew + - noble_pay + - pbac_z_ipko + - plus_bank + - santander_przelew24 + - tmobile_usbugi_bankowe + - toyota_bank + - velobank + - volkswagen_bank + nullable: true + type: string + reference: + description: Unique reference for this Przelewy24 payment. + maxLength: 5000 + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by + Przelewy24 directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + + Przelewy24 rarely provides this information so the attribute is + usually empty. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_p24 + type: object + x-expandableFields: [] + payment_method_details_passthrough_card: + description: '' + properties: + brand: + description: >- + Card brand. Can be `amex`, `diners`, `discover`, `eftpos_au`, `jcb`, + `link`, `mastercard`, `unionpay`, `visa`, or `unknown`. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + nullable: true + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + nullable: true + type: integer + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + last4: + description: The last four digits of the card. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_passthrough_card + type: object + x-expandableFields: [] + payment_method_details_pay_by_bank: + description: '' + properties: {} + title: payment_method_details_pay_by_bank + type: object + x-expandableFields: [] + payment_method_details_payco: + description: '' + properties: + buyer_id: + description: >- + A unique identifier for the buyer as determined by the local payment + processor. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_payco + type: object + x-expandableFields: [] + payment_method_details_paynow: + description: '' + properties: + reference: + description: Reference number associated with this PayNow payment + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_paynow + type: object + x-expandableFields: [] + payment_method_details_paypal: + description: '' + properties: + country: + description: >- + Two-letter ISO code representing the buyer's country. Values are + provided by PayPal directly (if supported) at the time of + authorization or settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + payer_email: + description: >- + Owner's email. Values are provided by PayPal directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + payer_id: + description: >- + PayPal account PayerID. This identifier uniquely identifies the + PayPal customer. + maxLength: 5000 + nullable: true + type: string + payer_name: + description: >- + Owner's full name. Values provided by PayPal directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + seller_protection: + anyOf: + - $ref: '#/components/schemas/paypal_seller_protection' + description: >- + The level of protection offered as defined by PayPal Seller + Protection for Merchants, for this transaction. + nullable: true + transaction_id: + description: A unique ID generated by PayPal for this transaction. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_paypal + type: object + x-expandableFields: + - seller_protection + payment_method_details_pix: + description: '' + properties: + bank_transaction_id: + description: Unique transaction id generated by BCB + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_pix + type: object + x-expandableFields: [] + payment_method_details_promptpay: + description: '' + properties: + reference: + description: Bill reference generated by PromptPay + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_promptpay + type: object + x-expandableFields: [] + payment_method_details_revolut_pay: + description: '' + properties: + funding: + $ref: >- + #/components/schemas/revolut_pay_underlying_payment_method_funding_details + title: payment_method_details_revolut_pay + type: object + x-expandableFields: + - funding + payment_method_details_samsung_pay: + description: '' + properties: + buyer_id: + description: >- + A unique identifier for the buyer as determined by the local payment + processor. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_samsung_pay + type: object + x-expandableFields: [] + payment_method_details_satispay: + description: '' + properties: {} + title: payment_method_details_satispay + type: object + x-expandableFields: [] + payment_method_details_sepa_debit: + description: '' + properties: + bank_code: + description: Bank code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + branch_code: + description: Branch code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country the bank account is + located in. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four characters of the IBAN. + maxLength: 5000 + nullable: true + type: string + mandate: + description: >- + Find the ID of the mandate used for this payment under the + [payment_method_details.sepa_debit.mandate](https://stripe.com/docs/api/charges/object#charge_object-payment_method_details-sepa_debit-mandate) + property on the Charge. Use this mandate ID to [retrieve the + Mandate](https://stripe.com/docs/api/mandates/retrieve). + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_sepa_debit + type: object + x-expandableFields: [] + payment_method_details_sofort: + description: '' + properties: + bank_code: + description: Bank code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bic: + description: Bank Identifier Code of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country the bank account is + located in. + maxLength: 5000 + nullable: true + type: string + generated_sepa_debit: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + The ID of the SEPA Direct Debit PaymentMethod which was generated by + this Charge. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + generated_sepa_debit_mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: >- + The mandate for the SEPA Direct Debit PaymentMethod which was + generated by this Charge. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + iban_last4: + description: Last four characters of the IBAN. + maxLength: 5000 + nullable: true + type: string + preferred_language: + description: >- + Preferred language of the SOFORT authorization page that the + customer is redirected to. + + Can be one of `de`, `en`, `es`, `fr`, `it`, `nl`, or `pl` + enum: + - de + - en + - es + - fr + - it + - nl + - pl + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by + SOFORT directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_sofort + type: object + x-expandableFields: + - generated_sepa_debit + - generated_sepa_debit_mandate + payment_method_details_stripe_account: + description: '' + properties: {} + title: payment_method_details_stripe_account + type: object + x-expandableFields: [] + payment_method_details_swish: + description: '' + properties: + fingerprint: + description: >- + Uniquely identifies the payer's Swish account. You can use this + attribute to check whether two Swish transactions were paid for by + the same payer + maxLength: 5000 + nullable: true + type: string + payment_reference: + description: Payer bank reference number for the payment + maxLength: 5000 + nullable: true + type: string + verified_phone_last4: + description: The last four digits of the Swish account phone number + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_swish + type: object + x-expandableFields: [] + payment_method_details_twint: + description: '' + properties: {} + title: payment_method_details_twint + type: object + x-expandableFields: [] + payment_method_details_us_bank_account: + description: '' + properties: + account_holder_type: + description: 'Account holder type: individual or company.' + enum: + - company + - individual + nullable: true + type: string + account_type: + description: 'Account type: checkings or savings. Defaults to checking if omitted.' + enum: + - checking + - savings + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: ID of the mandate used to make this payment. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + payment_reference: + description: Reference number to locate ACH payments with customer's bank. + maxLength: 5000 + nullable: true + type: string + routing_number: + description: Routing number of the bank account. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_us_bank_account + type: object + x-expandableFields: + - mandate + payment_method_details_wechat: + description: '' + properties: {} + title: payment_method_details_wechat + type: object + x-expandableFields: [] + payment_method_details_wechat_pay: + description: '' + properties: + fingerprint: + description: >- + Uniquely identifies this particular WeChat Pay account. You can use + this attribute to check whether two WeChat accounts are the same. + maxLength: 5000 + nullable: true + type: string + location: + description: >- + ID of the [location](https://stripe.com/docs/api/terminal/locations) + that this transaction's reader is assigned to. + maxLength: 5000 + type: string + reader: + description: >- + ID of the [reader](https://stripe.com/docs/api/terminal/readers) + this transaction was made on. + maxLength: 5000 + type: string + transaction_id: + description: Transaction ID of this particular WeChat Pay transaction. + maxLength: 5000 + nullable: true + type: string + title: payment_method_details_wechat_pay + type: object + x-expandableFields: [] + payment_method_details_zip: + description: '' + properties: {} + title: payment_method_details_zip + type: object + x-expandableFields: [] + payment_method_domain: + description: >- + A payment method domain represents a web domain that you have registered + with Stripe. + + Stripe Elements use registered payment method domains to control where + certain payment methods are shown. + + + Related guide: [Payment method + domains](https://stripe.com/docs/payments/payment-methods/pmd-registration). + properties: + amazon_pay: + $ref: >- + #/components/schemas/payment_method_domain_resource_payment_method_status + apple_pay: + $ref: >- + #/components/schemas/payment_method_domain_resource_payment_method_status + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + domain_name: + description: The domain name that this payment method domain object represents. + maxLength: 5000 + type: string + enabled: + description: >- + Whether this payment method domain is enabled. If the domain is not + enabled, payment methods that require a payment method domain will + not appear in Elements. + type: boolean + google_pay: + $ref: >- + #/components/schemas/payment_method_domain_resource_payment_method_status + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + klarna: + $ref: >- + #/components/schemas/payment_method_domain_resource_payment_method_status + link: + $ref: >- + #/components/schemas/payment_method_domain_resource_payment_method_status + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - payment_method_domain + type: string + paypal: + $ref: >- + #/components/schemas/payment_method_domain_resource_payment_method_status + required: + - amazon_pay + - apple_pay + - created + - domain_name + - enabled + - google_pay + - id + - klarna + - link + - livemode + - object + - paypal + title: PaymentMethodDomainResourcePaymentMethodDomain + type: object + x-expandableFields: + - amazon_pay + - apple_pay + - google_pay + - klarna + - link + - paypal + x-resourceId: payment_method_domain + payment_method_domain_resource_payment_method_status: + description: >- + Indicates the status of a specific payment method on a payment method + domain. + properties: + status: + description: The status of the payment method on the domain. + enum: + - active + - inactive + type: string + status_details: + $ref: >- + #/components/schemas/payment_method_domain_resource_payment_method_status_details + required: + - status + title: PaymentMethodDomainResourcePaymentMethodStatus + type: object + x-expandableFields: + - status_details + payment_method_domain_resource_payment_method_status_details: + description: >- + Contains additional details about the status of a payment method for a + specific payment method domain. + properties: + error_message: + description: >- + The error message associated with the status of the payment method + on the domain. + maxLength: 5000 + type: string + required: + - error_message + title: PaymentMethodDomainResourcePaymentMethodStatusDetails + type: object + x-expandableFields: [] + payment_method_eps: + description: '' + properties: + bank: + description: >- + The customer's bank. Should be one of `arzte_und_apotheker_bank`, + `austrian_anadi_bank_ag`, `bank_austria`, `bankhaus_carl_spangler`, + `bankhaus_schelhammer_und_schattera_ag`, `bawag_psk_ag`, + `bks_bank_ag`, `brull_kallmus_bank_ag`, `btv_vier_lander_bank`, + `capital_bank_grawe_gruppe_ag`, `deutsche_bank_ag`, `dolomitenbank`, + `easybank_ag`, `erste_bank_und_sparkassen`, + `hypo_alpeadriabank_international_ag`, + `hypo_noe_lb_fur_niederosterreich_u_wien`, + `hypo_oberosterreich_salzburg_steiermark`, `hypo_tirol_bank_ag`, + `hypo_vorarlberg_bank_ag`, + `hypo_bank_burgenland_aktiengesellschaft`, `marchfelder_bank`, + `oberbank_ag`, `raiffeisen_bankengruppe_osterreich`, + `schoellerbank_ag`, `sparda_bank_wien`, `volksbank_gruppe`, + `volkskreditbank_ag`, or `vr_bank_braunau`. + enum: + - arzte_und_apotheker_bank + - austrian_anadi_bank_ag + - bank_austria + - bankhaus_carl_spangler + - bankhaus_schelhammer_und_schattera_ag + - bawag_psk_ag + - bks_bank_ag + - brull_kallmus_bank_ag + - btv_vier_lander_bank + - capital_bank_grawe_gruppe_ag + - deutsche_bank_ag + - dolomitenbank + - easybank_ag + - erste_bank_und_sparkassen + - hypo_alpeadriabank_international_ag + - hypo_bank_burgenland_aktiengesellschaft + - hypo_noe_lb_fur_niederosterreich_u_wien + - hypo_oberosterreich_salzburg_steiermark + - hypo_tirol_bank_ag + - hypo_vorarlberg_bank_ag + - marchfelder_bank + - oberbank_ag + - raiffeisen_bankengruppe_osterreich + - schoellerbank_ag + - sparda_bank_wien + - volksbank_gruppe + - volkskreditbank_ag + - vr_bank_braunau + nullable: true + type: string + title: payment_method_eps + type: object + x-expandableFields: [] + payment_method_fpx: + description: '' + properties: + bank: + description: >- + The customer's bank, if provided. Can be one of `affin_bank`, + `agrobank`, `alliance_bank`, `ambank`, `bank_islam`, + `bank_muamalat`, `bank_rakyat`, `bsn`, `cimb`, `hong_leong_bank`, + `hsbc`, `kfh`, `maybank2u`, `ocbc`, `public_bank`, `rhb`, + `standard_chartered`, `uob`, `deutsche_bank`, `maybank2e`, + `pb_enterprise`, or `bank_of_china`. + enum: + - affin_bank + - agrobank + - alliance_bank + - ambank + - bank_islam + - bank_muamalat + - bank_of_china + - bank_rakyat + - bsn + - cimb + - deutsche_bank + - hong_leong_bank + - hsbc + - kfh + - maybank2e + - maybank2u + - ocbc + - pb_enterprise + - public_bank + - rhb + - standard_chartered + - uob + type: string + required: + - bank + title: payment_method_fpx + type: object + x-expandableFields: [] + payment_method_giropay: + description: '' + properties: {} + title: payment_method_giropay + type: object + x-expandableFields: [] + payment_method_grabpay: + description: '' + properties: {} + title: payment_method_grabpay + type: object + x-expandableFields: [] + payment_method_ideal: + description: '' + properties: + bank: + description: >- + The customer's bank, if provided. Can be one of `abn_amro`, + `asn_bank`, `bunq`, `handelsbanken`, `ing`, `knab`, `moneyou`, + `n26`, `nn`, `rabobank`, `regiobank`, `revolut`, `sns_bank`, + `triodos_bank`, `van_lanschot`, or `yoursafe`. + enum: + - abn_amro + - asn_bank + - bunq + - handelsbanken + - ing + - knab + - moneyou + - n26 + - nn + - rabobank + - regiobank + - revolut + - sns_bank + - triodos_bank + - van_lanschot + - yoursafe + nullable: true + type: string + bic: + description: >- + The Bank Identifier Code of the customer's bank, if the bank was + provided. + enum: + - ABNANL2A + - ASNBNL21 + - BITSNL2A + - BUNQNL2A + - FVLBNL22 + - HANDNL2A + - INGBNL2A + - KNABNL2H + - MOYONL21 + - NNBANL2G + - NTSBDEB1 + - RABONL2U + - RBRBNL21 + - REVOIE23 + - REVOLT21 + - SNSBNL2A + - TRIONL2U + nullable: true + type: string + title: payment_method_ideal + type: object + x-expandableFields: [] + payment_method_interac_present: + description: '' + properties: + brand: + description: 'Card brand. Can be `interac`, `mastercard` or `visa`.' + maxLength: 5000 + nullable: true + type: string + cardholder_name: + description: >- + The cardholder name as read from the card, in [ISO + 7813](https://en.wikipedia.org/wiki/ISO/IEC_7813) format. May + include alphanumeric characters, special characters and first/last + name separator (`/`). In some cases, the cardholder name may not be + available depending on how the issuer has configured the card. + Cardholder name is typically not available on swipe or contactless + payments, such as those made with Apple Pay and Google Pay. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + description: + description: A high-level description of the type of cards issued in this range. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + type: integer + fingerprint: + description: >- + Uniquely identifies this particular card number. You can use this + attribute to check whether two customers who’ve signed up with you + are using the same card number, for example. For payment methods + that tokenize card information (Apple Pay, Google Pay), the + tokenized number might be provided instead of the underlying card + number. + + + *As of May 1, 2021, card fingerprint in India for Connect changed to + allow two fingerprints for the same card---one for India and one for + the rest of the world.* + maxLength: 5000 + nullable: true + type: string + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + issuer: + description: The name of the card's issuing bank. + maxLength: 5000 + nullable: true + type: string + last4: + description: The last four digits of the card. + maxLength: 5000 + nullable: true + type: string + networks: + anyOf: + - $ref: '#/components/schemas/payment_method_card_present_networks' + description: >- + Contains information about card networks that can be used to process + the payment. + nullable: true + preferred_locales: + description: >- + EMV tag 5F2D. Preferred languages specified by the integrated + circuit chip. + items: + maxLength: 5000 + type: string + nullable: true + type: array + read_method: + description: How card details were read in this transaction. + enum: + - contact_emv + - contactless_emv + - contactless_magstripe_mode + - magnetic_stripe_fallback + - magnetic_stripe_track2 + nullable: true + type: string + required: + - exp_month + - exp_year + title: payment_method_interac_present + type: object + x-expandableFields: + - networks + payment_method_kakao_pay: + description: '' + properties: {} + title: payment_method_kakao_pay + type: object + x-expandableFields: [] + payment_method_klarna: + description: '' + properties: + dob: + anyOf: + - $ref: >- + #/components/schemas/payment_flows_private_payment_methods_klarna_dob + description: 'The customer''s date of birth, if provided.' + nullable: true + title: payment_method_klarna + type: object + x-expandableFields: + - dob + payment_method_konbini: + description: '' + properties: {} + title: payment_method_konbini + type: object + x-expandableFields: [] + payment_method_kr_card: + description: '' + properties: + brand: + description: The local credit or debit card brand. + enum: + - bc + - citi + - hana + - hyundai + - jeju + - jeonbuk + - kakaobank + - kbank + - kdbbank + - kookmin + - kwangju + - lotte + - mg + - nh + - post + - samsung + - savingsbank + - shinhan + - shinhyup + - suhyup + - tossbank + - woori + nullable: true + type: string + last4: + description: >- + The last four digits of the card. This may not be present for + American Express cards. + maxLength: 4 + nullable: true + type: string + title: payment_method_kr_card + type: object + x-expandableFields: [] + payment_method_link: + description: '' + properties: + email: + description: Account owner's email address. + maxLength: 5000 + nullable: true + type: string + title: payment_method_link + type: object + x-expandableFields: [] + payment_method_mobilepay: + description: '' + properties: {} + title: payment_method_mobilepay + type: object + x-expandableFields: [] + payment_method_multibanco: + description: '' + properties: {} + title: payment_method_multibanco + type: object + x-expandableFields: [] + payment_method_naver_pay: + description: '' + properties: + buyer_id: + description: >- + Uniquely identifies this particular Naver Pay account. You can use + this attribute to check whether two Naver Pay accounts are the same. + maxLength: 5000 + nullable: true + type: string + funding: + description: Whether to fund this transaction with Naver Pay points or a card. + enum: + - card + - points + type: string + x-stripeBypassValidation: true + required: + - funding + title: payment_method_naver_pay + type: object + x-expandableFields: [] + payment_method_nz_bank_account: + description: '' + properties: + account_holder_name: + description: >- + The name on the bank account. Only present if the account holder + name is different from the name of the authorized signatory + collected in the PaymentMethod’s billing details. + maxLength: 5000 + nullable: true + type: string + bank_code: + description: The numeric code for the bank account's bank. + maxLength: 5000 + type: string + bank_name: + description: The name of the bank. + maxLength: 5000 + type: string + branch_code: + description: The numeric code for the bank account's bank branch. + maxLength: 5000 + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + type: string + suffix: + description: The suffix of the bank account number. + maxLength: 5000 + nullable: true + type: string + required: + - bank_code + - bank_name + - branch_code + - last4 + title: payment_method_nz_bank_account + type: object + x-expandableFields: [] + payment_method_options_affirm: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + preferred_locale: + description: >- + Preferred language of the Affirm authorization page that the + customer is redirected to. + maxLength: 30 + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_affirm + type: object + x-expandableFields: [] + payment_method_options_afterpay_clearpay: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + reference: + description: >- + An internal identifier or reference that this payment corresponds + to. You must limit the identifier to 128 characters, and it can only + contain letters, numbers, underscores, backslashes, and dashes. + + This field differs from the statement descriptor and item name. + maxLength: 5000 + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + x-stripeBypassValidation: true + title: payment_method_options_afterpay_clearpay + type: object + x-expandableFields: [] + payment_method_options_alipay: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: payment_method_options_alipay + type: object + x-expandableFields: [] + payment_method_options_alma: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + title: payment_method_options_alma + type: object + x-expandableFields: [] + payment_method_options_amazon_pay: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: payment_method_options_amazon_pay + type: object + x-expandableFields: [] + payment_method_options_bancontact: + description: '' + properties: + preferred_language: + description: >- + Preferred language of the Bancontact authorization page that the + customer is redirected to. + enum: + - de + - en + - fr + - nl + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + required: + - preferred_language + title: payment_method_options_bancontact + type: object + x-expandableFields: [] + payment_method_options_billie: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + title: payment_method_options_billie + type: object + x-expandableFields: [] + payment_method_options_boleto: + description: '' + properties: + expires_after_days: + description: >- + The number of calendar days before a Boleto voucher expires. For + example, if you create a Boleto voucher on Monday and you set + expires_after_days to 2, the Boleto voucher will expire on Wednesday + at 23:59 America/Sao_Paulo time. + type: integer + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + required: + - expires_after_days + title: payment_method_options_boleto + type: object + x-expandableFields: [] + payment_method_options_card_installments: + description: '' + properties: + available_plans: + description: Installment plans that may be selected for this PaymentIntent. + items: + $ref: '#/components/schemas/payment_method_details_card_installments_plan' + nullable: true + type: array + enabled: + description: Whether Installments are enabled for this PaymentIntent. + type: boolean + plan: + anyOf: + - $ref: >- + #/components/schemas/payment_method_details_card_installments_plan + description: Installment plan selected for this PaymentIntent. + nullable: true + required: + - enabled + title: payment_method_options_card_installments + type: object + x-expandableFields: + - available_plans + - plan + payment_method_options_card_mandate_options: + description: '' + properties: + amount: + description: Amount to be charged for future payments. + type: integer + amount_type: + description: >- + One of `fixed` or `maximum`. If `fixed`, the `amount` param refers + to the exact amount to be charged in future payments. If `maximum`, + the amount charged can be up to the value passed for the `amount` + param. + enum: + - fixed + - maximum + type: string + description: + description: >- + A description of the mandate or subscription that is meant to be + displayed to the customer. + maxLength: 200 + nullable: true + type: string + end_date: + description: >- + End date of the mandate or subscription. If not provided, the + mandate will be active until canceled. If provided, end date should + be after start date. + format: unix-time + nullable: true + type: integer + interval: + description: >- + Specifies payment frequency. One of `day`, `week`, `month`, `year`, + or `sporadic`. + enum: + - day + - month + - sporadic + - week + - year + type: string + interval_count: + description: >- + The number of intervals between payments. For example, + `interval=month` and `interval_count=3` indicates one payment every + three months. Maximum of one year interval allowed (1 year, 12 + months, or 52 weeks). This parameter is optional when + `interval=sporadic`. + nullable: true + type: integer + reference: + description: Unique identifier for the mandate or subscription. + maxLength: 80 + type: string + start_date: + description: >- + Start date of the mandate or subscription. Start date should not be + lesser than yesterday. + format: unix-time + type: integer + supported_types: + description: >- + Specifies the type of mandates supported. Possible values are + `india`. + items: + enum: + - india + type: string + nullable: true + type: array + required: + - amount + - amount_type + - interval + - reference + - start_date + title: payment_method_options_card_mandate_options + type: object + x-expandableFields: [] + payment_method_options_card_present: + description: '' + properties: + request_extended_authorization: + description: >- + Request ability to capture this payment beyond the standard + [authorization validity + window](https://stripe.com/docs/terminal/features/extended-authorizations#authorization-validity) + nullable: true + type: boolean + request_incremental_authorization_support: + description: >- + Request ability to + [increment](https://stripe.com/docs/terminal/features/incremental-authorizations) + this PaymentIntent if the combination of MCC and card brand is + eligible. Check + [incremental_authorization_supported](https://stripe.com/docs/api/charges/object#charge_object-payment_method_details-card_present-incremental_authorization_supported) + in the + [Confirm](https://stripe.com/docs/api/payment_intents/confirm) + response to verify support. + nullable: true + type: boolean + routing: + $ref: '#/components/schemas/payment_method_options_card_present_routing' + title: payment_method_options_card_present + type: object + x-expandableFields: + - routing + payment_method_options_card_present_routing: + description: '' + properties: + requested_priority: + description: Requested routing priority + enum: + - domestic + - international + nullable: true + type: string + title: payment_method_options_card_present_routing + type: object + x-expandableFields: [] + payment_method_options_cashapp: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + - on_session + type: string + title: payment_method_options_cashapp + type: object + x-expandableFields: [] + payment_method_options_customer_balance: + description: '' + properties: + bank_transfer: + $ref: >- + #/components/schemas/payment_method_options_customer_balance_bank_transfer + funding_type: + description: >- + The funding method type to be used when there are not enough funds + in the customer balance. Permitted values include: `bank_transfer`. + enum: + - bank_transfer + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_customer_balance + type: object + x-expandableFields: + - bank_transfer + payment_method_options_customer_balance_bank_transfer: + description: '' + properties: + eu_bank_transfer: + $ref: >- + #/components/schemas/payment_method_options_customer_balance_eu_bank_account + requested_address_types: + description: >- + List of address types that should be returned in the + financial_addresses response. If not specified, all valid types will + be returned. + + + Permitted values include: `sort_code`, `zengin`, `iban`, or `spei`. + items: + enum: + - aba + - iban + - sepa + - sort_code + - spei + - swift + - zengin + type: string + x-stripeBypassValidation: true + type: array + type: + description: >- + The bank transfer type that this PaymentIntent is allowed to use for + funding Permitted values include: `eu_bank_transfer`, + `gb_bank_transfer`, `jp_bank_transfer`, `mx_bank_transfer`, or + `us_bank_transfer`. + enum: + - eu_bank_transfer + - gb_bank_transfer + - jp_bank_transfer + - mx_bank_transfer + - us_bank_transfer + nullable: true + type: string + x-stripeBypassValidation: true + title: payment_method_options_customer_balance_bank_transfer + type: object + x-expandableFields: + - eu_bank_transfer + payment_method_options_customer_balance_eu_bank_account: + description: '' + properties: + country: + description: >- + The desired country code of the bank account information. Permitted + values include: `BE`, `DE`, `ES`, `FR`, `IE`, or `NL`. + enum: + - BE + - DE + - ES + - FR + - IE + - NL + type: string + required: + - country + title: payment_method_options_customer_balance_eu_bank_account + type: object + x-expandableFields: [] + payment_method_options_fpx: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_fpx + type: object + x-expandableFields: [] + payment_method_options_giropay: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_giropay + type: object + x-expandableFields: [] + payment_method_options_grabpay: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_grabpay + type: object + x-expandableFields: [] + payment_method_options_ideal: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: payment_method_options_ideal + type: object + x-expandableFields: [] + payment_method_options_interac_present: + description: '' + properties: {} + title: payment_method_options_interac_present + type: object + x-expandableFields: [] + payment_method_options_klarna: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + preferred_locale: + description: >- + Preferred locale of the Klarna checkout page that the customer is + redirected to. + maxLength: 5000 + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + x-stripeBypassValidation: true + title: payment_method_options_klarna + type: object + x-expandableFields: [] + payment_method_options_konbini: + description: '' + properties: + confirmation_number: + description: >- + An optional 10 to 11 digit numeric-only string determining the + confirmation code at applicable convenience stores. + maxLength: 5000 + nullable: true + type: string + expires_after_days: + description: >- + The number of calendar days (between 1 and 60) after which Konbini + payment instructions will expire. For example, if a PaymentIntent is + confirmed with Konbini and `expires_after_days` set to 2 on Monday + JST, the instructions will expire on Wednesday 23:59:59 JST. + nullable: true + type: integer + expires_at: + description: >- + The timestamp at which the Konbini payment instructions will expire. + Only one of `expires_after_days` or `expires_at` may be set. + format: unix-time + nullable: true + type: integer + product_description: + description: >- + A product descriptor of up to 22 characters, which will appear to + customers at the convenience store. + maxLength: 5000 + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_konbini + type: object + x-expandableFields: [] + payment_method_options_kr_card: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: payment_method_options_kr_card + type: object + x-expandableFields: [] + payment_method_options_multibanco: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_multibanco + type: object + x-expandableFields: [] + payment_method_options_oxxo: + description: '' + properties: + expires_after_days: + description: >- + The number of calendar days before an OXXO invoice expires. For + example, if you create an OXXO invoice on Monday and you set + expires_after_days to 2, the OXXO invoice will expire on Wednesday + at 23:59 America/Mexico_City time. + type: integer + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + required: + - expires_after_days + title: payment_method_options_oxxo + type: object + x-expandableFields: [] + payment_method_options_p24: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_p24 + type: object + x-expandableFields: [] + payment_method_options_pay_by_bank: + description: '' + properties: {} + title: payment_method_options_pay_by_bank + type: object + x-expandableFields: [] + payment_method_options_paynow: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_paynow + type: object + x-expandableFields: [] + payment_method_options_paypal: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + preferred_locale: + description: >- + Preferred locale of the PayPal checkout page that the customer is + redirected to. + maxLength: 5000 + nullable: true + type: string + reference: + description: >- + A reference of the PayPal transaction visible to customer which is + mapped to PayPal's invoice ID. This must be a globally unique ID if + you have configured in your PayPal settings to block multiple + payments per invoice ID. + maxLength: 5000 + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: payment_method_options_paypal + type: object + x-expandableFields: [] + payment_method_options_pix: + description: '' + properties: + expires_after_seconds: + description: >- + The number of seconds (between 10 and 1209600) after which Pix + payment will expire. + nullable: true + type: integer + expires_at: + description: The timestamp at which the Pix expires. + nullable: true + type: integer + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_pix + type: object + x-expandableFields: [] + payment_method_options_promptpay: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_promptpay + type: object + x-expandableFields: [] + payment_method_options_revolut_pay: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: payment_method_options_revolut_pay + type: object + x-expandableFields: [] + payment_method_options_satispay: + description: '' + properties: + capture_method: + description: >- + Controls when the funds will be captured from the customer's + account. + enum: + - manual + type: string + title: payment_method_options_satispay + type: object + x-expandableFields: [] + payment_method_options_sofort: + description: '' + properties: + preferred_language: + description: >- + Preferred language of the SOFORT authorization page that the + customer is redirected to. + enum: + - de + - en + - es + - fr + - it + - nl + - pl + nullable: true + type: string + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + - off_session + type: string + title: payment_method_options_sofort + type: object + x-expandableFields: [] + payment_method_options_twint: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_twint + type: object + x-expandableFields: [] + payment_method_options_us_bank_account_mandate_options: + description: '' + properties: + collection_method: + description: Mandate collection method + enum: + - paper + type: string + x-stripeBypassValidation: true + title: payment_method_options_us_bank_account_mandate_options + type: object + x-expandableFields: [] + payment_method_options_wechat_pay: + description: '' + properties: + app_id: + description: >- + The app ID registered with WeChat Pay. Only required when client is + ios or android. + maxLength: 5000 + nullable: true + type: string + client: + description: The client type that the end customer will pay from + enum: + - android + - ios + - web + nullable: true + type: string + x-stripeBypassValidation: true + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_wechat_pay + type: object + x-expandableFields: [] + payment_method_options_zip: + description: '' + properties: + setup_future_usage: + description: >- + Indicates that you intend to make future payments with this + PaymentIntent's payment method. + + + If you provide a Customer with the PaymentIntent, you can use this + parameter to [attach the payment + method](/payments/save-during-payment) to the Customer after the + PaymentIntent is confirmed and the customer completes any required + actions. If you don't provide a Customer, you can still + [attach](/api/payment_methods/attach) the payment method to a + Customer after the transaction completes. + + + If the payment method is `card_present` and isn't a digital wallet, + Stripe creates and attaches a + [generated_card](/api/charges/object#charge_object-payment_method_details-card_present-generated_card) + payment method representing the card to the Customer instead. + + + When processing card payments, Stripe uses `setup_future_usage` to + help you comply with regional legislation and network rules, such as + [SCA](/strong-customer-authentication). + enum: + - none + type: string + title: payment_method_options_zip + type: object + x-expandableFields: [] + payment_method_oxxo: + description: '' + properties: {} + title: payment_method_oxxo + type: object + x-expandableFields: [] + payment_method_p24: + description: '' + properties: + bank: + description: 'The customer''s bank, if provided.' + enum: + - alior_bank + - bank_millennium + - bank_nowy_bfg_sa + - bank_pekao_sa + - banki_spbdzielcze + - blik + - bnp_paribas + - boz + - citi_handlowy + - credit_agricole + - envelobank + - etransfer_pocztowy24 + - getin_bank + - ideabank + - ing + - inteligo + - mbank_mtransfer + - nest_przelew + - noble_pay + - pbac_z_ipko + - plus_bank + - santander_przelew24 + - tmobile_usbugi_bankowe + - toyota_bank + - velobank + - volkswagen_bank + nullable: true + type: string + x-stripeBypassValidation: true + title: payment_method_p24 + type: object + x-expandableFields: [] + payment_method_pay_by_bank: + description: '' + properties: {} + title: payment_method_pay_by_bank + type: object + x-expandableFields: [] + payment_method_payco: + description: '' + properties: {} + title: payment_method_payco + type: object + x-expandableFields: [] + payment_method_paynow: + description: '' + properties: {} + title: payment_method_paynow + type: object + x-expandableFields: [] + payment_method_paypal: + description: '' + properties: + country: + description: >- + Two-letter ISO code representing the buyer's country. Values are + provided by PayPal directly (if supported) at the time of + authorization or settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + payer_email: + description: >- + Owner's email. Values are provided by PayPal directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + payer_id: + description: >- + PayPal account PayerID. This identifier uniquely identifies the + PayPal customer. + maxLength: 5000 + nullable: true + type: string + title: payment_method_paypal + type: object + x-expandableFields: [] + payment_method_pix: + description: '' + properties: {} + title: payment_method_pix + type: object + x-expandableFields: [] + payment_method_promptpay: + description: '' + properties: {} + title: payment_method_promptpay + type: object + x-expandableFields: [] + payment_method_revolut_pay: + description: '' + properties: {} + title: payment_method_revolut_pay + type: object + x-expandableFields: [] + payment_method_samsung_pay: + description: '' + properties: {} + title: payment_method_samsung_pay + type: object + x-expandableFields: [] + payment_method_satispay: + description: '' + properties: {} + title: payment_method_satispay + type: object + x-expandableFields: [] + payment_method_sepa_debit: + description: '' + properties: + bank_code: + description: Bank code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + branch_code: + description: Branch code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country the bank account is + located in. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + generated_from: + anyOf: + - $ref: '#/components/schemas/sepa_debit_generated_from' + description: Information about the object that generated this PaymentMethod. + nullable: true + last4: + description: Last four characters of the IBAN. + maxLength: 5000 + nullable: true + type: string + title: payment_method_sepa_debit + type: object + x-expandableFields: + - generated_from + payment_method_sofort: + description: '' + properties: + country: + description: >- + Two-letter ISO code representing the country the bank account is + located in. + maxLength: 5000 + nullable: true + type: string + title: payment_method_sofort + type: object + x-expandableFields: [] + payment_method_swish: + description: '' + properties: {} + title: payment_method_swish + type: object + x-expandableFields: [] + payment_method_twint: + description: '' + properties: {} + title: payment_method_twint + type: object + x-expandableFields: [] + payment_method_us_bank_account: + description: '' + properties: + account_holder_type: + description: 'Account holder type: individual or company.' + enum: + - company + - individual + nullable: true + type: string + account_type: + description: 'Account type: checkings or savings. Defaults to checking if omitted.' + enum: + - checking + - savings + nullable: true + type: string + bank_name: + description: The name of the bank. + maxLength: 5000 + nullable: true + type: string + financial_connections_account: + description: >- + The ID of the Financial Connections Account used to create the + payment method. + maxLength: 5000 + nullable: true + type: string + fingerprint: + description: >- + Uniquely identifies this particular bank account. You can use this + attribute to check whether two bank accounts are the same. + maxLength: 5000 + nullable: true + type: string + last4: + description: Last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + networks: + anyOf: + - $ref: '#/components/schemas/us_bank_account_networks' + description: >- + Contains information about US bank account networks that can be + used. + nullable: true + routing_number: + description: Routing number of the bank account. + maxLength: 5000 + nullable: true + type: string + status_details: + anyOf: + - $ref: >- + #/components/schemas/payment_method_us_bank_account_status_details + description: >- + Contains information about the future reusability of this + PaymentMethod. + nullable: true + title: payment_method_us_bank_account + type: object + x-expandableFields: + - networks + - status_details + payment_method_us_bank_account_blocked: + description: '' + properties: + network_code: + description: The ACH network code that resulted in this block. + enum: + - R02 + - R03 + - R04 + - R05 + - R07 + - R08 + - R10 + - R11 + - R16 + - R20 + - R29 + - R31 + nullable: true + type: string + reason: + description: The reason why this PaymentMethod's fingerprint has been blocked + enum: + - bank_account_closed + - bank_account_frozen + - bank_account_invalid_details + - bank_account_restricted + - bank_account_unusable + - debit_not_authorized + nullable: true + type: string + title: payment_method_us_bank_account_blocked + type: object + x-expandableFields: [] + payment_method_us_bank_account_status_details: + description: '' + properties: + blocked: + $ref: '#/components/schemas/payment_method_us_bank_account_blocked' + title: payment_method_us_bank_account_status_details + type: object + x-expandableFields: + - blocked + payment_method_wechat_pay: + description: '' + properties: {} + title: payment_method_wechat_pay + type: object + x-expandableFields: [] + payment_method_zip: + description: '' + properties: {} + title: payment_method_zip + type: object + x-expandableFields: [] + payment_pages_checkout_session_adaptive_pricing: + description: '' + properties: + enabled: + description: Whether Adaptive Pricing is enabled. + type: boolean + required: + - enabled + title: PaymentPagesCheckoutSessionAdaptivePricing + type: object + x-expandableFields: [] + payment_pages_checkout_session_after_expiration: + description: '' + properties: + recovery: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_after_expiration_recovery + description: >- + When set, configuration used to recover the Checkout Session on + expiry. + nullable: true + title: PaymentPagesCheckoutSessionAfterExpiration + type: object + x-expandableFields: + - recovery + payment_pages_checkout_session_after_expiration_recovery: + description: '' + properties: + allow_promotion_codes: + description: >- + Enables user redeemable promotion codes on the recovered Checkout + Sessions. Defaults to `false` + type: boolean + enabled: + description: >- + If `true`, a recovery url will be generated to recover this Checkout + Session if it + + expires before a transaction is completed. It will be attached to + the + + Checkout Session object upon expiration. + type: boolean + expires_at: + description: The timestamp at which the recovery URL will expire. + format: unix-time + nullable: true + type: integer + url: + description: >- + URL that creates a new Checkout Session when clicked that is a copy + of this expired Checkout Session + maxLength: 5000 + nullable: true + type: string + required: + - allow_promotion_codes + - enabled + title: PaymentPagesCheckoutSessionAfterExpirationRecovery + type: object + x-expandableFields: [] + payment_pages_checkout_session_automatic_tax: + description: '' + properties: + enabled: + description: Indicates whether automatic tax is enabled for the session + type: boolean + liability: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The account that's liable for tax. If set, the business address and + tax registrations required to perform the tax calculation are loaded + from this account. The tax transaction is returned in the report of + the connected account. + nullable: true + provider: + description: The tax provider powering automatic tax. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + The status of the most recent automated tax calculation for this + session. + enum: + - complete + - failed + - requires_location_inputs + nullable: true + type: string + required: + - enabled + title: PaymentPagesCheckoutSessionAutomaticTax + type: object + x-expandableFields: + - liability + payment_pages_checkout_session_checkout_address_details: + description: '' + properties: + address: + $ref: '#/components/schemas/address' + name: + description: Customer name. + maxLength: 5000 + type: string + required: + - address + - name + title: PaymentPagesCheckoutSessionCheckoutAddressDetails + type: object + x-expandableFields: + - address + payment_pages_checkout_session_collected_information: + description: '' + properties: + shipping_details: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_checkout_address_details + description: Shipping information for this Checkout Session. + nullable: true + title: PaymentPagesCheckoutSessionCollectedInformation + type: object + x-expandableFields: + - shipping_details + payment_pages_checkout_session_consent: + description: '' + properties: + promotions: + description: >- + If `opt_in`, the customer consents to receiving promotional + communications + + from the merchant about this Checkout Session. + enum: + - opt_in + - opt_out + nullable: true + type: string + terms_of_service: + description: >- + If `accepted`, the customer in this Checkout Session has agreed to + the merchant's terms of service. + enum: + - accepted + nullable: true + type: string + x-stripeBypassValidation: true + title: PaymentPagesCheckoutSessionConsent + type: object + x-expandableFields: [] + payment_pages_checkout_session_consent_collection: + description: '' + properties: + payment_method_reuse_agreement: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_payment_method_reuse_agreement + description: >- + If set to `hidden`, it will hide legal text related to the reuse of + a payment method. + nullable: true + promotions: + description: >- + If set to `auto`, enables the collection of customer consent for + promotional communications. The Checkout + + Session will determine whether to display an option to opt into + promotional communication + + from the merchant depending on the customer's locale. Only available + to US merchants. + enum: + - auto + - none + nullable: true + type: string + terms_of_service: + description: >- + If set to `required`, it requires customers to accept the terms of + service before being able to pay. + enum: + - none + - required + nullable: true + type: string + title: PaymentPagesCheckoutSessionConsentCollection + type: object + x-expandableFields: + - payment_method_reuse_agreement + payment_pages_checkout_session_currency_conversion: + description: '' + properties: + amount_subtotal: + description: >- + Total of all items in source currency before discounts or taxes are + applied. + type: integer + amount_total: + description: >- + Total of all items in source currency after discounts and taxes are + applied. + type: integer + fx_rate: + description: >- + Exchange rate used to convert source currency amounts to customer + currency amounts + format: decimal + type: string + source_currency: + description: Creation currency of the CheckoutSession before localization + maxLength: 5000 + type: string + required: + - amount_subtotal + - amount_total + - fx_rate + - source_currency + title: PaymentPagesCheckoutSessionCurrencyConversion + type: object + x-expandableFields: [] + payment_pages_checkout_session_custom_fields: + description: '' + properties: + dropdown: + $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_fields_dropdown + key: + description: >- + String of your choice that your integration can use to reconcile + this field. Must be unique to this field, alphanumeric, and up to + 200 characters. + maxLength: 5000 + type: string + label: + $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_fields_label + numeric: + $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_fields_numeric + optional: + description: >- + Whether the customer is required to complete the field before + completing the Checkout Session. Defaults to `false`. + type: boolean + text: + $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_fields_text + type: + description: The type of the field. + enum: + - dropdown + - numeric + - text + type: string + required: + - key + - label + - optional + - type + title: PaymentPagesCheckoutSessionCustomFields + type: object + x-expandableFields: + - dropdown + - label + - numeric + - text + payment_pages_checkout_session_custom_fields_dropdown: + description: '' + properties: + default_value: + description: The value that will pre-fill on the payment page. + maxLength: 5000 + nullable: true + type: string + options: + description: >- + The options available for the customer to select. Up to 200 options + allowed. + items: + $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_fields_option + type: array + value: + description: >- + The option selected by the customer. This will be the `value` for + the option. + maxLength: 5000 + nullable: true + type: string + required: + - options + title: PaymentPagesCheckoutSessionCustomFieldsDropdown + type: object + x-expandableFields: + - options + payment_pages_checkout_session_custom_fields_label: + description: '' + properties: + custom: + description: >- + Custom text for the label, displayed to the customer. Up to 50 + characters. + maxLength: 5000 + nullable: true + type: string + type: + description: The type of the label. + enum: + - custom + type: string + required: + - type + title: PaymentPagesCheckoutSessionCustomFieldsLabel + type: object + x-expandableFields: [] + payment_pages_checkout_session_custom_fields_numeric: + description: '' + properties: + default_value: + description: The value that will pre-fill the field on the payment page. + maxLength: 5000 + nullable: true + type: string + maximum_length: + description: The maximum character length constraint for the customer's input. + nullable: true + type: integer + minimum_length: + description: The minimum character length requirement for the customer's input. + nullable: true + type: integer + value: + description: 'The value entered by the customer, containing only digits.' + maxLength: 5000 + nullable: true + type: string + title: PaymentPagesCheckoutSessionCustomFieldsNumeric + type: object + x-expandableFields: [] + payment_pages_checkout_session_custom_fields_option: + description: '' + properties: + label: + description: >- + The label for the option, displayed to the customer. Up to 100 + characters. + maxLength: 5000 + type: string + value: + description: >- + The value for this option, not displayed to the customer, used by + your integration to reconcile the option selected by the customer. + Must be unique to this option, alphanumeric, and up to 100 + characters. + maxLength: 5000 + type: string + required: + - label + - value + title: PaymentPagesCheckoutSessionCustomFieldsOption + type: object + x-expandableFields: [] + payment_pages_checkout_session_custom_fields_text: + description: '' + properties: + default_value: + description: The value that will pre-fill the field on the payment page. + maxLength: 5000 + nullable: true + type: string + maximum_length: + description: The maximum character length constraint for the customer's input. + nullable: true + type: integer + minimum_length: + description: The minimum character length requirement for the customer's input. + nullable: true + type: integer + value: + description: The value entered by the customer. + maxLength: 5000 + nullable: true + type: string + title: PaymentPagesCheckoutSessionCustomFieldsText + type: object + x-expandableFields: [] + payment_pages_checkout_session_custom_text: + description: '' + properties: + after_submit: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_text_position + description: >- + Custom text that should be displayed after the payment confirmation + button. + nullable: true + shipping_address: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_text_position + description: >- + Custom text that should be displayed alongside shipping address + collection. + nullable: true + submit: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_text_position + description: >- + Custom text that should be displayed alongside the payment + confirmation button. + nullable: true + terms_of_service_acceptance: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_custom_text_position + description: >- + Custom text that should be displayed in place of the default terms + of service agreement text. + nullable: true + title: PaymentPagesCheckoutSessionCustomText + type: object + x-expandableFields: + - after_submit + - shipping_address + - submit + - terms_of_service_acceptance + payment_pages_checkout_session_custom_text_position: + description: '' + properties: + message: + description: Text may be up to 1200 characters in length. + maxLength: 500 + type: string + required: + - message + title: PaymentPagesCheckoutSessionCustomTextPosition + type: object + x-expandableFields: [] + payment_pages_checkout_session_customer_details: + description: '' + properties: + address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + The customer's address after a completed Checkout Session. Note: + This property is populated only for sessions on or after March 30, + 2022. + nullable: true + email: + description: >- + The email associated with the Customer, if one exists, on the + Checkout Session after a completed Checkout Session or at time of + session expiry. + + Otherwise, if the customer has consented to promotional content, + this value is the most recent valid email provided by the customer + on the Checkout form. + maxLength: 5000 + nullable: true + type: string + name: + description: >- + The customer's name after a completed Checkout Session. Note: This + property is populated only for sessions on or after March 30, 2022. + maxLength: 5000 + nullable: true + type: string + phone: + description: The customer's phone number after a completed Checkout Session. + maxLength: 5000 + nullable: true + type: string + tax_exempt: + description: The customer’s tax exempt status after a completed Checkout Session. + enum: + - exempt + - none + - reverse + nullable: true + type: string + tax_ids: + description: The customer’s tax IDs after a completed Checkout Session. + items: + $ref: '#/components/schemas/payment_pages_checkout_session_tax_id' + nullable: true + type: array + title: PaymentPagesCheckoutSessionCustomerDetails + type: object + x-expandableFields: + - address + - tax_ids + payment_pages_checkout_session_discount: + description: '' + properties: + coupon: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/coupon' + description: Coupon attached to the Checkout Session. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/coupon' + promotion_code: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/promotion_code' + description: Promotion code attached to the Checkout Session. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/promotion_code' + title: PaymentPagesCheckoutSessionDiscount + type: object + x-expandableFields: + - coupon + - promotion_code + payment_pages_checkout_session_invoice_creation: + description: '' + properties: + enabled: + description: >- + Indicates whether invoice creation is enabled for the Checkout + Session. + type: boolean + invoice_data: + $ref: '#/components/schemas/payment_pages_checkout_session_invoice_settings' + required: + - enabled + - invoice_data + title: PaymentPagesCheckoutSessionInvoiceCreation + type: object + x-expandableFields: + - invoice_data + payment_pages_checkout_session_invoice_settings: + description: '' + properties: + account_tax_ids: + description: The account tax IDs associated with the invoice. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + nullable: true + type: array + custom_fields: + description: Custom fields displayed on the invoice. + items: + $ref: '#/components/schemas/invoice_setting_custom_field' + nullable: true + type: array + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + footer: + description: Footer displayed on the invoice. + maxLength: 5000 + nullable: true + type: string + issuer: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The connected account that issues the invoice. The invoice is + presented with the branding and support information of the specified + account. + nullable: true + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + rendering_options: + anyOf: + - $ref: '#/components/schemas/invoice_setting_checkout_rendering_options' + description: Options for invoice PDF rendering. + nullable: true + title: PaymentPagesCheckoutSessionInvoiceSettings + type: object + x-expandableFields: + - account_tax_ids + - custom_fields + - issuer + - rendering_options + payment_pages_checkout_session_optional_item: + description: '' + properties: + adjustable_quantity: + anyOf: + - $ref: >- + #/components/schemas/payment_pages_checkout_session_optional_item_adjustable_quantity + nullable: true + price: + maxLength: 5000 + type: string + quantity: + type: integer + required: + - price + - quantity + title: PaymentPagesCheckoutSessionOptionalItem + type: object + x-expandableFields: + - adjustable_quantity + payment_pages_checkout_session_optional_item_adjustable_quantity: + description: '' + properties: + enabled: + description: >- + Set to true if the quantity can be adjusted to any non-negative + integer. + type: boolean + maximum: + description: >- + The maximum quantity of this item the customer can purchase. By + default this value is 99. You can specify a value up to 999999. + nullable: true + type: integer + minimum: + description: >- + The minimum quantity of this item the customer must purchase, if + they choose to purchase it. Because this item is optional, the + customer will always be able to remove it from their order, even if + the `minimum` configured here is greater than 0. By default this + value is 0. + nullable: true + type: integer + required: + - enabled + title: PaymentPagesCheckoutSessionOptionalItemAdjustableQuantity + type: object + x-expandableFields: [] + payment_pages_checkout_session_payment_method_reuse_agreement: + description: '' + properties: + position: + description: >- + Determines the position and visibility of the payment method reuse + agreement in the UI. When set to `auto`, Stripe's defaults will be + used. + + + When set to `hidden`, the payment method reuse agreement text will + always be hidden in the UI. + enum: + - auto + - hidden + type: string + required: + - position + title: PaymentPagesCheckoutSessionPaymentMethodReuseAgreement + type: object + x-expandableFields: [] + payment_pages_checkout_session_permissions: + description: '' + properties: + update_shipping_details: + description: >- + Determines which entity is allowed to update the shipping details. + + + Default is `client_only`. Stripe Checkout client will automatically + update the shipping details. If set to `server_only`, only your + server is allowed to update the shipping details. + + + When set to `server_only`, you must add the onShippingDetailsChange + event handler when initializing the Stripe Checkout client and + manually update the shipping details from your server using the + Stripe API. + enum: + - client_only + - server_only + nullable: true + type: string + title: PaymentPagesCheckoutSessionPermissions + type: object + x-expandableFields: [] + payment_pages_checkout_session_phone_number_collection: + description: '' + properties: + enabled: + description: Indicates whether phone number collection is enabled for the session + type: boolean + required: + - enabled + title: PaymentPagesCheckoutSessionPhoneNumberCollection + type: object + x-expandableFields: [] + payment_pages_checkout_session_saved_payment_method_options: + description: '' + properties: + allow_redisplay_filters: + description: >- + Uses the `allow_redisplay` value of each saved payment method to + filter the set presented to a returning customer. By default, only + saved payment methods with ’allow_redisplay: ‘always’ are shown in + Checkout. + items: + enum: + - always + - limited + - unspecified + type: string + nullable: true + type: array + payment_method_remove: + description: >- + Enable customers to choose if they wish to remove their saved + payment methods. Disabled by default. + enum: + - disabled + - enabled + nullable: true + type: string + payment_method_save: + description: >- + Enable customers to choose if they wish to save their payment method + for future use. Disabled by default. + enum: + - disabled + - enabled + nullable: true + type: string + title: PaymentPagesCheckoutSessionSavedPaymentMethodOptions + type: object + x-expandableFields: [] + payment_pages_checkout_session_shipping_address_collection: + description: '' + properties: + allowed_countries: + description: >- + An array of two-letter ISO country codes representing which + countries Checkout should provide as options for + + shipping locations. Unsupported country codes: `AS, CX, CC, CU, HM, + IR, KP, MH, FM, NF, MP, PW, SY, UM, VI`. + items: + enum: + - AC + - AD + - AE + - AF + - AG + - AI + - AL + - AM + - AO + - AQ + - AR + - AT + - AU + - AW + - AX + - AZ + - BA + - BB + - BD + - BE + - BF + - BG + - BH + - BI + - BJ + - BL + - BM + - BN + - BO + - BQ + - BR + - BS + - BT + - BV + - BW + - BY + - BZ + - CA + - CD + - CF + - CG + - CH + - CI + - CK + - CL + - CM + - CN + - CO + - CR + - CV + - CW + - CY + - CZ + - DE + - DJ + - DK + - DM + - DO + - DZ + - EC + - EE + - EG + - EH + - ER + - ES + - ET + - FI + - FJ + - FK + - FO + - FR + - GA + - GB + - GD + - GE + - GF + - GG + - GH + - GI + - GL + - GM + - GN + - GP + - GQ + - GR + - GS + - GT + - GU + - GW + - GY + - HK + - HN + - HR + - HT + - HU + - ID + - IE + - IL + - IM + - IN + - IO + - IQ + - IS + - IT + - JE + - JM + - JO + - JP + - KE + - KG + - KH + - KI + - KM + - KN + - KR + - KW + - KY + - KZ + - LA + - LB + - LC + - LI + - LK + - LR + - LS + - LT + - LU + - LV + - LY + - MA + - MC + - MD + - ME + - MF + - MG + - MK + - ML + - MM + - MN + - MO + - MQ + - MR + - MS + - MT + - MU + - MV + - MW + - MX + - MY + - MZ + - NA + - NC + - NE + - NG + - NI + - NL + - 'NO' + - NP + - NR + - NU + - NZ + - OM + - PA + - PE + - PF + - PG + - PH + - PK + - PL + - PM + - PN + - PR + - PS + - PT + - PY + - QA + - RE + - RO + - RS + - RU + - RW + - SA + - SB + - SC + - SD + - SE + - SG + - SH + - SI + - SJ + - SK + - SL + - SM + - SN + - SO + - SR + - SS + - ST + - SV + - SX + - SZ + - TA + - TC + - TD + - TF + - TG + - TH + - TJ + - TK + - TL + - TM + - TN + - TO + - TR + - TT + - TV + - TW + - TZ + - UA + - UG + - US + - UY + - UZ + - VA + - VC + - VE + - VG + - VN + - VU + - WF + - WS + - XK + - YE + - YT + - ZA + - ZM + - ZW + - ZZ + type: string + type: array + required: + - allowed_countries + title: PaymentPagesCheckoutSessionShippingAddressCollection + type: object + x-expandableFields: [] + payment_pages_checkout_session_shipping_cost: + description: '' + properties: + amount_subtotal: + description: Total shipping cost before any discounts or taxes are applied. + type: integer + amount_tax: + description: >- + Total tax amount applied due to shipping costs. If no tax was + applied, defaults to 0. + type: integer + amount_total: + description: Total shipping cost after discounts and taxes are applied. + type: integer + shipping_rate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/shipping_rate' + description: The ID of the ShippingRate for this order. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/shipping_rate' + taxes: + description: The taxes applied to the shipping rate. + items: + $ref: '#/components/schemas/line_items_tax_amount' + type: array + required: + - amount_subtotal + - amount_tax + - amount_total + title: PaymentPagesCheckoutSessionShippingCost + type: object + x-expandableFields: + - shipping_rate + - taxes + payment_pages_checkout_session_shipping_option: + description: '' + properties: + shipping_amount: + description: A non-negative integer in cents representing how much to charge. + type: integer + shipping_rate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/shipping_rate' + description: The shipping rate. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/shipping_rate' + required: + - shipping_amount + - shipping_rate + title: PaymentPagesCheckoutSessionShippingOption + type: object + x-expandableFields: + - shipping_rate + payment_pages_checkout_session_tax_id: + description: '' + properties: + type: + description: >- + The type of the tax ID, one of `ad_nrt`, `ar_cuit`, `eu_vat`, + `bo_tin`, `br_cnpj`, `br_cpf`, `cn_tin`, `co_nit`, `cr_tin`, + `do_rcn`, `ec_ruc`, `eu_oss_vat`, `hr_oib`, `pe_ruc`, `ro_tin`, + `rs_pib`, `sv_nit`, `uy_ruc`, `ve_rif`, `vn_tin`, `gb_vat`, + `nz_gst`, `au_abn`, `au_arn`, `in_gst`, `no_vat`, `no_voec`, + `za_vat`, `ch_vat`, `mx_rfc`, `sg_uen`, `ru_inn`, `ru_kpp`, `ca_bn`, + `hk_br`, `es_cif`, `tw_vat`, `th_vat`, `jp_cn`, `jp_rn`, `jp_trn`, + `li_uid`, `li_vat`, `my_itn`, `us_ein`, `kr_brn`, `ca_qst`, + `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `my_sst`, + `sg_gst`, `ae_trn`, `cl_tin`, `sa_vat`, `id_npwp`, `my_frp`, + `il_vat`, `ge_vat`, `ua_vat`, `is_vat`, `bg_uic`, `hu_tin`, + `si_tin`, `ke_pin`, `tr_tin`, `eg_tin`, `ph_tin`, `al_tin`, + `bh_vat`, `kz_bin`, `ng_tin`, `om_vat`, `de_stn`, `ch_uid`, + `tz_vat`, `uz_vat`, `uz_tin`, `md_vat`, `ma_vat`, `by_tin`, + `ao_tin`, `bs_tin`, `bb_tin`, `cd_nif`, `mr_nif`, `me_pib`, + `zw_tin`, `ba_tin`, `gn_nif`, `mk_vat`, `sr_fin`, `sn_ninea`, + `am_tin`, `np_pan`, `tj_tin`, `ug_tin`, `zm_tin`, `kh_tin`, + `aw_tin`, `az_tin`, `bd_bin`, `bj_ifu`, `et_tin`, `kg_tin`, + `la_tin`, `cm_niu`, `cv_nif`, `bf_ifu`, or `unknown` + enum: + - ad_nrt + - ae_trn + - al_tin + - am_tin + - ao_tin + - ar_cuit + - au_abn + - au_arn + - aw_tin + - az_tin + - ba_tin + - bb_tin + - bd_bin + - bf_ifu + - bg_uic + - bh_vat + - bj_ifu + - bo_tin + - br_cnpj + - br_cpf + - bs_tin + - by_tin + - ca_bn + - ca_gst_hst + - ca_pst_bc + - ca_pst_mb + - ca_pst_sk + - ca_qst + - cd_nif + - ch_uid + - ch_vat + - cl_tin + - cm_niu + - cn_tin + - co_nit + - cr_tin + - cv_nif + - de_stn + - do_rcn + - ec_ruc + - eg_tin + - es_cif + - et_tin + - eu_oss_vat + - eu_vat + - gb_vat + - ge_vat + - gn_nif + - hk_br + - hr_oib + - hu_tin + - id_npwp + - il_vat + - in_gst + - is_vat + - jp_cn + - jp_rn + - jp_trn + - ke_pin + - kg_tin + - kh_tin + - kr_brn + - kz_bin + - la_tin + - li_uid + - li_vat + - ma_vat + - md_vat + - me_pib + - mk_vat + - mr_nif + - mx_rfc + - my_frp + - my_itn + - my_sst + - ng_tin + - no_vat + - no_voec + - np_pan + - nz_gst + - om_vat + - pe_ruc + - ph_tin + - ro_tin + - rs_pib + - ru_inn + - ru_kpp + - sa_vat + - sg_gst + - sg_uen + - si_tin + - sn_ninea + - sr_fin + - sv_nit + - th_vat + - tj_tin + - tr_tin + - tw_vat + - tz_vat + - ua_vat + - ug_tin + - unknown + - us_ein + - uy_ruc + - uz_tin + - uz_vat + - ve_rif + - vn_tin + - za_vat + - zm_tin + - zw_tin + type: string + value: + description: The value of the tax ID. + maxLength: 5000 + nullable: true + type: string + required: + - type + title: PaymentPagesCheckoutSessionTaxID + type: object + x-expandableFields: [] + payment_pages_checkout_session_tax_id_collection: + description: '' + properties: + enabled: + description: Indicates whether tax ID collection is enabled for the session + type: boolean + required: + description: Indicates whether a tax ID is required on the payment page + enum: + - if_supported + - never + type: string + required: + - enabled + - required + title: PaymentPagesCheckoutSessionTaxIDCollection + type: object + x-expandableFields: [] + payment_pages_checkout_session_total_details: + description: '' + properties: + amount_discount: + description: This is the sum of all the discounts. + type: integer + amount_shipping: + description: This is the sum of all the shipping amounts. + nullable: true + type: integer + amount_tax: + description: This is the sum of all the tax amounts. + type: integer + breakdown: + $ref: >- + #/components/schemas/payment_pages_checkout_session_total_details_resource_breakdown + required: + - amount_discount + - amount_tax + title: PaymentPagesCheckoutSessionTotalDetails + type: object + x-expandableFields: + - breakdown + payment_pages_checkout_session_total_details_resource_breakdown: + description: '' + properties: + discounts: + description: The aggregated discounts. + items: + $ref: '#/components/schemas/line_items_discount_amount' + type: array + taxes: + description: The aggregated tax amounts by rate. + items: + $ref: '#/components/schemas/line_items_tax_amount' + type: array + required: + - discounts + - taxes + title: PaymentPagesCheckoutSessionTotalDetailsResourceBreakdown + type: object + x-expandableFields: + - discounts + - taxes + payment_pages_private_card_payment_method_options_resource_restrictions: + description: '' + properties: + brands_blocked: + description: >- + Specify the card brands to block in the Checkout Session. If a + customer enters or selects a card belonging to a blocked brand, they + can't complete the Session. + items: + enum: + - american_express + - discover_global_network + - mastercard + - visa + type: string + type: array + title: PaymentPagesPrivateCardPaymentMethodOptionsResourceRestrictions + type: object + x-expandableFields: [] + payment_source: + anyOf: + - $ref: '#/components/schemas/account' + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + title: Polymorphic + x-resourceId: payment_source + x-stripeBypassValidation: true + payout: + description: >- + A `Payout` object is created when you receive funds from Stripe, or when + you + + initiate a payout to either a bank account or debit card of a [connected + + Stripe account](/docs/connect/bank-debit-card-payouts). You can retrieve + individual payouts, + + and list all payouts. Payouts are made on [varying + + schedules](/docs/connect/manage-payout-schedule), depending on your + country and + + industry. + + + Related guide: [Receiving payouts](https://stripe.com/docs/payouts) + properties: + amount: + description: >- + The amount (in cents (or local equivalent)) that transfers to your + bank account or debit card. + type: integer + application_fee: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application_fee' + description: >- + The application fee (if any) for the payout. [See the Connect + documentation](https://stripe.com/docs/connect/instant-payouts#monetization-and-fees) + for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application_fee' + application_fee_amount: + description: >- + The amount of the application fee (if any) requested for the payout. + [See the Connect + documentation](https://stripe.com/docs/connect/instant-payouts#monetization-and-fees) + for details. + nullable: true + type: integer + arrival_date: + description: >- + Date that you can expect the payout to arrive in the bank. This + factors in delays to account for weekends or bank holidays. + format: unix-time + type: integer + automatic: + description: >- + Returns `true` if the payout is created by an [automated payout + schedule](https://stripe.com/docs/payouts#payout-schedule) and + `false` if it's [requested + manually](https://stripe.com/docs/payouts#manual-payouts). + type: boolean + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + ID of the balance transaction that describes the impact of this + payout on your account balance. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + destination: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/deleted_bank_account' + - $ref: '#/components/schemas/deleted_card' + description: ID of the bank account or card the payout is sent to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/deleted_bank_account' + - $ref: '#/components/schemas/deleted_card' + x-stripeBypassValidation: true + failure_balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + If the payout fails or cancels, this is the ID of the balance + transaction that reverses the initial balance transaction and + returns the funds from the failed payout back in your balance. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + failure_code: + description: >- + Error code that provides a reason for a payout failure, if + available. View our [list of failure + codes](https://stripe.com/docs/api#payout_failures). + maxLength: 5000 + nullable: true + type: string + failure_message: + description: 'Message that provides the reason for a payout failure, if available.' + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + method: + description: >- + The method used to send this payout, which can be `standard` or + `instant`. `instant` is supported for payouts to debit cards and + bank accounts in certain countries. Learn more about [bank support + for Instant + Payouts](https://stripe.com/docs/payouts/instant-payouts-banks). + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - payout + type: string + original_payout: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payout' + description: >- + If the payout reverses another, this is the ID of the original + payout. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payout' + reconciliation_status: + description: >- + If `completed`, you can use the [Balance Transactions + API](https://stripe.com/docs/api/balance_transactions/list#balance_transaction_list-payout) + to list all balance transactions that are paid out in this payout. + enum: + - completed + - in_progress + - not_applicable + type: string + reversed_by: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payout' + description: >- + If the payout reverses, this is the ID of the payout that reverses + this payout. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payout' + source_type: + description: >- + The source balance this payout came from, which can be one of the + following: `card`, `fpx`, or `bank_account`. + maxLength: 5000 + type: string + statement_descriptor: + description: >- + Extra information about a payout that displays on the user's bank + statement. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + Current status of the payout: `paid`, `pending`, `in_transit`, + `canceled` or `failed`. A payout is `pending` until it's submitted + to the bank, when it becomes `in_transit`. The status changes to + `paid` if the transaction succeeds, or to `failed` or `canceled` + (within 5 business days). Some payouts that fail might initially + show as `paid`, then change to `failed`. + maxLength: 5000 + type: string + trace_id: + anyOf: + - $ref: '#/components/schemas/payouts_trace_id' + description: >- + A value that generates from the beneficiary's bank that allows users + to track payouts with their bank. Banks might call this a "reference + number" or something similar. + nullable: true + type: + description: Can be `bank_account` or `card`. + enum: + - bank_account + - card + type: string + x-stripeBypassValidation: true + required: + - amount + - arrival_date + - automatic + - created + - currency + - id + - livemode + - method + - object + - reconciliation_status + - source_type + - status + - type + title: Payout + type: object + x-expandableFields: + - application_fee + - balance_transaction + - destination + - failure_balance_transaction + - original_payout + - reversed_by + - trace_id + x-resourceId: payout + payouts_trace_id: + description: '' + properties: + status: + description: >- + Possible values are `pending`, `supported`, and `unsupported`. When + `payout.status` is `pending` or `in_transit`, this will be + `pending`. When the payout transitions to `paid`, `failed`, or + `canceled`, this status will become `supported` or `unsupported` + shortly after in most cases. In some cases, this may appear as + `pending` for up to 10 days after `arrival_date` until transitioning + to `supported` or `unsupported`. + maxLength: 5000 + type: string + value: + description: >- + The trace ID value if `trace_id.status` is `supported`, otherwise + `nil`. + maxLength: 5000 + nullable: true + type: string + required: + - status + title: PayoutsTraceID + type: object + x-expandableFields: [] + paypal_seller_protection: + description: '' + properties: + dispute_categories: + description: >- + An array of conditions that are covered for the transaction, if + applicable. + items: + enum: + - fraudulent + - product_not_received + type: string + x-stripeBypassValidation: true + nullable: true + type: array + status: + description: >- + Indicates whether the transaction is eligible for PayPal's seller + protection. + enum: + - eligible + - not_eligible + - partially_eligible + type: string + required: + - status + title: paypal_seller_protection + type: object + x-expandableFields: [] + person: + description: >- + This is an object representing a person associated with a Stripe + account. + + + A platform can only access a subset of data in a person for an account + where + [account.controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`, which includes Standard and Express accounts, after + creating an Account Link or Account Session to start Connect onboarding. + + + See the [Standard onboarding](/connect/standard-accounts) or [Express + onboarding](/connect/express-accounts) documentation for information + about prefilling information and account onboarding steps. Learn more + about [handling identity verification with the + API](/connect/handling-api-verification#person-information). + properties: + account: + description: The account the person is associated with. + maxLength: 5000 + type: string + additional_tos_acceptances: + $ref: '#/components/schemas/person_additional_tos_acceptances' + address: + $ref: '#/components/schemas/address' + address_kana: + anyOf: + - $ref: '#/components/schemas/legal_entity_japan_address' + nullable: true + address_kanji: + anyOf: + - $ref: '#/components/schemas/legal_entity_japan_address' + nullable: true + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + dob: + $ref: '#/components/schemas/legal_entity_dob' + email: + description: >- + The person's email address. Also available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + first_name: + description: >- + The person's first name. Also available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + first_name_kana: + description: >- + The Kana variation of the person's first name (Japan only). Also + available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + first_name_kanji: + description: >- + The Kanji variation of the person's first name (Japan only). Also + available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + full_name_aliases: + description: >- + A list of alternate names or aliases that the person is known by. + Also available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + items: + maxLength: 5000 + type: string + type: array + future_requirements: + anyOf: + - $ref: '#/components/schemas/person_future_requirements' + nullable: true + gender: + description: The person's gender. + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + id_number_provided: + description: >- + Whether the person's `id_number` was provided. True if either the + full ID number was provided or if only the required part of the ID + number was provided (ex. last four of an individual's SSN for the US + indicated by `ssn_last_4_provided`). + type: boolean + id_number_secondary_provided: + description: Whether the person's `id_number_secondary` was provided. + type: boolean + last_name: + description: >- + The person's last name. Also available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + last_name_kana: + description: >- + The Kana variation of the person's last name (Japan only). Also + available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + last_name_kanji: + description: >- + The Kanji variation of the person's last name (Japan only). Also + available for accounts where + [controller.requirement_collection](/api/accounts/object#account_object-controller-requirement_collection) + is `stripe`. + maxLength: 5000 + nullable: true + type: string + maiden_name: + description: The person's maiden name. + maxLength: 5000 + nullable: true + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + nationality: + description: The country where the person is a national. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - person + type: string + phone: + description: The person's phone number. + maxLength: 5000 + nullable: true + type: string + political_exposure: + description: >- + Indicates if the person or any of their representatives, family + members, or other closely related persons, declares that they hold + or have held an important public job or function, in any + jurisdiction. + enum: + - existing + - none + type: string + registered_address: + $ref: '#/components/schemas/address' + relationship: + $ref: '#/components/schemas/person_relationship' + requirements: + anyOf: + - $ref: '#/components/schemas/person_requirements' + nullable: true + ssn_last_4_provided: + description: >- + Whether the last four digits of the person's Social Security number + have been provided (U.S. only). + type: boolean + us_cfpb_data: + anyOf: + - $ref: '#/components/schemas/person_us_cfpb_data' + description: Demographic data related to the person. + nullable: true + verification: + $ref: '#/components/schemas/legal_entity_person_verification' + required: + - account + - created + - id + - object + title: Person + type: object + x-expandableFields: + - additional_tos_acceptances + - address + - address_kana + - address_kanji + - dob + - future_requirements + - registered_address + - relationship + - requirements + - us_cfpb_data + - verification + x-resourceId: person + person_additional_tos_acceptance: + description: '' + properties: + date: + description: >- + The Unix timestamp marking when the legal guardian accepted the + service agreement. + format: unix-time + nullable: true + type: integer + ip: + description: >- + The IP address from which the legal guardian accepted the service + agreement. + maxLength: 5000 + nullable: true + type: string + user_agent: + description: >- + The user agent of the browser from which the legal guardian accepted + the service agreement. + maxLength: 5000 + nullable: true + type: string + title: PersonAdditionalTOSAcceptance + type: object + x-expandableFields: [] + person_additional_tos_acceptances: + description: '' + properties: + account: + anyOf: + - $ref: '#/components/schemas/person_additional_tos_acceptance' + description: >- + Details on the legal guardian's acceptance of the main Stripe + service agreement. + nullable: true + title: PersonAdditionalTOSAcceptances + type: object + x-expandableFields: + - account + person_ethnicity_details: + description: '' + properties: + ethnicity: + description: The persons ethnicity + items: + enum: + - cuban + - hispanic_or_latino + - mexican + - not_hispanic_or_latino + - other_hispanic_or_latino + - prefer_not_to_answer + - puerto_rican + type: string + nullable: true + type: array + ethnicity_other: + description: 'Please specify your origin, when other is selected.' + maxLength: 5000 + nullable: true + type: string + title: PersonEthnicityDetails + type: object + x-expandableFields: [] + person_future_requirements: + description: '' + properties: + alternatives: + description: >- + Fields that are due and can be satisfied by providing the + corresponding alternative fields instead. + items: + $ref: '#/components/schemas/account_requirements_alternative' + nullable: true + type: array + currently_due: + description: >- + Fields that need to be collected to keep the person's account + enabled. If not collected by the account's + `future_requirements[current_deadline]`, these fields will + transition to the main `requirements` hash, and may immediately + become `past_due`, but the account may also be given a grace period + depending on the account's enablement state prior to transition. + items: + maxLength: 5000 + type: string + type: array + errors: + description: >- + Fields that are `currently_due` and need to be collected again + because validation or verification failed. + items: + $ref: '#/components/schemas/account_requirements_error' + type: array + eventually_due: + description: >- + Fields you must collect when all thresholds are reached. As they + become required, they appear in `currently_due` as well, and the + account's `future_requirements[current_deadline]` becomes set. + items: + maxLength: 5000 + type: string + type: array + past_due: + description: >- + Fields that weren't collected by the account's + `requirements.current_deadline`. These fields need to be collected + to enable the person's account. New fields will never appear here; + `future_requirements.past_due` will always be a subset of + `requirements.past_due`. + items: + maxLength: 5000 + type: string + type: array + pending_verification: + description: >- + Fields that might become required depending on the results of + verification or review. It's an empty array unless an asynchronous + verification is pending. If verification fails, these fields move to + `eventually_due` or `currently_due`. Fields might appear in + `eventually_due` or `currently_due` and in `pending_verification` if + verification fails but another verification is still pending. + items: + maxLength: 5000 + type: string + type: array + required: + - currently_due + - errors + - eventually_due + - past_due + - pending_verification + title: PersonFutureRequirements + type: object + x-expandableFields: + - alternatives + - errors + person_race_details: + description: '' + properties: + race: + description: The persons race. + items: + enum: + - african_american + - american_indian_or_alaska_native + - asian + - asian_indian + - black_or_african_american + - chinese + - ethiopian + - filipino + - guamanian_or_chamorro + - haitian + - jamaican + - japanese + - korean + - native_hawaiian + - native_hawaiian_or_other_pacific_islander + - nigerian + - other_asian + - other_black_or_african_american + - other_pacific_islander + - prefer_not_to_answer + - samoan + - somali + - vietnamese + - white + type: string + nullable: true + type: array + race_other: + description: 'Please specify your race, when other is selected.' + maxLength: 5000 + nullable: true + type: string + title: PersonRaceDetails + type: object + x-expandableFields: [] + person_relationship: + description: '' + properties: + authorizer: + description: >- + Whether the person is the authorizer of the account's + representative. + nullable: true + type: boolean + director: + description: >- + Whether the person is a director of the account's legal entity. + Directors are typically members of the governing board of the + company, or responsible for ensuring the company meets its + regulatory obligations. + nullable: true + type: boolean + executive: + description: >- + Whether the person has significant responsibility to control, + manage, or direct the organization. + nullable: true + type: boolean + legal_guardian: + description: >- + Whether the person is the legal guardian of the account's + representative. + nullable: true + type: boolean + owner: + description: Whether the person is an owner of the account’s legal entity. + nullable: true + type: boolean + percent_ownership: + description: The percent owned by the person of the account's legal entity. + nullable: true + type: number + representative: + description: >- + Whether the person is authorized as the primary representative of + the account. This is the person nominated by the business to provide + information about themselves, and general information about the + account. There can only be one representative at any given time. At + the time the account is created, this person should be set to the + person responsible for opening the account. + nullable: true + type: boolean + title: + description: 'The person''s title (e.g., CEO, Support Engineer).' + maxLength: 5000 + nullable: true + type: string + title: PersonRelationship + type: object + x-expandableFields: [] + person_requirements: + description: '' + properties: + alternatives: + description: >- + Fields that are due and can be satisfied by providing the + corresponding alternative fields instead. + items: + $ref: '#/components/schemas/account_requirements_alternative' + nullable: true + type: array + currently_due: + description: >- + Fields that need to be collected to keep the person's account + enabled. If not collected by the account's `current_deadline`, these + fields appear in `past_due` as well, and the account is disabled. + items: + maxLength: 5000 + type: string + type: array + errors: + description: >- + Fields that are `currently_due` and need to be collected again + because validation or verification failed. + items: + $ref: '#/components/schemas/account_requirements_error' + type: array + eventually_due: + description: >- + Fields you must collect when all thresholds are reached. As they + become required, they appear in `currently_due` as well, and the + account's `current_deadline` becomes set. + items: + maxLength: 5000 + type: string + type: array + past_due: + description: >- + Fields that weren't collected by the account's `current_deadline`. + These fields need to be collected to enable the person's account. + items: + maxLength: 5000 + type: string + type: array + pending_verification: + description: >- + Fields that might become required depending on the results of + verification or review. It's an empty array unless an asynchronous + verification is pending. If verification fails, these fields move to + `eventually_due`, `currently_due`, or `past_due`. Fields might + appear in `eventually_due`, `currently_due`, or `past_due` and in + `pending_verification` if verification fails but another + verification is still pending. + items: + maxLength: 5000 + type: string + type: array + required: + - currently_due + - errors + - eventually_due + - past_due + - pending_verification + title: PersonRequirements + type: object + x-expandableFields: + - alternatives + - errors + person_us_cfpb_data: + description: '' + properties: + ethnicity_details: + anyOf: + - $ref: '#/components/schemas/person_ethnicity_details' + description: The persons ethnicity details + nullable: true + race_details: + anyOf: + - $ref: '#/components/schemas/person_race_details' + description: The persons race details + nullable: true + self_identified_gender: + description: The persons self-identified gender + maxLength: 5000 + nullable: true + type: string + title: PersonUSCfpbData + type: object + x-expandableFields: + - ethnicity_details + - race_details + plan: + description: >- + You can now model subscriptions more flexibly using the [Prices + API](https://stripe.com/docs/api#prices). It replaces the Plans API and + is backwards compatible to simplify your migration. + + + Plans define the base price, currency, and billing cycle for recurring + purchases of products. + + [Products](https://stripe.com/docs/api#products) help you track + inventory or provisioning, and plans help you track pricing. Different + physical goods or levels of service should be represented by products, + and pricing options should be represented by plans. This approach lets + you change prices without having to change your provisioning scheme. + + + For example, you might have a single "gold" product that has plans for + $10/month, $100/year, €9/month, and €90/year. + + + Related guides: [Set up a + subscription](https://stripe.com/docs/billing/subscriptions/set-up-subscription) + and more about [products and + prices](https://stripe.com/docs/products-prices/overview). + properties: + active: + description: Whether the plan can be used for new purchases. + type: boolean + amount: + description: >- + The unit amount in cents (or local equivalent) to be charged, + represented as a whole integer if possible. Only set if + `billing_scheme=per_unit`. + nullable: true + type: integer + amount_decimal: + description: >- + The unit amount in cents (or local equivalent) to be charged, + represented as a decimal string with at most 12 decimal places. Only + set if `billing_scheme=per_unit`. + format: decimal + nullable: true + type: string + billing_scheme: + description: >- + Describes how to compute the price per period. Either `per_unit` or + `tiered`. `per_unit` indicates that the fixed amount (specified in + `amount`) will be charged per unit in `quantity` (for plans with + `usage_type=licensed`), or per unit of total usage (for plans with + `usage_type=metered`). `tiered` indicates that the unit pricing will + be computed using a tiering strategy as defined using the `tiers` + and `tiers_mode` attributes. + enum: + - per_unit + - tiered + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + interval: + description: >- + The frequency at which a subscription is billed. One of `day`, + `week`, `month` or `year`. + enum: + - day + - month + - week + - year + type: string + interval_count: + description: >- + The number of intervals (specified in the `interval` attribute) + between subscription billings. For example, `interval=month` and + `interval_count=3` bills every 3 months. + type: integer + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + meter: + description: The meter tracking the usage of a metered price + maxLength: 5000 + nullable: true + type: string + nickname: + description: 'A brief description of the plan, hidden from customers.' + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - plan + type: string + product: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/product' + - $ref: '#/components/schemas/deleted_product' + description: The product whose pricing this plan determines. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/product' + - $ref: '#/components/schemas/deleted_product' + tiers: + description: >- + Each element represents a pricing tier. This parameter requires + `billing_scheme` to be set to `tiered`. See also the documentation + for `billing_scheme`. + items: + $ref: '#/components/schemas/plan_tier' + type: array + tiers_mode: + description: >- + Defines if the tiering price should be `graduated` or `volume` + based. In `volume`-based tiering, the maximum quantity within a + period determines the per unit price. In `graduated` tiering, + pricing can change as the quantity grows. + enum: + - graduated + - volume + nullable: true + type: string + transform_usage: + anyOf: + - $ref: '#/components/schemas/transform_usage' + description: >- + Apply a transformation to the reported usage or set quantity before + computing the amount billed. Cannot be combined with `tiers`. + nullable: true + trial_period_days: + description: >- + Default number of trial days when subscribing a customer to this + plan using + [`trial_from_plan=true`](https://stripe.com/docs/api#create_subscription-trial_from_plan). + nullable: true + type: integer + usage_type: + description: >- + Configures how the quantity per period should be determined. Can be + either `metered` or `licensed`. `licensed` automatically bills the + `quantity` set when adding it to a subscription. `metered` + aggregates the total usage based on usage records. Defaults to + `licensed`. + enum: + - licensed + - metered + type: string + required: + - active + - billing_scheme + - created + - currency + - id + - interval + - interval_count + - livemode + - object + - usage_type + title: Plan + type: object + x-expandableFields: + - product + - tiers + - transform_usage + x-resourceId: plan + plan_tier: + description: '' + properties: + flat_amount: + description: Price for the entire tier. + nullable: true + type: integer + flat_amount_decimal: + description: >- + Same as `flat_amount`, but contains a decimal value with at most 12 + decimal places. + format: decimal + nullable: true + type: string + unit_amount: + description: Per unit price for units relevant to the tier. + nullable: true + type: integer + unit_amount_decimal: + description: >- + Same as `unit_amount`, but contains a decimal value with at most 12 + decimal places. + format: decimal + nullable: true + type: string + up_to: + description: Up to and including to this quantity will be contained in the tier. + nullable: true + type: integer + title: PlanTier + type: object + x-expandableFields: [] + platform_earning_fee_source: + description: '' + properties: + charge: + description: Charge ID that created this application fee. + maxLength: 5000 + type: string + payout: + description: Payout ID that created this application fee. + maxLength: 5000 + type: string + type: + description: Type of object that created the application fee. + enum: + - charge + - payout + type: string + x-stripeBypassValidation: true + required: + - type + title: PlatformEarningFeeSource + type: object + x-expandableFields: [] + portal_business_profile: + description: '' + properties: + headline: + description: The messaging shown to customers in the portal. + maxLength: 5000 + nullable: true + type: string + privacy_policy_url: + description: A link to the business’s publicly available privacy policy. + maxLength: 5000 + nullable: true + type: string + terms_of_service_url: + description: A link to the business’s publicly available terms of service. + maxLength: 5000 + nullable: true + type: string + title: PortalBusinessProfile + type: object + x-expandableFields: [] + portal_customer_update: + description: '' + properties: + allowed_updates: + description: >- + The types of customer updates that are supported. When empty, + customers are not updateable. + items: + enum: + - address + - email + - name + - phone + - shipping + - tax_id + type: string + type: array + enabled: + description: Whether the feature is enabled. + type: boolean + required: + - allowed_updates + - enabled + title: PortalCustomerUpdate + type: object + x-expandableFields: [] + portal_features: + description: '' + properties: + customer_update: + $ref: '#/components/schemas/portal_customer_update' + invoice_history: + $ref: '#/components/schemas/portal_invoice_list' + payment_method_update: + $ref: '#/components/schemas/portal_payment_method_update' + subscription_cancel: + $ref: '#/components/schemas/portal_subscription_cancel' + subscription_update: + $ref: '#/components/schemas/portal_subscription_update' + required: + - customer_update + - invoice_history + - payment_method_update + - subscription_cancel + - subscription_update + title: PortalFeatures + type: object + x-expandableFields: + - customer_update + - invoice_history + - payment_method_update + - subscription_cancel + - subscription_update + portal_flows_after_completion_hosted_confirmation: + description: '' + properties: + custom_message: + description: >- + A custom message to display to the customer after the flow is + completed. + maxLength: 5000 + nullable: true + type: string + title: PortalFlowsAfterCompletionHostedConfirmation + type: object + x-expandableFields: [] + portal_flows_after_completion_redirect: + description: '' + properties: + return_url: + description: >- + The URL the customer will be redirected to after the flow is + completed. + maxLength: 5000 + type: string + required: + - return_url + title: PortalFlowsAfterCompletionRedirect + type: object + x-expandableFields: [] + portal_flows_coupon_offer: + description: '' + properties: + coupon: + description: The ID of the coupon to be offered. + maxLength: 5000 + type: string + required: + - coupon + title: PortalFlowsCouponOffer + type: object + x-expandableFields: [] + portal_flows_flow: + description: '' + properties: + after_completion: + $ref: '#/components/schemas/portal_flows_flow_after_completion' + subscription_cancel: + anyOf: + - $ref: '#/components/schemas/portal_flows_flow_subscription_cancel' + description: Configuration when `flow.type=subscription_cancel`. + nullable: true + subscription_update: + anyOf: + - $ref: '#/components/schemas/portal_flows_flow_subscription_update' + description: Configuration when `flow.type=subscription_update`. + nullable: true + subscription_update_confirm: + anyOf: + - $ref: >- + #/components/schemas/portal_flows_flow_subscription_update_confirm + description: Configuration when `flow.type=subscription_update_confirm`. + nullable: true + type: + description: Type of flow that the customer will go through. + enum: + - payment_method_update + - subscription_cancel + - subscription_update + - subscription_update_confirm + type: string + required: + - after_completion + - type + title: PortalFlowsFlow + type: object + x-expandableFields: + - after_completion + - subscription_cancel + - subscription_update + - subscription_update_confirm + portal_flows_flow_after_completion: + description: '' + properties: + hosted_confirmation: + anyOf: + - $ref: >- + #/components/schemas/portal_flows_after_completion_hosted_confirmation + description: Configuration when `after_completion.type=hosted_confirmation`. + nullable: true + redirect: + anyOf: + - $ref: '#/components/schemas/portal_flows_after_completion_redirect' + description: Configuration when `after_completion.type=redirect`. + nullable: true + type: + description: The specified type of behavior after the flow is completed. + enum: + - hosted_confirmation + - portal_homepage + - redirect + type: string + required: + - type + title: PortalFlowsFlowAfterCompletion + type: object + x-expandableFields: + - hosted_confirmation + - redirect + portal_flows_flow_subscription_cancel: + description: '' + properties: + retention: + anyOf: + - $ref: '#/components/schemas/portal_flows_retention' + description: Specify a retention strategy to be used in the cancellation flow. + nullable: true + subscription: + description: The ID of the subscription to be canceled. + maxLength: 5000 + type: string + required: + - subscription + title: PortalFlowsFlowSubscriptionCancel + type: object + x-expandableFields: + - retention + portal_flows_flow_subscription_update: + description: '' + properties: + subscription: + description: The ID of the subscription to be updated. + maxLength: 5000 + type: string + required: + - subscription + title: PortalFlowsFlowSubscriptionUpdate + type: object + x-expandableFields: [] + portal_flows_flow_subscription_update_confirm: + description: '' + properties: + discounts: + description: >- + The coupon or promotion code to apply to this subscription update. + Currently, only up to one may be specified. + items: + $ref: >- + #/components/schemas/portal_flows_subscription_update_confirm_discount + nullable: true + type: array + items: + description: >- + The [subscription + item](https://stripe.com/docs/api/subscription_items) to be updated + through this flow. Currently, only up to one may be specified and + subscriptions with multiple items are not updatable. + items: + $ref: '#/components/schemas/portal_flows_subscription_update_confirm_item' + type: array + subscription: + description: The ID of the subscription to be updated. + maxLength: 5000 + type: string + required: + - items + - subscription + title: PortalFlowsFlowSubscriptionUpdateConfirm + type: object + x-expandableFields: + - discounts + - items + portal_flows_retention: + description: '' + properties: + coupon_offer: + anyOf: + - $ref: '#/components/schemas/portal_flows_coupon_offer' + description: Configuration when `retention.type=coupon_offer`. + nullable: true + type: + description: Type of retention strategy that will be used. + enum: + - coupon_offer + type: string + required: + - type + title: PortalFlowsRetention + type: object + x-expandableFields: + - coupon_offer + portal_flows_subscription_update_confirm_discount: + description: '' + properties: + coupon: + description: The ID of the coupon to apply to this subscription update. + maxLength: 5000 + nullable: true + type: string + promotion_code: + description: The ID of a promotion code to apply to this subscription update. + maxLength: 5000 + nullable: true + type: string + title: PortalFlowsSubscriptionUpdateConfirmDiscount + type: object + x-expandableFields: [] + portal_flows_subscription_update_confirm_item: + description: '' + properties: + id: + description: >- + The ID of the [subscription + item](https://stripe.com/docs/api/subscriptions/object#subscription_object-items-data-id) + to be updated. + maxLength: 5000 + nullable: true + type: string + price: + description: >- + The price the customer should subscribe to through this flow. The + price must also be included in the configuration's + [`features.subscription_update.products`](https://stripe.com/docs/api/customer_portal/configuration#portal_configuration_object-features-subscription_update-products). + maxLength: 5000 + nullable: true + type: string + quantity: + description: >- + [Quantity](https://stripe.com/docs/subscriptions/quantities) for + this item that the customer should subscribe to through this flow. + type: integer + title: PortalFlowsSubscriptionUpdateConfirmItem + type: object + x-expandableFields: [] + portal_invoice_list: + description: '' + properties: + enabled: + description: Whether the feature is enabled. + type: boolean + required: + - enabled + title: PortalInvoiceList + type: object + x-expandableFields: [] + portal_login_page: + description: '' + properties: + enabled: + description: >- + If `true`, a shareable `url` will be generated that will take your + customers to a hosted login page for the customer portal. + + + If `false`, the previously generated `url`, if any, will be + deactivated. + type: boolean + url: + description: >- + A shareable URL to the hosted portal login page. Your customers will + be able to log in with their + [email](https://stripe.com/docs/api/customers/object#customer_object-email) + and receive a link to their customer portal. + maxLength: 5000 + nullable: true + type: string + required: + - enabled + title: PortalLoginPage + type: object + x-expandableFields: [] + portal_payment_method_update: + description: '' + properties: + enabled: + description: Whether the feature is enabled. + type: boolean + required: + - enabled + title: PortalPaymentMethodUpdate + type: object + x-expandableFields: [] + portal_resource_schedule_update_at_period_end: + description: '' + properties: + conditions: + description: >- + List of conditions. When any condition is true, an update will be + scheduled at the end of the current period. + items: + $ref: >- + #/components/schemas/portal_resource_schedule_update_at_period_end_condition + type: array + required: + - conditions + title: PortalResourceScheduleUpdateAtPeriodEnd + type: object + x-expandableFields: + - conditions + portal_resource_schedule_update_at_period_end_condition: + description: '' + properties: + type: + description: The type of condition. + enum: + - decreasing_item_amount + - shortening_interval + type: string + required: + - type + title: PortalResourceScheduleUpdateAtPeriodEndCondition + type: object + x-expandableFields: [] + portal_subscription_cancel: + description: '' + properties: + cancellation_reason: + $ref: '#/components/schemas/portal_subscription_cancellation_reason' + enabled: + description: Whether the feature is enabled. + type: boolean + mode: + description: >- + Whether to cancel subscriptions immediately or at the end of the + billing period. + enum: + - at_period_end + - immediately + type: string + proration_behavior: + description: >- + Whether to create prorations when canceling subscriptions. Possible + values are `none` and `create_prorations`. + enum: + - always_invoice + - create_prorations + - none + type: string + required: + - cancellation_reason + - enabled + - mode + - proration_behavior + title: PortalSubscriptionCancel + type: object + x-expandableFields: + - cancellation_reason + portal_subscription_cancellation_reason: + description: '' + properties: + enabled: + description: Whether the feature is enabled. + type: boolean + options: + description: Which cancellation reasons will be given as options to the customer. + items: + enum: + - customer_service + - low_quality + - missing_features + - other + - switched_service + - too_complex + - too_expensive + - unused + type: string + type: array + required: + - enabled + - options + title: PortalSubscriptionCancellationReason + type: object + x-expandableFields: [] + portal_subscription_update: + description: '' + properties: + default_allowed_updates: + description: >- + The types of subscription updates that are supported for items + listed in the `products` attribute. When empty, subscriptions are + not updateable. + items: + enum: + - price + - promotion_code + - quantity + type: string + type: array + enabled: + description: Whether the feature is enabled. + type: boolean + products: + description: The list of up to 10 products that support subscription updates. + items: + $ref: '#/components/schemas/portal_subscription_update_product' + nullable: true + type: array + proration_behavior: + description: >- + Determines how to handle prorations resulting from subscription + updates. Valid values are `none`, `create_prorations`, and + `always_invoice`. Defaults to a value of `none` if you don't set it + during creation. + enum: + - always_invoice + - create_prorations + - none + type: string + schedule_at_period_end: + $ref: '#/components/schemas/portal_resource_schedule_update_at_period_end' + required: + - default_allowed_updates + - enabled + - proration_behavior + - schedule_at_period_end + title: PortalSubscriptionUpdate + type: object + x-expandableFields: + - products + - schedule_at_period_end + portal_subscription_update_product: + description: '' + properties: + prices: + description: >- + The list of price IDs which, when subscribed to, a subscription can + be updated. + items: + maxLength: 5000 + type: string + type: array + product: + description: The product ID. + maxLength: 5000 + type: string + required: + - prices + - product + title: PortalSubscriptionUpdateProduct + type: object + x-expandableFields: [] + price: + description: >- + Prices define the unit cost, currency, and (optional) billing cycle for + both recurring and one-time purchases of products. + + [Products](https://stripe.com/docs/api#products) help you track + inventory or provisioning, and prices help you track payment terms. + Different physical goods or levels of service should be represented by + products, and pricing options should be represented by prices. This + approach lets you change prices without having to change your + provisioning scheme. + + + For example, you might have a single "gold" product that has prices for + $10/month, $100/year, and €9 once. + + + Related guides: [Set up a + subscription](https://stripe.com/docs/billing/subscriptions/set-up-subscription), + [create an invoice](https://stripe.com/docs/billing/invoices/create), + and more about [products and + prices](https://stripe.com/docs/products-prices/overview). + properties: + active: + description: Whether the price can be used for new purchases. + type: boolean + billing_scheme: + description: >- + Describes how to compute the price per period. Either `per_unit` or + `tiered`. `per_unit` indicates that the fixed amount (specified in + `unit_amount` or `unit_amount_decimal`) will be charged per unit in + `quantity` (for prices with `usage_type=licensed`), or per unit of + total usage (for prices with `usage_type=metered`). `tiered` + indicates that the unit pricing will be computed using a tiering + strategy as defined using the `tiers` and `tiers_mode` attributes. + enum: + - per_unit + - tiered + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + currency_options: + additionalProperties: + $ref: '#/components/schemas/currency_option' + description: >- + Prices defined in each available currency option. Each key must be a + three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html) and a + [supported currency](https://stripe.com/docs/currencies). + type: object + custom_unit_amount: + anyOf: + - $ref: '#/components/schemas/custom_unit_amount' + description: >- + When set, provides configuration for the amount to be adjusted by + the customer during Checkout Sessions and Payment Links. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + lookup_key: + description: >- + A lookup key used to retrieve prices dynamically from a static + string. This may be up to 200 characters. + maxLength: 5000 + nullable: true + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + nickname: + description: 'A brief description of the price, hidden from customers.' + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - price + type: string + product: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/product' + - $ref: '#/components/schemas/deleted_product' + description: The ID of the product this price is associated with. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/product' + - $ref: '#/components/schemas/deleted_product' + recurring: + anyOf: + - $ref: '#/components/schemas/recurring' + description: >- + The recurring components of a price such as `interval` and + `usage_type`. + nullable: true + tax_behavior: + description: >- + Only required if a [default tax + behavior](https://stripe.com/docs/tax/products-prices-tax-categories-tax-behavior#setting-a-default-tax-behavior-(recommended)) + was not provided in the Stripe Tax settings. Specifies whether the + price is considered inclusive of taxes or exclusive of taxes. One of + `inclusive`, `exclusive`, or `unspecified`. Once specified as either + `inclusive` or `exclusive`, it cannot be changed. + enum: + - exclusive + - inclusive + - unspecified + nullable: true + type: string + tiers: + description: >- + Each element represents a pricing tier. This parameter requires + `billing_scheme` to be set to `tiered`. See also the documentation + for `billing_scheme`. + items: + $ref: '#/components/schemas/price_tier' + type: array + tiers_mode: + description: >- + Defines if the tiering price should be `graduated` or `volume` + based. In `volume`-based tiering, the maximum quantity within a + period determines the per unit price. In `graduated` tiering, + pricing can change as the quantity grows. + enum: + - graduated + - volume + nullable: true + type: string + transform_quantity: + anyOf: + - $ref: '#/components/schemas/transform_quantity' + description: >- + Apply a transformation to the reported usage or set quantity before + computing the amount billed. Cannot be combined with `tiers`. + nullable: true + type: + description: >- + One of `one_time` or `recurring` depending on whether the price is + for a one-time purchase or a recurring (subscription) purchase. + enum: + - one_time + - recurring + type: string + unit_amount: + description: >- + The unit amount in cents (or local equivalent) to be charged, + represented as a whole integer if possible. Only set if + `billing_scheme=per_unit`. + nullable: true + type: integer + unit_amount_decimal: + description: >- + The unit amount in cents (or local equivalent) to be charged, + represented as a decimal string with at most 12 decimal places. Only + set if `billing_scheme=per_unit`. + format: decimal + nullable: true + type: string + required: + - active + - billing_scheme + - created + - currency + - id + - livemode + - metadata + - object + - product + - type + title: Price + type: object + x-expandableFields: + - currency_options + - custom_unit_amount + - product + - recurring + - tiers + - transform_quantity + x-resourceId: price + price_tier: + description: '' + properties: + flat_amount: + description: Price for the entire tier. + nullable: true + type: integer + flat_amount_decimal: + description: >- + Same as `flat_amount`, but contains a decimal value with at most 12 + decimal places. + format: decimal + nullable: true + type: string + unit_amount: + description: Per unit price for units relevant to the tier. + nullable: true + type: integer + unit_amount_decimal: + description: >- + Same as `unit_amount`, but contains a decimal value with at most 12 + decimal places. + format: decimal + nullable: true + type: string + up_to: + description: Up to and including to this quantity will be contained in the tier. + nullable: true + type: integer + title: PriceTier + type: object + x-expandableFields: [] + product: + description: >- + Products describe the specific goods or services you offer to your + customers. + + For example, you might offer a Standard and Premium version of your + goods or service; each version would be a separate Product. + + They can be used in conjunction with + [Prices](https://stripe.com/docs/api#prices) to configure pricing in + Payment Links, Checkout, and Subscriptions. + + + Related guides: [Set up a + subscription](https://stripe.com/docs/billing/subscriptions/set-up-subscription), + + [share a Payment Link](https://stripe.com/docs/payment-links), + + [accept payments with + Checkout](https://stripe.com/docs/payments/accept-a-payment#create-product-prices-upfront), + + and more about [Products and + Prices](https://stripe.com/docs/products-prices/overview) + properties: + active: + description: Whether the product is currently available for purchase. + type: boolean + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + default_price: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/price' + description: >- + The ID of the [Price](https://stripe.com/docs/api/prices) object + that is the default price for this product. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/price' + description: + description: >- + The product's description, meant to be displayable to the customer. + Use this field to optionally store a long form explanation of the + product being sold for your own rendering purposes. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + images: + description: >- + A list of up to 8 URLs of images for this product, meant to be + displayable to the customer. + items: + maxLength: 5000 + type: string + type: array + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + marketing_features: + description: >- + A list of up to 15 marketing features for this product. These are + displayed in [pricing + tables](https://stripe.com/docs/payments/checkout/pricing-table). + items: + $ref: '#/components/schemas/product_marketing_feature' + type: array + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + name: + description: 'The product''s name, meant to be displayable to the customer.' + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - product + type: string + package_dimensions: + anyOf: + - $ref: '#/components/schemas/package_dimensions' + description: The dimensions of this product for shipping purposes. + nullable: true + shippable: + description: 'Whether this product is shipped (i.e., physical goods).' + nullable: true + type: boolean + statement_descriptor: + description: >- + Extra information about a product which will appear on your + customer's credit card statement. In the case that multiple products + are billed at once, the first statement descriptor will be used. + Only used for subscription payments. + maxLength: 5000 + nullable: true + type: string + tax_code: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_code' + description: 'A [tax code](https://stripe.com/docs/tax/tax-categories) ID.' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_code' + unit_label: + description: >- + A label that represents units of this product. When set, this will + be included in customers' receipts, invoices, Checkout, and the + customer portal. + maxLength: 5000 + nullable: true + type: string + updated: + description: >- + Time at which the object was last updated. Measured in seconds since + the Unix epoch. + format: unix-time + type: integer + url: + description: A URL of a publicly-accessible webpage for this product. + maxLength: 2048 + nullable: true + type: string + required: + - active + - created + - id + - images + - livemode + - marketing_features + - metadata + - name + - object + - updated + title: Product + type: object + x-expandableFields: + - default_price + - marketing_features + - package_dimensions + - tax_code + x-resourceId: product + product_feature: + description: >- + A product_feature represents an attachment between a feature and a + product. + + When a product is purchased that has a feature attached, Stripe will + create an entitlement to the feature for the purchasing customer. + properties: + entitlement_feature: + $ref: '#/components/schemas/entitlements.feature' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - product_feature + type: string + required: + - entitlement_feature + - id + - livemode + - object + title: ProductFeature + type: object + x-expandableFields: + - entitlement_feature + x-resourceId: product_feature + product_marketing_feature: + description: '' + properties: + name: + description: The marketing feature name. Up to 80 characters long. + maxLength: 5000 + type: string + title: ProductMarketingFeature + type: object + x-expandableFields: [] + promotion_code: + description: >- + A Promotion Code represents a customer-redeemable code for a + [coupon](https://stripe.com/docs/api#coupons). It can be used to + + create multiple codes for a single coupon. + properties: + active: + description: >- + Whether the promotion code is currently active. A promotion code is + only active if the coupon is also valid. + type: boolean + code: + description: >- + The customer-facing code. Regardless of case, this code must be + unique across all active promotion codes for each customer. Valid + characters are lower case letters (a-z), upper case letters (A-Z), + and digits (0-9). + maxLength: 5000 + type: string + coupon: + $ref: '#/components/schemas/coupon' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: The customer that this promotion code can be used by. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + expires_at: + description: Date at which the promotion code can no longer be redeemed. + format: unix-time + nullable: true + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + max_redemptions: + description: Maximum number of times this promotion code can be redeemed. + nullable: true + type: integer + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - promotion_code + type: string + restrictions: + $ref: '#/components/schemas/promotion_codes_resource_restrictions' + times_redeemed: + description: Number of times this promotion code has been used. + type: integer + required: + - active + - code + - coupon + - created + - id + - livemode + - object + - restrictions + - times_redeemed + title: PromotionCode + type: object + x-expandableFields: + - coupon + - customer + - restrictions + x-resourceId: promotion_code + promotion_code_currency_option: + description: '' + properties: + minimum_amount: + description: >- + Minimum amount required to redeem this Promotion Code into a Coupon + (e.g., a purchase must be $100 or more to work). + type: integer + required: + - minimum_amount + title: PromotionCodeCurrencyOption + type: object + x-expandableFields: [] + promotion_codes_resource_restrictions: + description: '' + properties: + currency_options: + additionalProperties: + $ref: '#/components/schemas/promotion_code_currency_option' + description: >- + Promotion code restrictions defined in each available currency + option. Each key must be a three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html) and a + [supported currency](https://stripe.com/docs/currencies). + type: object + first_time_transaction: + description: >- + A Boolean indicating if the Promotion Code should only be redeemed + for Customers without any successful payments or invoices + type: boolean + minimum_amount: + description: >- + Minimum amount required to redeem this Promotion Code into a Coupon + (e.g., a purchase must be $100 or more to work). + nullable: true + type: integer + minimum_amount_currency: + description: >- + Three-letter [ISO code](https://stripe.com/docs/currencies) for + minimum_amount + maxLength: 5000 + nullable: true + type: string + required: + - first_time_transaction + title: PromotionCodesResourceRestrictions + type: object + x-expandableFields: + - currency_options + quote: + description: >- + A Quote is a way to model prices that you'd like to provide to a + customer. + + Once accepted, it will automatically create an invoice, subscription or + subscription schedule. + properties: + amount_subtotal: + description: Total before any discounts or taxes are applied. + type: integer + amount_total: + description: Total after discounts and taxes are applied. + type: integer + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + description: ID of the Connect Application that created the quote. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + application_fee_amount: + description: >- + The amount of the application fee (if any) that will be requested to + be applied to the payment and transferred to the application owner's + Stripe account. Only applicable if there are no line items with + recurring prices on the quote. + nullable: true + type: integer + application_fee_percent: + description: >- + A non-negative decimal between 0 and 100, with at most two decimal + places. This represents the percentage of the subscription invoice + total that will be transferred to the application owner's Stripe + account. Only applicable if there are line items with recurring + prices on the quote. + nullable: true + type: number + automatic_tax: + $ref: '#/components/schemas/quotes_resource_automatic_tax' + collection_method: + description: >- + Either `charge_automatically`, or `send_invoice`. When charging + automatically, Stripe will attempt to pay invoices at the end of the + subscription cycle or on finalization using the default payment + method attached to the subscription or customer. When sending an + invoice, Stripe will email your customer an invoice with payment + instructions and mark the subscription as `active`. Defaults to + `charge_automatically`. + enum: + - charge_automatically + - send_invoice + type: string + computed: + $ref: '#/components/schemas/quotes_resource_computed' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + nullable: true + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: >- + The customer which this quote belongs to. A customer is required + before finalizing the quote. Once specified, it cannot be changed. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + default_tax_rates: + description: The tax rates applied to this quote. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_rate' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_rate' + type: array + description: + description: A description that will be displayed on the quote PDF. + maxLength: 5000 + nullable: true + type: string + discounts: + description: The discounts applied to this quote. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + type: array + expires_at: + description: >- + The date on which the quote will be canceled if in `open` or `draft` + status. Measured in seconds since the Unix epoch. + format: unix-time + type: integer + footer: + description: A footer that will be displayed on the quote PDF. + maxLength: 5000 + nullable: true + type: string + from_quote: + anyOf: + - $ref: '#/components/schemas/quotes_resource_from_quote' + description: >- + Details of the quote that was cloned. See the [cloning + documentation](https://stripe.com/docs/quotes/clone) for more + details. + nullable: true + header: + description: A header that will be displayed on the quote PDF. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + - $ref: '#/components/schemas/deleted_invoice' + description: The invoice that was created from this quote. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + - $ref: '#/components/schemas/deleted_invoice' + invoice_settings: + $ref: '#/components/schemas/invoice_setting_quote_setting' + line_items: + description: A list of items the customer is being quoted for. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: QuotesResourceListLineItems + type: object + x-expandableFields: + - data + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + number: + description: >- + A unique number that identifies this particular quote. This number + is assigned once the quote is + [finalized](https://stripe.com/docs/quotes/overview#finalize). + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - quote + type: string + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account on behalf of which to charge. See the [Connect + documentation](https://support.stripe.com/questions/sending-invoices-on-behalf-of-connected-accounts) + for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + status: + description: The status of the quote. + enum: + - accepted + - canceled + - draft + - open + type: string + x-stripeBypassValidation: true + status_transitions: + $ref: '#/components/schemas/quotes_resource_status_transitions' + subscription: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/subscription' + description: The subscription that was created or updated from this quote. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/subscription' + subscription_data: + $ref: >- + #/components/schemas/quotes_resource_subscription_data_subscription_data + subscription_schedule: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/subscription_schedule' + description: >- + The subscription schedule that was created or updated from this + quote. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/subscription_schedule' + test_clock: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/test_helpers.test_clock' + description: ID of the test clock this quote belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/test_helpers.test_clock' + total_details: + $ref: '#/components/schemas/quotes_resource_total_details' + transfer_data: + anyOf: + - $ref: '#/components/schemas/quotes_resource_transfer_data' + description: >- + The account (if any) the payments will be attributed to for tax + reporting, and where funds from each payment will be transferred to + for each of the invoices. + nullable: true + required: + - amount_subtotal + - amount_total + - automatic_tax + - collection_method + - computed + - created + - discounts + - expires_at + - id + - invoice_settings + - livemode + - metadata + - object + - status + - status_transitions + - subscription_data + - total_details + title: Quote + type: object + x-expandableFields: + - application + - automatic_tax + - computed + - customer + - default_tax_rates + - discounts + - from_quote + - invoice + - invoice_settings + - line_items + - on_behalf_of + - status_transitions + - subscription + - subscription_data + - subscription_schedule + - test_clock + - total_details + - transfer_data + x-resourceId: quote + quotes_resource_automatic_tax: + description: '' + properties: + enabled: + description: Automatically calculate taxes + type: boolean + liability: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The account that's liable for tax. If set, the business address and + tax registrations required to perform the tax calculation are loaded + from this account. The tax transaction is returned in the report of + the connected account. + nullable: true + provider: + description: The tax provider powering automatic tax. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + The status of the most recent automated tax calculation for this + quote. + enum: + - complete + - failed + - requires_location_inputs + nullable: true + type: string + required: + - enabled + title: QuotesResourceAutomaticTax + type: object + x-expandableFields: + - liability + quotes_resource_computed: + description: '' + properties: + recurring: + anyOf: + - $ref: '#/components/schemas/quotes_resource_recurring' + description: >- + The definitive totals and line items the customer will be charged on + a recurring basis. Takes into account the line items with recurring + prices and discounts with `duration=forever` coupons only. Defaults + to `null` if no inputted line items with recurring prices. + nullable: true + upfront: + $ref: '#/components/schemas/quotes_resource_upfront' + required: + - upfront + title: QuotesResourceComputed + type: object + x-expandableFields: + - recurring + - upfront + quotes_resource_from_quote: + description: '' + properties: + is_revision: + description: Whether this quote is a revision of a different quote. + type: boolean + quote: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/quote' + description: The quote that was cloned. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/quote' + required: + - is_revision + - quote + title: QuotesResourceFromQuote + type: object + x-expandableFields: + - quote + quotes_resource_recurring: + description: '' + properties: + amount_subtotal: + description: Total before any discounts or taxes are applied. + type: integer + amount_total: + description: Total after discounts and taxes are applied. + type: integer + interval: + description: >- + The frequency at which a subscription is billed. One of `day`, + `week`, `month` or `year`. + enum: + - day + - month + - week + - year + type: string + interval_count: + description: >- + The number of intervals (specified in the `interval` attribute) + between subscription billings. For example, `interval=month` and + `interval_count=3` bills every 3 months. + type: integer + total_details: + $ref: '#/components/schemas/quotes_resource_total_details' + required: + - amount_subtotal + - amount_total + - interval + - interval_count + - total_details + title: QuotesResourceRecurring + type: object + x-expandableFields: + - total_details + quotes_resource_status_transitions: + description: '' + properties: + accepted_at: + description: >- + The time that the quote was accepted. Measured in seconds since Unix + epoch. + format: unix-time + nullable: true + type: integer + canceled_at: + description: >- + The time that the quote was canceled. Measured in seconds since Unix + epoch. + format: unix-time + nullable: true + type: integer + finalized_at: + description: >- + The time that the quote was finalized. Measured in seconds since + Unix epoch. + format: unix-time + nullable: true + type: integer + title: QuotesResourceStatusTransitions + type: object + x-expandableFields: [] + quotes_resource_subscription_data_subscription_data: + description: '' + properties: + description: + description: >- + The subscription's description, meant to be displayable to the + customer. Use this field to optionally store an explanation of the + subscription for rendering in Stripe surfaces and certain local + payment methods UIs. + maxLength: 5000 + nullable: true + type: string + effective_date: + description: >- + When creating a new subscription, the date of which the subscription + schedule will start after the quote is accepted. This date is + ignored if it is in the past when the quote is accepted. Measured in + seconds since the Unix epoch. + format: unix-time + nullable: true + type: integer + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + will set metadata on the subscription or subscription schedule when + the quote is accepted. If a recurring price is included in + `line_items`, this field will be passed to the resulting + subscription's `metadata` field. If + `subscription_data.effective_date` is used, this field will be + passed to the resulting subscription schedule's `phases.metadata` + field. Unlike object-level metadata, this field is declarative. + Updates will clear prior values. + nullable: true + type: object + trial_period_days: + description: >- + Integer representing the number of trial period days before the + customer is charged for the first time. + nullable: true + type: integer + title: QuotesResourceSubscriptionDataSubscriptionData + type: object + x-expandableFields: [] + quotes_resource_total_details: + description: '' + properties: + amount_discount: + description: This is the sum of all the discounts. + type: integer + amount_shipping: + description: This is the sum of all the shipping amounts. + nullable: true + type: integer + amount_tax: + description: This is the sum of all the tax amounts. + type: integer + breakdown: + $ref: >- + #/components/schemas/quotes_resource_total_details_resource_breakdown + required: + - amount_discount + - amount_tax + title: QuotesResourceTotalDetails + type: object + x-expandableFields: + - breakdown + quotes_resource_total_details_resource_breakdown: + description: '' + properties: + discounts: + description: The aggregated discounts. + items: + $ref: '#/components/schemas/line_items_discount_amount' + type: array + taxes: + description: The aggregated tax amounts by rate. + items: + $ref: '#/components/schemas/line_items_tax_amount' + type: array + required: + - discounts + - taxes + title: QuotesResourceTotalDetailsResourceBreakdown + type: object + x-expandableFields: + - discounts + - taxes + quotes_resource_transfer_data: + description: '' + properties: + amount: + description: >- + The amount in cents (or local equivalent) that will be transferred + to the destination account when the invoice is paid. By default, the + entire amount is transferred to the destination. + nullable: true + type: integer + amount_percent: + description: >- + A non-negative decimal between 0 and 100, with at most two decimal + places. This represents the percentage of the subscription invoice + total that will be transferred to the destination account. By + default, the entire amount will be transferred to the destination. + nullable: true + type: number + destination: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account where funds from the payment will be transferred to upon + payment success. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + required: + - destination + title: QuotesResourceTransferData + type: object + x-expandableFields: + - destination + quotes_resource_upfront: + description: '' + properties: + amount_subtotal: + description: Total before any discounts or taxes are applied. + type: integer + amount_total: + description: Total after discounts and taxes are applied. + type: integer + line_items: + description: >- + The line items that will appear on the next invoice after this quote + is accepted. This does not include pending invoice items that exist + on the customer but may still be included in the next invoice. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: QuotesResourceListLineItems + type: object + x-expandableFields: + - data + total_details: + $ref: '#/components/schemas/quotes_resource_total_details' + required: + - amount_subtotal + - amount_total + - total_details + title: QuotesResourceUpfront + type: object + x-expandableFields: + - line_items + - total_details + radar.early_fraud_warning: + description: >- + An early fraud warning indicates that the card issuer has notified us + that a + + charge may be fraudulent. + + + Related guide: [Early fraud + warnings](https://stripe.com/docs/disputes/measuring#early-fraud-warnings) + properties: + actionable: + description: >- + An EFW is actionable if it has not received a dispute and has not + been fully refunded. You may wish to proactively refund a charge + that receives an EFW, in order to avoid receiving a dispute later. + type: boolean + charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: >- + ID of the charge this early fraud warning is for, optionally + expanded. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + fraud_type: + description: >- + The type of fraud labelled by the issuer. One of + `card_never_received`, `fraudulent_card_application`, + `made_with_counterfeit_card`, `made_with_lost_card`, + `made_with_stolen_card`, `misc`, `unauthorized_use_of_card`. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - radar.early_fraud_warning + type: string + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: >- + ID of the Payment Intent this early fraud warning is for, optionally + expanded. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + required: + - actionable + - charge + - created + - fraud_type + - id + - livemode + - object + title: RadarEarlyFraudWarning + type: object + x-expandableFields: + - charge + - payment_intent + x-resourceId: radar.early_fraud_warning + radar.value_list: + description: >- + Value lists allow you to group values together which can then be + referenced in rules. + + + Related guide: [Default Stripe + lists](https://stripe.com/docs/radar/lists#managing-list-items) + properties: + alias: + description: The name of the value list for use in rules. + maxLength: 5000 + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + created_by: + description: The name or email address of the user who created this value list. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + item_type: + description: >- + The type of items in the value list. One of `card_fingerprint`, + `us_bank_account_fingerprint`, `sepa_debit_fingerprint`, `card_bin`, + `email`, `ip_address`, `country`, `string`, `case_sensitive_string`, + or `customer_id`. + enum: + - card_bin + - card_fingerprint + - case_sensitive_string + - country + - customer_id + - email + - ip_address + - sepa_debit_fingerprint + - string + - us_bank_account_fingerprint + type: string + list_items: + description: List of items contained within this value list. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/radar.value_list_item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: RadarListListItemList + type: object + x-expandableFields: + - data + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + name: + description: The name of the value list. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - radar.value_list + type: string + required: + - alias + - created + - created_by + - id + - item_type + - list_items + - livemode + - metadata + - name + - object + title: RadarListList + type: object + x-expandableFields: + - list_items + x-resourceId: radar.value_list + radar.value_list_item: + description: >- + Value list items allow you to add specific values to a given Radar value + list, which can then be used in rules. + + + Related guide: [Managing list + items](https://stripe.com/docs/radar/lists#managing-list-items) + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + created_by: + description: >- + The name or email address of the user who added this item to the + value list. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - radar.value_list_item + type: string + value: + description: The value of the item. + maxLength: 5000 + type: string + value_list: + description: The identifier of the value list this item belongs to. + maxLength: 5000 + type: string + required: + - created + - created_by + - id + - livemode + - object + - value + - value_list + title: RadarListListItem + type: object + x-expandableFields: [] + x-resourceId: radar.value_list_item + radar_radar_options: + description: >- + Options to configure Radar. See [Radar + Session](https://stripe.com/docs/radar/radar-session) for more + information. + properties: + session: + description: >- + A [Radar Session](https://stripe.com/docs/radar/radar-session) is a + snapshot of the browser metadata and device details that help Radar + make more accurate predictions on your payments. + maxLength: 5000 + type: string + title: RadarRadarOptions + type: object + x-expandableFields: [] + radar_review_resource_location: + description: '' + properties: + city: + description: The city where the payment originated. + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter ISO code representing the country where the payment + originated. + maxLength: 5000 + nullable: true + type: string + latitude: + description: The geographic latitude where the payment originated. + nullable: true + type: number + longitude: + description: The geographic longitude where the payment originated. + nullable: true + type: number + region: + description: The state/county/province/region where the payment originated. + maxLength: 5000 + nullable: true + type: string + title: RadarReviewResourceLocation + type: object + x-expandableFields: [] + radar_review_resource_session: + description: '' + properties: + browser: + description: 'The browser used in this browser session (e.g., `Chrome`).' + maxLength: 5000 + nullable: true + type: string + device: + description: >- + Information about the device used for the browser session (e.g., + `Samsung SM-G930T`). + maxLength: 5000 + nullable: true + type: string + platform: + description: 'The platform for the browser session (e.g., `Macintosh`).' + maxLength: 5000 + nullable: true + type: string + version: + description: 'The version for the browser session (e.g., `61.0.3163.100`).' + maxLength: 5000 + nullable: true + type: string + title: RadarReviewResourceSession + type: object + x-expandableFields: [] + received_payment_method_details_financial_account: + description: '' + properties: + id: + description: The FinancialAccount ID. + maxLength: 5000 + type: string + network: + description: >- + The rails the ReceivedCredit was sent over. A FinancialAccount can + only send funds over `stripe`. + enum: + - stripe + type: string + required: + - id + - network + title: received_payment_method_details_financial_account + type: object + x-expandableFields: [] + recurring: + description: '' + properties: + interval: + description: >- + The frequency at which a subscription is billed. One of `day`, + `week`, `month` or `year`. + enum: + - day + - month + - week + - year + type: string + interval_count: + description: >- + The number of intervals (specified in the `interval` attribute) + between subscription billings. For example, `interval=month` and + `interval_count=3` bills every 3 months. + type: integer + meter: + description: The meter tracking the usage of a metered price + maxLength: 5000 + nullable: true + type: string + usage_type: + description: >- + Configures how the quantity per period should be determined. Can be + either `metered` or `licensed`. `licensed` automatically bills the + `quantity` set when adding it to a subscription. `metered` + aggregates the total usage based on usage records. Defaults to + `licensed`. + enum: + - licensed + - metered + type: string + required: + - interval + - interval_count + - usage_type + title: Recurring + type: object + x-expandableFields: [] + refund: + description: >- + Refund objects allow you to refund a previously created charge that + isn't + + refunded yet. Funds are refunded to the credit or debit card that's + + initially charged. + + + Related guide: [Refunds](https://stripe.com/docs/refunds) + properties: + amount: + description: 'Amount, in cents (or local equivalent).' + type: integer + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + Balance transaction that describes the impact on your account + balance. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: ID of the charge that's refunded. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. You can use this for + displaying to users (available on non-card refunds only). + maxLength: 5000 + type: string + destination_details: + $ref: '#/components/schemas/refund_destination_details' + failure_balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + After the refund fails, this balance transaction describes the + adjustment made on your account balance that reverses the initial + balance transaction. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + failure_reason: + description: >- + Provides the reason for the refund failure. Possible values are: + `lost_or_stolen_card`, `expired_or_canceled_card`, + `charge_for_pending_refund_disputed`, `insufficient_funds`, + `declined`, `merchant_request`, or `unknown`. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + instructions_email: + description: >- + For payment methods without native refund support (for example, + Konbini, PromptPay), provide an email address for the customer to + receive refund instructions. + maxLength: 5000 + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + next_action: + $ref: '#/components/schemas/refund_next_action' + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - refund + type: string + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: ID of the PaymentIntent that's refunded. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + pending_reason: + description: >- + Provides the reason for why the refund is pending. Possible values + are: `processing`, `insufficient_funds`, or `charge_pending`. + enum: + - charge_pending + - insufficient_funds + - processing + type: string + presentment_details: + $ref: >- + #/components/schemas/payment_flows_payment_intent_presentment_details + reason: + description: >- + Reason for the refund, which is either user-provided (`duplicate`, + `fraudulent`, or `requested_by_customer`) or generated by Stripe + internally (`expired_uncaptured_charge`). + enum: + - duplicate + - expired_uncaptured_charge + - fraudulent + - requested_by_customer + nullable: true + type: string + x-stripeBypassValidation: true + receipt_number: + description: >- + This is the transaction number that appears on email receipts sent + for this refund. + maxLength: 5000 + nullable: true + type: string + source_transfer_reversal: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/transfer_reversal' + description: >- + The transfer reversal that's associated with the refund. Only + present if the charge came from another Stripe account. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/transfer_reversal' + status: + description: >- + Status of the refund. This can be `pending`, `requires_action`, + `succeeded`, `failed`, or `canceled`. Learn more about [failed + refunds](https://stripe.com/docs/refunds#failed-refunds). + maxLength: 5000 + nullable: true + type: string + transfer_reversal: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/transfer_reversal' + description: >- + This refers to the transfer reversal object if the accompanying + transfer reverses. This is only applicable if the charge was created + using the destination parameter. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/transfer_reversal' + required: + - amount + - created + - currency + - id + - object + title: Refund + type: object + x-expandableFields: + - balance_transaction + - charge + - destination_details + - failure_balance_transaction + - next_action + - payment_intent + - presentment_details + - source_transfer_reversal + - transfer_reversal + x-resourceId: refund + refund_destination_details: + description: '' + properties: + affirm: + $ref: '#/components/schemas/destination_details_unimplemented' + afterpay_clearpay: + $ref: '#/components/schemas/destination_details_unimplemented' + alipay: + $ref: '#/components/schemas/destination_details_unimplemented' + alma: + $ref: '#/components/schemas/destination_details_unimplemented' + amazon_pay: + $ref: '#/components/schemas/destination_details_unimplemented' + au_bank_transfer: + $ref: '#/components/schemas/destination_details_unimplemented' + blik: + $ref: '#/components/schemas/refund_destination_details_blik' + br_bank_transfer: + $ref: '#/components/schemas/refund_destination_details_br_bank_transfer' + card: + $ref: '#/components/schemas/refund_destination_details_card' + cashapp: + $ref: '#/components/schemas/destination_details_unimplemented' + customer_cash_balance: + $ref: '#/components/schemas/destination_details_unimplemented' + eps: + $ref: '#/components/schemas/destination_details_unimplemented' + eu_bank_transfer: + $ref: '#/components/schemas/refund_destination_details_eu_bank_transfer' + gb_bank_transfer: + $ref: '#/components/schemas/refund_destination_details_gb_bank_transfer' + giropay: + $ref: '#/components/schemas/destination_details_unimplemented' + grabpay: + $ref: '#/components/schemas/destination_details_unimplemented' + jp_bank_transfer: + $ref: '#/components/schemas/refund_destination_details_jp_bank_transfer' + klarna: + $ref: '#/components/schemas/destination_details_unimplemented' + multibanco: + $ref: '#/components/schemas/refund_destination_details_multibanco' + mx_bank_transfer: + $ref: '#/components/schemas/refund_destination_details_mx_bank_transfer' + nz_bank_transfer: + $ref: '#/components/schemas/destination_details_unimplemented' + p24: + $ref: '#/components/schemas/refund_destination_details_p24' + paynow: + $ref: '#/components/schemas/destination_details_unimplemented' + paypal: + $ref: '#/components/schemas/refund_destination_details_paypal' + pix: + $ref: '#/components/schemas/destination_details_unimplemented' + revolut: + $ref: '#/components/schemas/destination_details_unimplemented' + sofort: + $ref: '#/components/schemas/destination_details_unimplemented' + swish: + $ref: '#/components/schemas/refund_destination_details_swish' + th_bank_transfer: + $ref: '#/components/schemas/refund_destination_details_th_bank_transfer' + type: + description: >- + The type of transaction-specific details of the payment method used + in the refund (e.g., `card`). An additional hash is included on + `destination_details` with a name matching this value. It contains + information specific to the refund transaction. + maxLength: 5000 + type: string + us_bank_transfer: + $ref: '#/components/schemas/refund_destination_details_us_bank_transfer' + wechat_pay: + $ref: '#/components/schemas/destination_details_unimplemented' + zip: + $ref: '#/components/schemas/destination_details_unimplemented' + required: + - type + title: refund_destination_details + type: object + x-expandableFields: + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - au_bank_transfer + - blik + - br_bank_transfer + - card + - cashapp + - customer_cash_balance + - eps + - eu_bank_transfer + - gb_bank_transfer + - giropay + - grabpay + - jp_bank_transfer + - klarna + - multibanco + - mx_bank_transfer + - nz_bank_transfer + - p24 + - paynow + - paypal + - pix + - revolut + - sofort + - swish + - th_bank_transfer + - us_bank_transfer + - wechat_pay + - zip + refund_destination_details_blik: + description: '' + properties: + network_decline_code: + description: >- + For refunds declined by the network, a decline code provided by the + network which indicates the reason the refund failed. + maxLength: 5000 + nullable: true + type: string + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_blik + type: object + x-expandableFields: [] + refund_destination_details_br_bank_transfer: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_br_bank_transfer + type: object + x-expandableFields: [] + refund_destination_details_card: + description: '' + properties: + reference: + description: Value of the reference number assigned to the refund. + maxLength: 5000 + type: string + reference_status: + description: >- + Status of the reference number on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + type: string + reference_type: + description: Type of the reference number assigned to the refund. + maxLength: 5000 + type: string + type: + description: 'The type of refund. This can be `refund`, `reversal`, or `pending`.' + enum: + - pending + - refund + - reversal + type: string + required: + - type + title: refund_destination_details_card + type: object + x-expandableFields: [] + refund_destination_details_eu_bank_transfer: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_eu_bank_transfer + type: object + x-expandableFields: [] + refund_destination_details_gb_bank_transfer: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_gb_bank_transfer + type: object + x-expandableFields: [] + refund_destination_details_jp_bank_transfer: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_jp_bank_transfer + type: object + x-expandableFields: [] + refund_destination_details_multibanco: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_multibanco + type: object + x-expandableFields: [] + refund_destination_details_mx_bank_transfer: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_mx_bank_transfer + type: object + x-expandableFields: [] + refund_destination_details_p24: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_p24 + type: object + x-expandableFields: [] + refund_destination_details_paypal: + description: '' + properties: + network_decline_code: + description: >- + For refunds declined by the network, a decline code provided by the + network which indicates the reason the refund failed. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_paypal + type: object + x-expandableFields: [] + refund_destination_details_swish: + description: '' + properties: + network_decline_code: + description: >- + For refunds declined by the network, a decline code provided by the + network which indicates the reason the refund failed. + maxLength: 5000 + nullable: true + type: string + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_swish + type: object + x-expandableFields: [] + refund_destination_details_th_bank_transfer: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_th_bank_transfer + type: object + x-expandableFields: [] + refund_destination_details_us_bank_transfer: + description: '' + properties: + reference: + description: The reference assigned to the refund. + maxLength: 5000 + nullable: true + type: string + reference_status: + description: >- + Status of the reference on the refund. This can be `pending`, + `available` or `unavailable`. + maxLength: 5000 + nullable: true + type: string + title: refund_destination_details_us_bank_transfer + type: object + x-expandableFields: [] + refund_next_action: + description: '' + properties: + display_details: + $ref: '#/components/schemas/refund_next_action_display_details' + type: + description: Type of the next action to perform. + maxLength: 5000 + type: string + required: + - type + title: RefundNextAction + type: object + x-expandableFields: + - display_details + refund_next_action_display_details: + description: '' + properties: + email_sent: + $ref: '#/components/schemas/email_sent' + expires_at: + description: The expiry timestamp. + format: unix-time + type: integer + required: + - email_sent + - expires_at + title: RefundNextActionDisplayDetails + type: object + x-expandableFields: + - email_sent + reporting.report_run: + description: >- + The Report Run object represents an instance of a report type generated + with + + specific run parameters. Once the object is created, Stripe begins + processing the report. + + When the report has finished running, it will give you a reference to a + file + + where you can retrieve your results. For an overview, see + + [API Access to + Reports](https://stripe.com/docs/reporting/statements/api). + + + Note that certain report types can only be run based on your live-mode + data (not test-mode + + data), and will error when queried without a [live-mode API + key](https://stripe.com/docs/keys#test-live-modes). + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + error: + description: >- + If something should go wrong during the run, a message about the + failure (populated when + `status=failed`). + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + `true` if the report is run on live mode data and `false` if it is + run on test mode data. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - reporting.report_run + type: string + parameters: + $ref: >- + #/components/schemas/financial_reporting_finance_report_run_run_parameters + report_type: + description: >- + The ID of the [report + type](https://stripe.com/docs/reports/report-types) to run, such as + `"balance.summary.1"`. + maxLength: 5000 + type: string + result: + anyOf: + - $ref: '#/components/schemas/file' + description: >- + The file object representing the result of the report run (populated + when + `status=succeeded`). + nullable: true + status: + description: >- + Status of this report run. This will be `pending` when the run is + initially created. + When the run finishes, this will be set to `succeeded` and the `result` field will be populated. + Rarely, we may encounter an error, at which point this will be set to `failed` and the `error` field will be populated. + maxLength: 5000 + type: string + succeeded_at: + description: |- + Timestamp at which this run successfully finished (populated when + `status=succeeded`). Measured in seconds since the Unix epoch. + format: unix-time + nullable: true + type: integer + required: + - created + - id + - livemode + - object + - parameters + - report_type + - status + title: reporting_report_run + type: object + x-expandableFields: + - parameters + - result + x-resourceId: reporting.report_run + reporting.report_type: + description: >- + The Report Type resource corresponds to a particular type of report, + such as + + the "Activity summary" or "Itemized payouts" reports. These objects are + + identified by an ID belonging to a set of enumerated values. See + + [API Access to Reports + documentation](https://stripe.com/docs/reporting/statements/api) + + for those Report Type IDs, along with required and optional parameters. + + + Note that certain report types can only be run based on your live-mode + data (not test-mode + + data), and will error when queried without a [live-mode API + key](https://stripe.com/docs/keys#test-live-modes). + properties: + data_available_end: + description: >- + Most recent time for which this Report Type is available. Measured + in seconds since the Unix epoch. + format: unix-time + type: integer + data_available_start: + description: >- + Earliest time for which this Report Type is available. Measured in + seconds since the Unix epoch. + format: unix-time + type: integer + default_columns: + description: >- + List of column names that are included by default when this Report + Type gets run. (If the Report Type doesn't support the `columns` + parameter, this will be null.) + items: + maxLength: 5000 + type: string + nullable: true + type: array + id: + description: >- + The [ID of the Report + Type](https://stripe.com/docs/reporting/statements/api#available-report-types), + such as `balance.summary.1`. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + name: + description: Human-readable name of the Report Type + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - reporting.report_type + type: string + updated: + description: >- + When this Report Type was latest updated. Measured in seconds since + the Unix epoch. + format: unix-time + type: integer + version: + description: >- + Version of the Report Type. Different versions report with the same + ID will have the same purpose, but may take different run parameters + or have different result schemas. + type: integer + required: + - data_available_end + - data_available_start + - id + - livemode + - name + - object + - updated + - version + title: reporting_report_type + type: object + x-expandableFields: [] + x-resourceId: reporting.report_type + reserve_transaction: + description: '' + properties: + amount: + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - reserve_transaction + type: string + required: + - amount + - currency + - id + - object + title: ReserveTransaction + type: object + x-expandableFields: [] + review: + description: >- + Reviews can be used to supplement automated fraud detection with human + expertise. + + + Learn more about [Radar](/radar) and reviewing payments + + [here](https://stripe.com/docs/radar/reviews). + properties: + billing_zip: + description: 'The ZIP or postal code of the card used, if applicable.' + maxLength: 5000 + nullable: true + type: string + charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: The charge associated with this review. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + closed_reason: + description: >- + The reason the review was closed, or null if it has not yet been + closed. One of `approved`, `refunded`, `refunded_as_fraud`, + `disputed`, `redacted`, or `canceled`. + enum: + - approved + - canceled + - disputed + - redacted + - refunded + - refunded_as_fraud + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + ip_address: + description: The IP address where the payment originated. + maxLength: 5000 + nullable: true + type: string + ip_address_location: + anyOf: + - $ref: '#/components/schemas/radar_review_resource_location' + description: >- + Information related to the location of the payment. Note that this + information is an approximation and attempts to locate the nearest + population center - it should not be used to determine a specific + address. + nullable: true + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - review + type: string + open: + description: 'If `true`, the review needs action.' + type: boolean + opened_reason: + description: The reason the review was opened. One of `rule` or `manual`. + enum: + - manual + - rule + type: string + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: 'The PaymentIntent ID associated with this review, if one exists.' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + reason: + description: >- + The reason the review is currently open or closed. One of `rule`, + `manual`, `approved`, `refunded`, `refunded_as_fraud`, `disputed`, + `redacted`, or `canceled`. + maxLength: 5000 + type: string + session: + anyOf: + - $ref: '#/components/schemas/radar_review_resource_session' + description: >- + Information related to the browsing session of the user who + initiated the payment. + nullable: true + required: + - created + - id + - livemode + - object + - open + - opened_reason + - reason + title: RadarReview + type: object + x-expandableFields: + - charge + - ip_address_location + - payment_intent + - session + x-resourceId: review + revolut_pay_underlying_payment_method_funding_details: + description: '' + properties: + card: + $ref: '#/components/schemas/payment_method_details_passthrough_card' + type: + description: funding type of the underlying payment method. + enum: + - card + nullable: true + type: string + title: revolut_pay_underlying_payment_method_funding_details + type: object + x-expandableFields: + - card + rule: + description: '' + properties: + action: + description: The action taken on the payment. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + predicate: + description: The predicate to evaluate the payment against. + maxLength: 5000 + type: string + required: + - action + - id + - predicate + title: RadarRule + type: object + x-expandableFields: [] + scheduled_query_run: + description: >- + If you have [scheduled a Sigma + query](https://stripe.com/docs/sigma/scheduled-queries), you'll + + receive a `sigma.scheduled_query_run.created` webhook each time the + query + + runs. The webhook contains a `ScheduledQueryRun` object, which you can + use to + + retrieve the query results. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + data_load_time: + description: >- + When the query was run, Sigma contained a snapshot of your Stripe + data at this time. + format: unix-time + type: integer + error: + $ref: '#/components/schemas/sigma_scheduled_query_run_error' + file: + anyOf: + - $ref: '#/components/schemas/file' + description: The file object representing the results of the query. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - scheduled_query_run + type: string + result_available_until: + description: >- + Time at which the result expires and is no longer available for + download. + format: unix-time + type: integer + sql: + description: SQL for the query. + maxLength: 100000 + type: string + status: + description: >- + The query's execution status, which will be `completed` for + successful runs, and `canceled`, `failed`, or `timed_out` otherwise. + maxLength: 5000 + type: string + title: + description: Title of the query. + maxLength: 5000 + type: string + required: + - created + - data_load_time + - id + - livemode + - object + - result_available_until + - sql + - status + - title + title: ScheduledQueryRun + type: object + x-expandableFields: + - error + - file + x-resourceId: scheduled_query_run + schedules_phase_automatic_tax: + description: '' + properties: + disabled_reason: + description: 'If Stripe disabled automatic tax, this enum describes why.' + enum: + - requires_location_inputs + nullable: true + type: string + enabled: + description: >- + Whether Stripe automatically computes tax on invoices created during + this phase. + type: boolean + liability: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The account that's liable for tax. If set, the business address and + tax registrations required to perform the tax calculation are loaded + from this account. The tax transaction is returned in the report of + the connected account. + nullable: true + required: + - enabled + title: SchedulesPhaseAutomaticTax + type: object + x-expandableFields: + - liability + secret_service_resource_scope: + description: '' + properties: + type: + description: The secret scope type. + enum: + - account + - user + type: string + user: + description: 'The user ID, if type is set to "user"' + maxLength: 5000 + type: string + required: + - type + title: SecretServiceResourceScope + type: object + x-expandableFields: [] + sepa_debit_generated_from: + description: '' + properties: + charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: 'The ID of the Charge that generated this PaymentMethod, if any.' + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + setup_attempt: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/setup_attempt' + description: >- + The ID of the SetupAttempt that generated this PaymentMethod, if + any. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/setup_attempt' + title: sepa_debit_generated_from + type: object + x-expandableFields: + - charge + - setup_attempt + setup_attempt: + description: |- + A SetupAttempt describes one attempted confirmation of a SetupIntent, + whether that confirmation is successful or unsuccessful. You can use + SetupAttempts to inspect details of a specific attempt at setting up a + payment method using a SetupIntent. + properties: + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + description: >- + The value of + [application](https://stripe.com/docs/api/setup_intents/object#setup_intent_object-application) + on the SetupIntent at the time of this confirmation. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + attach_to_self: + description: >- + If present, the SetupIntent's payment method will be attached to the + in-context Stripe Account. + + + It can only be used for this Stripe Account’s own money movement + flows like InboundTransfer and OutboundTransfers. It cannot be set + to true when setting up a PaymentMethod for a Customer, and defaults + to false when attaching a PaymentMethod to a Customer. + type: boolean + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: >- + The value of + [customer](https://stripe.com/docs/api/setup_intents/object#setup_intent_object-customer) + on the SetupIntent at the time of this confirmation. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + flow_directions: + description: >- + Indicates the directions of money movement for which this payment + method is intended to be used. + + + Include `inbound` if you intend to use the payment method as the + origin to pull funds from. Include `outbound` if you intend to use + the payment method as the destination to send funds to. You can + include both if you intend to use the payment method for both + purposes. + items: + enum: + - inbound + - outbound + type: string + nullable: true + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - setup_attempt + type: string + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The value of + [on_behalf_of](https://stripe.com/docs/api/setup_intents/object#setup_intent_object-on_behalf_of) + on the SetupIntent at the time of this confirmation. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: ID of the payment method used with this SetupAttempt. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + payment_method_details: + $ref: '#/components/schemas/setup_attempt_payment_method_details' + setup_error: + anyOf: + - $ref: '#/components/schemas/api_errors' + description: >- + The error encountered during this attempt to confirm the + SetupIntent, if any. + nullable: true + setup_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/setup_intent' + description: ID of the SetupIntent that this attempt belongs to. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/setup_intent' + status: + description: >- + Status of this SetupAttempt, one of `requires_confirmation`, + `requires_action`, `processing`, `succeeded`, `failed`, or + `abandoned`. + maxLength: 5000 + type: string + usage: + description: >- + The value of + [usage](https://stripe.com/docs/api/setup_intents/object#setup_intent_object-usage) + on the SetupIntent at the time of this confirmation, one of + `off_session` or `on_session`. + maxLength: 5000 + type: string + required: + - created + - id + - livemode + - object + - payment_method + - payment_method_details + - setup_intent + - status + - usage + title: PaymentFlowsSetupIntentSetupAttempt + type: object + x-expandableFields: + - application + - customer + - on_behalf_of + - payment_method + - payment_method_details + - setup_error + - setup_intent + x-resourceId: setup_attempt + setup_attempt_payment_method_details: + description: '' + properties: + acss_debit: + $ref: '#/components/schemas/setup_attempt_payment_method_details_acss_debit' + amazon_pay: + $ref: '#/components/schemas/setup_attempt_payment_method_details_amazon_pay' + au_becs_debit: + $ref: >- + #/components/schemas/setup_attempt_payment_method_details_au_becs_debit + bacs_debit: + $ref: '#/components/schemas/setup_attempt_payment_method_details_bacs_debit' + bancontact: + $ref: '#/components/schemas/setup_attempt_payment_method_details_bancontact' + boleto: + $ref: '#/components/schemas/setup_attempt_payment_method_details_boleto' + card: + $ref: '#/components/schemas/setup_attempt_payment_method_details_card' + card_present: + $ref: >- + #/components/schemas/setup_attempt_payment_method_details_card_present + cashapp: + $ref: '#/components/schemas/setup_attempt_payment_method_details_cashapp' + ideal: + $ref: '#/components/schemas/setup_attempt_payment_method_details_ideal' + kakao_pay: + $ref: '#/components/schemas/setup_attempt_payment_method_details_kakao_pay' + klarna: + $ref: '#/components/schemas/setup_attempt_payment_method_details_klarna' + kr_card: + $ref: '#/components/schemas/setup_attempt_payment_method_details_kr_card' + link: + $ref: '#/components/schemas/setup_attempt_payment_method_details_link' + naver_pay: + $ref: '#/components/schemas/setup_attempt_payment_method_details_naver_pay' + nz_bank_account: + $ref: >- + #/components/schemas/setup_attempt_payment_method_details_nz_bank_account + paypal: + $ref: '#/components/schemas/setup_attempt_payment_method_details_paypal' + revolut_pay: + $ref: >- + #/components/schemas/setup_attempt_payment_method_details_revolut_pay + sepa_debit: + $ref: '#/components/schemas/setup_attempt_payment_method_details_sepa_debit' + sofort: + $ref: '#/components/schemas/setup_attempt_payment_method_details_sofort' + type: + description: >- + The type of the payment method used in the SetupIntent (e.g., + `card`). An additional hash is included on `payment_method_details` + with a name matching this value. It contains confirmation-specific + information for the payment method. + maxLength: 5000 + type: string + us_bank_account: + $ref: >- + #/components/schemas/setup_attempt_payment_method_details_us_bank_account + required: + - type + title: SetupAttemptPaymentMethodDetails + type: object + x-expandableFields: + - acss_debit + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - boleto + - card + - card_present + - cashapp + - ideal + - kakao_pay + - klarna + - kr_card + - link + - naver_pay + - nz_bank_account + - paypal + - revolut_pay + - sepa_debit + - sofort + - us_bank_account + setup_attempt_payment_method_details_acss_debit: + description: '' + properties: {} + title: setup_attempt_payment_method_details_acss_debit + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_amazon_pay: + description: '' + properties: {} + title: setup_attempt_payment_method_details_amazon_pay + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_au_becs_debit: + description: '' + properties: {} + title: setup_attempt_payment_method_details_au_becs_debit + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_bacs_debit: + description: '' + properties: {} + title: setup_attempt_payment_method_details_bacs_debit + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_bancontact: + description: '' + properties: + bank_code: + description: Bank code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bic: + description: Bank Identifier Code of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + generated_sepa_debit: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + The ID of the SEPA Direct Debit PaymentMethod which was generated by + this SetupAttempt. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + generated_sepa_debit_mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: >- + The mandate for the SEPA Direct Debit PaymentMethod which was + generated by this SetupAttempt. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + iban_last4: + description: Last four characters of the IBAN. + maxLength: 5000 + nullable: true + type: string + preferred_language: + description: >- + Preferred language of the Bancontact authorization page that the + customer is redirected to. + + Can be one of `en`, `de`, `fr`, or `nl` + enum: + - de + - en + - fr + - nl + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by + Bancontact directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + title: setup_attempt_payment_method_details_bancontact + type: object + x-expandableFields: + - generated_sepa_debit + - generated_sepa_debit_mandate + setup_attempt_payment_method_details_boleto: + description: '' + properties: {} + title: setup_attempt_payment_method_details_boleto + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_card: + description: '' + properties: + brand: + description: >- + Card brand. Can be `amex`, `diners`, `discover`, `eftpos_au`, `jcb`, + `link`, `mastercard`, `unionpay`, `visa`, or `unknown`. + maxLength: 5000 + nullable: true + type: string + checks: + anyOf: + - $ref: >- + #/components/schemas/setup_attempt_payment_method_details_card_checks + description: >- + Check results by Card networks on Card address and CVC at the time + of authorization + nullable: true + country: + description: >- + Two-letter ISO code representing the country of the card. You could + use this attribute to get a sense of the international breakdown of + cards you've collected. + maxLength: 5000 + nullable: true + type: string + exp_month: + description: Two-digit number representing the card's expiration month. + nullable: true + type: integer + exp_year: + description: Four-digit number representing the card's expiration year. + nullable: true + type: integer + fingerprint: + description: >- + Uniquely identifies this particular card number. You can use this + attribute to check whether two customers who’ve signed up with you + are using the same card number, for example. For payment methods + that tokenize card information (Apple Pay, Google Pay), the + tokenized number might be provided instead of the underlying card + number. + + + *As of May 1, 2021, card fingerprint in India for Connect changed to + allow two fingerprints for the same card---one for India and one for + the rest of the world.* + maxLength: 5000 + nullable: true + type: string + funding: + description: >- + Card funding type. Can be `credit`, `debit`, `prepaid`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + last4: + description: The last four digits of the card. + maxLength: 5000 + nullable: true + type: string + network: + description: >- + Identifies which network this charge was processed on. Can be + `amex`, `cartes_bancaires`, `diners`, `discover`, `eftpos_au`, + `interac`, `jcb`, `link`, `mastercard`, `unionpay`, `visa`, or + `unknown`. + maxLength: 5000 + nullable: true + type: string + three_d_secure: + anyOf: + - $ref: '#/components/schemas/three_d_secure_details' + description: Populated if this authorization used 3D Secure authentication. + nullable: true + wallet: + anyOf: + - $ref: >- + #/components/schemas/setup_attempt_payment_method_details_card_wallet + description: >- + If this Card is part of a card wallet, this contains the details of + the card wallet. + nullable: true + title: setup_attempt_payment_method_details_card + type: object + x-expandableFields: + - checks + - three_d_secure + - wallet + setup_attempt_payment_method_details_card_checks: + description: '' + properties: + address_line1_check: + description: >- + If a address line1 was provided, results of the check, one of + `pass`, `fail`, `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + address_postal_code_check: + description: >- + If a address postal code was provided, results of the check, one of + `pass`, `fail`, `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + cvc_check: + description: >- + If a CVC was provided, results of the check, one of `pass`, `fail`, + `unavailable`, or `unchecked`. + maxLength: 5000 + nullable: true + type: string + title: setup_attempt_payment_method_details_card_checks + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_card_present: + description: '' + properties: + generated_card: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + The ID of the Card PaymentMethod which was generated by this + SetupAttempt. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + offline: + anyOf: + - $ref: '#/components/schemas/payment_method_details_card_present_offline' + description: Details about payments collected offline. + nullable: true + title: setup_attempt_payment_method_details_card_present + type: object + x-expandableFields: + - generated_card + - offline + setup_attempt_payment_method_details_card_wallet: + description: '' + properties: + apple_pay: + $ref: '#/components/schemas/payment_method_details_card_wallet_apple_pay' + google_pay: + $ref: '#/components/schemas/payment_method_details_card_wallet_google_pay' + type: + description: >- + The type of the card wallet, one of `apple_pay`, `google_pay`, or + `link`. An additional hash is included on the Wallet subhash with a + name matching this value. It contains additional information + specific to the card wallet type. + enum: + - apple_pay + - google_pay + - link + type: string + required: + - type + title: setup_attempt_payment_method_details_card_wallet + type: object + x-expandableFields: + - apple_pay + - google_pay + setup_attempt_payment_method_details_cashapp: + description: '' + properties: {} + title: setup_attempt_payment_method_details_cashapp + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_ideal: + description: '' + properties: + bank: + description: >- + The customer's bank. Can be one of `abn_amro`, `asn_bank`, `bunq`, + `handelsbanken`, `ing`, `knab`, `moneyou`, `n26`, `nn`, `rabobank`, + `regiobank`, `revolut`, `sns_bank`, `triodos_bank`, `van_lanschot`, + or `yoursafe`. + enum: + - abn_amro + - asn_bank + - bunq + - handelsbanken + - ing + - knab + - moneyou + - n26 + - nn + - rabobank + - regiobank + - revolut + - sns_bank + - triodos_bank + - van_lanschot + - yoursafe + nullable: true + type: string + bic: + description: The Bank Identifier Code of the customer's bank. + enum: + - ABNANL2A + - ASNBNL21 + - BITSNL2A + - BUNQNL2A + - FVLBNL22 + - HANDNL2A + - INGBNL2A + - KNABNL2H + - MOYONL21 + - NNBANL2G + - NTSBDEB1 + - RABONL2U + - RBRBNL21 + - REVOIE23 + - REVOLT21 + - SNSBNL2A + - TRIONL2U + nullable: true + type: string + generated_sepa_debit: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + The ID of the SEPA Direct Debit PaymentMethod which was generated by + this SetupAttempt. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + generated_sepa_debit_mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: >- + The mandate for the SEPA Direct Debit PaymentMethod which was + generated by this SetupAttempt. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + iban_last4: + description: Last four characters of the IBAN. + maxLength: 5000 + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by iDEAL + directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + title: setup_attempt_payment_method_details_ideal + type: object + x-expandableFields: + - generated_sepa_debit + - generated_sepa_debit_mandate + setup_attempt_payment_method_details_kakao_pay: + description: '' + properties: {} + title: setup_attempt_payment_method_details_kakao_pay + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_klarna: + description: '' + properties: {} + title: setup_attempt_payment_method_details_klarna + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_kr_card: + description: '' + properties: {} + title: setup_attempt_payment_method_details_kr_card + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_link: + description: '' + properties: {} + title: setup_attempt_payment_method_details_link + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_naver_pay: + description: '' + properties: + buyer_id: + description: >- + Uniquely identifies this particular Naver Pay account. You can use + this attribute to check whether two Naver Pay accounts are the same. + maxLength: 5000 + type: string + title: setup_attempt_payment_method_details_naver_pay + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_nz_bank_account: + description: '' + properties: {} + title: setup_attempt_payment_method_details_nz_bank_account + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_paypal: + description: '' + properties: {} + title: setup_attempt_payment_method_details_paypal + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_revolut_pay: + description: '' + properties: {} + title: setup_attempt_payment_method_details_revolut_pay + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_sepa_debit: + description: '' + properties: {} + title: setup_attempt_payment_method_details_sepa_debit + type: object + x-expandableFields: [] + setup_attempt_payment_method_details_sofort: + description: '' + properties: + bank_code: + description: Bank code of bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bank_name: + description: Name of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + bic: + description: Bank Identifier Code of the bank associated with the bank account. + maxLength: 5000 + nullable: true + type: string + generated_sepa_debit: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + The ID of the SEPA Direct Debit PaymentMethod which was generated by + this SetupAttempt. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + generated_sepa_debit_mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: >- + The mandate for the SEPA Direct Debit PaymentMethod which was + generated by this SetupAttempt. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + iban_last4: + description: Last four characters of the IBAN. + maxLength: 5000 + nullable: true + type: string + preferred_language: + description: >- + Preferred language of the Sofort authorization page that the + customer is redirected to. + + Can be one of `en`, `de`, `fr`, or `nl` + enum: + - de + - en + - fr + - nl + nullable: true + type: string + verified_name: + description: >- + Owner's verified full name. Values are verified or provided by + Sofort directly + + (if supported) at the time of authorization or settlement. They + cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + title: setup_attempt_payment_method_details_sofort + type: object + x-expandableFields: + - generated_sepa_debit + - generated_sepa_debit_mandate + setup_attempt_payment_method_details_us_bank_account: + description: '' + properties: {} + title: setup_attempt_payment_method_details_us_bank_account + type: object + x-expandableFields: [] + setup_intent: + description: >- + A SetupIntent guides you through the process of setting up and saving a + customer's payment credentials for future payments. + + For example, you can use a SetupIntent to set up and save your + customer's card without immediately collecting a payment. + + Later, you can use + [PaymentIntents](https://stripe.com/docs/api#payment_intents) to drive + the payment flow. + + + Create a SetupIntent when you're ready to collect your customer's + payment credentials. + + Don't maintain long-lived, unconfirmed SetupIntents because they might + not be valid. + + The SetupIntent transitions through multiple + [statuses](https://docs.stripe.com/payments/intents#intent-statuses) as + it guides + + you through the setup process. + + + Successful SetupIntents result in payment credentials that are optimized + for future payments. + + For example, cardholders in [certain + regions](https://stripe.com/guides/strong-customer-authentication) might + need to be run through + + [Strong Customer + Authentication](https://docs.stripe.com/strong-customer-authentication) + during payment method collection + + to streamline later [off-session + payments](https://docs.stripe.com/payments/setup-intents). + + If you use the SetupIntent with a + [Customer](https://stripe.com/docs/api#setup_intent_object-customer), + + it automatically attaches the resulting payment method to that Customer + after successful setup. + + We recommend using SetupIntents or + [setup_future_usage](https://stripe.com/docs/api#payment_intent_object-setup_future_usage) + on + + PaymentIntents to save payment methods to prevent saving invalid or + unoptimized payment methods. + + + By using SetupIntents, you can reduce friction for your customers, even + as regulations change over time. + + + Related guide: [Setup Intents + API](https://docs.stripe.com/payments/setup-intents) + properties: + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + description: ID of the Connect application that created the SetupIntent. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + attach_to_self: + description: >- + If present, the SetupIntent's payment method will be attached to the + in-context Stripe Account. + + + It can only be used for this Stripe Account’s own money movement + flows like InboundTransfer and OutboundTransfers. It cannot be set + to true when setting up a PaymentMethod for a Customer, and defaults + to false when attaching a PaymentMethod to a Customer. + type: boolean + automatic_payment_methods: + anyOf: + - $ref: >- + #/components/schemas/payment_flows_automatic_payment_methods_setup_intent + description: >- + Settings for dynamic payment methods compatible with this Setup + Intent + nullable: true + cancellation_reason: + description: >- + Reason for cancellation of this SetupIntent, one of `abandoned`, + `requested_by_customer`, or `duplicate`. + enum: + - abandoned + - duplicate + - requested_by_customer + nullable: true + type: string + client_secret: + description: >- + The client secret of this SetupIntent. Used for client-side + retrieval using a publishable key. + + + The client secret can be used to complete payment setup from your + frontend. It should not be stored, logged, or exposed to anyone + other than the customer. Make sure that you have TLS enabled on any + page that includes the client secret. + maxLength: 5000 + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: >- + ID of the Customer this SetupIntent belongs to, if one exists. + + + If present, the SetupIntent's payment method will be attached to the + Customer on successful setup. Payment methods attached to other + Customers cannot be used with this SetupIntent. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + flow_directions: + description: >- + Indicates the directions of money movement for which this payment + method is intended to be used. + + + Include `inbound` if you intend to use the payment method as the + origin to pull funds from. Include `outbound` if you intend to use + the payment method as the destination to send funds to. You can + include both if you intend to use the payment method for both + purposes. + items: + enum: + - inbound + - outbound + type: string + nullable: true + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + last_setup_error: + anyOf: + - $ref: '#/components/schemas/api_errors' + description: The error encountered in the previous SetupIntent confirmation. + nullable: true + latest_attempt: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/setup_attempt' + description: The most recent SetupAttempt for this SetupIntent. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/setup_attempt' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: ID of the multi use Mandate generated by the SetupIntent. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + next_action: + anyOf: + - $ref: '#/components/schemas/setup_intent_next_action' + description: >- + If present, this property tells you what actions you need to take in + order for your customer to continue payment setup. + nullable: true + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - setup_intent + type: string + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: The account (if any) for which the setup is intended. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + ID of the payment method used with this SetupIntent. If the payment + method is `card_present` and isn't a digital wallet, then the + [generated_card](https://docs.stripe.com/api/setup_attempts/object#setup_attempt_object-payment_method_details-card_present-generated_card) + associated with the `latest_attempt` is attached to the Customer + instead. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + payment_method_configuration_details: + anyOf: + - $ref: >- + #/components/schemas/payment_method_config_biz_payment_method_configuration_details + description: >- + Information about the [payment method + configuration](https://stripe.com/docs/api/payment_method_configurations) + used for this Setup Intent. + nullable: true + payment_method_options: + anyOf: + - $ref: '#/components/schemas/setup_intent_payment_method_options' + description: Payment method-specific configuration for this SetupIntent. + nullable: true + payment_method_types: + description: >- + The list of payment method types (e.g. card) that this SetupIntent + is allowed to set up. + items: + maxLength: 5000 + type: string + type: array + single_use_mandate: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/mandate' + description: ID of the single_use Mandate generated by the SetupIntent. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/mandate' + status: + description: >- + [Status](https://stripe.com/docs/payments/intents#intent-statuses) + of this SetupIntent, one of `requires_payment_method`, + `requires_confirmation`, `requires_action`, `processing`, + `canceled`, or `succeeded`. + enum: + - canceled + - processing + - requires_action + - requires_confirmation + - requires_payment_method + - succeeded + type: string + usage: + description: >- + Indicates how the payment method is intended to be used in the + future. + + + Use `on_session` if you intend to only reuse the payment method when + the customer is in your checkout flow. Use `off_session` if your + customer may or may not be in your checkout flow. If not provided, + this value defaults to `off_session`. + maxLength: 5000 + type: string + required: + - created + - id + - livemode + - object + - payment_method_types + - status + - usage + title: SetupIntent + type: object + x-expandableFields: + - application + - automatic_payment_methods + - customer + - last_setup_error + - latest_attempt + - mandate + - next_action + - on_behalf_of + - payment_method + - payment_method_configuration_details + - payment_method_options + - single_use_mandate + x-resourceId: setup_intent + setup_intent_next_action: + description: '' + properties: + cashapp_handle_redirect_or_display_qr_code: + $ref: >- + #/components/schemas/payment_intent_next_action_cashapp_handle_redirect_or_display_qr_code + redirect_to_url: + $ref: '#/components/schemas/setup_intent_next_action_redirect_to_url' + type: + description: >- + Type of the next action to perform. Refer to the other child + attributes under `next_action` for available values. Examples + include: `redirect_to_url`, `use_stripe_sdk`, + `alipay_handle_redirect`, `oxxo_display_details`, or + `verify_with_microdeposits`. + maxLength: 5000 + type: string + use_stripe_sdk: + description: >- + When confirming a SetupIntent with Stripe.js, Stripe.js depends on + the contents of this dictionary to invoke authentication flows. The + shape of the contents is subject to change and is only intended to + be used by Stripe.js. + type: object + verify_with_microdeposits: + $ref: >- + #/components/schemas/setup_intent_next_action_verify_with_microdeposits + required: + - type + title: SetupIntentNextAction + type: object + x-expandableFields: + - cashapp_handle_redirect_or_display_qr_code + - redirect_to_url + - verify_with_microdeposits + setup_intent_next_action_redirect_to_url: + description: '' + properties: + return_url: + description: >- + If the customer does not exit their browser while authenticating, + they will be redirected to this specified URL after completion. + maxLength: 5000 + nullable: true + type: string + url: + description: The URL you must redirect your customer to in order to authenticate. + maxLength: 5000 + nullable: true + type: string + title: SetupIntentNextActionRedirectToUrl + type: object + x-expandableFields: [] + setup_intent_next_action_verify_with_microdeposits: + description: '' + properties: + arrival_date: + description: The timestamp when the microdeposits are expected to land. + format: unix-time + type: integer + hosted_verification_url: + description: >- + The URL for the hosted verification page, which allows customers to + verify their bank account. + maxLength: 5000 + type: string + microdeposit_type: + description: >- + The type of the microdeposit sent to the customer. Used to + distinguish between different verification methods. + enum: + - amounts + - descriptor_code + nullable: true + type: string + required: + - arrival_date + - hosted_verification_url + title: SetupIntentNextActionVerifyWithMicrodeposits + type: object + x-expandableFields: [] + setup_intent_payment_method_options: + description: '' + properties: + acss_debit: + anyOf: + - $ref: >- + #/components/schemas/setup_intent_payment_method_options_acss_debit + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + amazon_pay: + anyOf: + - $ref: >- + #/components/schemas/setup_intent_payment_method_options_amazon_pay + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + bacs_debit: + anyOf: + - $ref: >- + #/components/schemas/setup_intent_payment_method_options_bacs_debit + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + card: + anyOf: + - $ref: '#/components/schemas/setup_intent_payment_method_options_card' + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + card_present: + anyOf: + - $ref: >- + #/components/schemas/setup_intent_payment_method_options_card_present + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + link: + anyOf: + - $ref: '#/components/schemas/setup_intent_payment_method_options_link' + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + paypal: + anyOf: + - $ref: '#/components/schemas/setup_intent_payment_method_options_paypal' + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + sepa_debit: + anyOf: + - $ref: >- + #/components/schemas/setup_intent_payment_method_options_sepa_debit + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + us_bank_account: + anyOf: + - $ref: >- + #/components/schemas/setup_intent_payment_method_options_us_bank_account + - $ref: >- + #/components/schemas/setup_intent_type_specific_payment_method_options_client + title: SetupIntentPaymentMethodOptions + type: object + x-expandableFields: + - acss_debit + - amazon_pay + - bacs_debit + - card + - card_present + - link + - paypal + - sepa_debit + - us_bank_account + setup_intent_payment_method_options_acss_debit: + description: '' + properties: + currency: + description: Currency supported by the bank account + enum: + - cad + - usd + nullable: true + type: string + mandate_options: + $ref: >- + #/components/schemas/setup_intent_payment_method_options_mandate_options_acss_debit + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: setup_intent_payment_method_options_acss_debit + type: object + x-expandableFields: + - mandate_options + setup_intent_payment_method_options_amazon_pay: + description: '' + properties: {} + title: setup_intent_payment_method_options_amazon_pay + type: object + x-expandableFields: [] + setup_intent_payment_method_options_bacs_debit: + description: '' + properties: + mandate_options: + $ref: >- + #/components/schemas/setup_intent_payment_method_options_mandate_options_bacs_debit + title: setup_intent_payment_method_options_bacs_debit + type: object + x-expandableFields: + - mandate_options + setup_intent_payment_method_options_card: + description: '' + properties: + mandate_options: + anyOf: + - $ref: >- + #/components/schemas/setup_intent_payment_method_options_card_mandate_options + description: >- + Configuration options for setting up an eMandate for cards issued in + India. + nullable: true + network: + description: >- + Selected network to process this SetupIntent on. Depends on the + available networks of the card attached to the setup intent. Can be + only set confirm-time. + enum: + - amex + - cartes_bancaires + - diners + - discover + - eftpos_au + - girocard + - interac + - jcb + - link + - mastercard + - unionpay + - unknown + - visa + nullable: true + type: string + request_three_d_secure: + description: >- + We strongly recommend that you rely on our SCA Engine to + automatically prompt your customers for authentication based on risk + level and [other + requirements](https://stripe.com/docs/strong-customer-authentication). + However, if you wish to request 3D Secure based on logic from your + own fraud engine, provide this option. If not provided, this value + defaults to `automatic`. Read our guide on [manually requesting 3D + Secure](https://stripe.com/docs/payments/3d-secure/authentication-flow#manual-three-ds) + for more information on how this configuration interacts with Radar + and our SCA Engine. + enum: + - any + - automatic + - challenge + nullable: true + type: string + x-stripeBypassValidation: true + title: setup_intent_payment_method_options_card + type: object + x-expandableFields: + - mandate_options + setup_intent_payment_method_options_card_mandate_options: + description: '' + properties: + amount: + description: Amount to be charged for future payments. + type: integer + amount_type: + description: >- + One of `fixed` or `maximum`. If `fixed`, the `amount` param refers + to the exact amount to be charged in future payments. If `maximum`, + the amount charged can be up to the value passed for the `amount` + param. + enum: + - fixed + - maximum + type: string + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + A description of the mandate or subscription that is meant to be + displayed to the customer. + maxLength: 200 + nullable: true + type: string + end_date: + description: >- + End date of the mandate or subscription. If not provided, the + mandate will be active until canceled. If provided, end date should + be after start date. + format: unix-time + nullable: true + type: integer + interval: + description: >- + Specifies payment frequency. One of `day`, `week`, `month`, `year`, + or `sporadic`. + enum: + - day + - month + - sporadic + - week + - year + type: string + interval_count: + description: >- + The number of intervals between payments. For example, + `interval=month` and `interval_count=3` indicates one payment every + three months. Maximum of one year interval allowed (1 year, 12 + months, or 52 weeks). This parameter is optional when + `interval=sporadic`. + nullable: true + type: integer + reference: + description: Unique identifier for the mandate or subscription. + maxLength: 80 + type: string + start_date: + description: >- + Start date of the mandate or subscription. Start date should not be + lesser than yesterday. + format: unix-time + type: integer + supported_types: + description: >- + Specifies the type of mandates supported. Possible values are + `india`. + items: + enum: + - india + type: string + nullable: true + type: array + required: + - amount + - amount_type + - currency + - interval + - reference + - start_date + title: setup_intent_payment_method_options_card_mandate_options + type: object + x-expandableFields: [] + setup_intent_payment_method_options_card_present: + description: '' + properties: {} + title: setup_intent_payment_method_options_card_present + type: object + x-expandableFields: [] + setup_intent_payment_method_options_link: + description: '' + properties: {} + title: setup_intent_payment_method_options_link + type: object + x-expandableFields: [] + setup_intent_payment_method_options_mandate_options_acss_debit: + description: '' + properties: + custom_mandate_url: + description: A URL for custom mandate text + maxLength: 5000 + type: string + default_for: + description: >- + List of Stripe products where this mandate can be selected + automatically. + items: + enum: + - invoice + - subscription + type: string + type: array + interval_description: + description: >- + Description of the interval. Only required if the 'payment_schedule' + parameter is 'interval' or 'combined'. + maxLength: 5000 + nullable: true + type: string + payment_schedule: + description: Payment schedule for the mandate. + enum: + - combined + - interval + - sporadic + nullable: true + type: string + transaction_type: + description: Transaction type of the mandate. + enum: + - business + - personal + nullable: true + type: string + title: setup_intent_payment_method_options_mandate_options_acss_debit + type: object + x-expandableFields: [] + setup_intent_payment_method_options_mandate_options_bacs_debit: + description: '' + properties: + reference_prefix: + description: >- + Prefix used to generate the Mandate reference. Must be at most 12 + characters long. Must consist of only uppercase letters, numbers, + spaces, or the following special characters: '/', '_', '-', '&', + '.'. Cannot begin with 'DDIC' or 'STRIPE'. + maxLength: 5000 + type: string + title: setup_intent_payment_method_options_mandate_options_bacs_debit + type: object + x-expandableFields: [] + setup_intent_payment_method_options_mandate_options_sepa_debit: + description: '' + properties: + reference_prefix: + description: >- + Prefix used to generate the Mandate reference. Must be at most 12 + characters long. Must consist of only uppercase letters, numbers, + spaces, or the following special characters: '/', '_', '-', '&', + '.'. Cannot begin with 'STRIPE'. + maxLength: 5000 + type: string + title: setup_intent_payment_method_options_mandate_options_sepa_debit + type: object + x-expandableFields: [] + setup_intent_payment_method_options_paypal: + description: '' + properties: + billing_agreement_id: + description: >- + The PayPal Billing Agreement ID (BAID). This is an ID generated by + PayPal which represents the mandate between the merchant and the + customer. + maxLength: 5000 + nullable: true + type: string + title: setup_intent_payment_method_options_paypal + type: object + x-expandableFields: [] + setup_intent_payment_method_options_sepa_debit: + description: '' + properties: + mandate_options: + $ref: >- + #/components/schemas/setup_intent_payment_method_options_mandate_options_sepa_debit + title: setup_intent_payment_method_options_sepa_debit + type: object + x-expandableFields: + - mandate_options + setup_intent_payment_method_options_us_bank_account: + description: '' + properties: + financial_connections: + $ref: '#/components/schemas/linked_account_options_common' + mandate_options: + $ref: >- + #/components/schemas/payment_method_options_us_bank_account_mandate_options + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: setup_intent_payment_method_options_us_bank_account + type: object + x-expandableFields: + - financial_connections + - mandate_options + setup_intent_type_specific_payment_method_options_client: + description: '' + properties: + verification_method: + description: Bank account verification method. + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: SetupIntentTypeSpecificPaymentMethodOptionsClient + type: object + x-expandableFields: [] + shipping: + description: '' + properties: + address: + $ref: '#/components/schemas/address' + carrier: + description: >- + The delivery service that shipped a physical product, such as Fedex, + UPS, USPS, etc. + maxLength: 5000 + nullable: true + type: string + name: + description: Recipient name. + maxLength: 5000 + type: string + phone: + description: Recipient phone (including extension). + maxLength: 5000 + nullable: true + type: string + tracking_number: + description: >- + The tracking number for a physical product, obtained from the + delivery service. If multiple tracking numbers were generated for + this purchase, please separate them with commas. + maxLength: 5000 + nullable: true + type: string + title: Shipping + type: object + x-expandableFields: + - address + shipping_rate: + description: >- + Shipping rates describe the price of shipping presented to your + customers and + + applied to a purchase. For more information, see [Charge for + shipping](https://stripe.com/docs/payments/during-payment/charge-shipping). + properties: + active: + description: >- + Whether the shipping rate can be used for new purchases. Defaults to + `true`. + type: boolean + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + delivery_estimate: + anyOf: + - $ref: '#/components/schemas/shipping_rate_delivery_estimate' + description: >- + The estimated range for how long shipping will take, meant to be + displayable to the customer. This will appear on CheckoutSessions. + nullable: true + display_name: + description: >- + The name of the shipping rate, meant to be displayable to the + customer. This will appear on CheckoutSessions. + maxLength: 5000 + nullable: true + type: string + fixed_amount: + $ref: '#/components/schemas/shipping_rate_fixed_amount' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - shipping_rate + type: string + tax_behavior: + description: >- + Specifies whether the rate is considered inclusive of taxes or + exclusive of taxes. One of `inclusive`, `exclusive`, or + `unspecified`. + enum: + - exclusive + - inclusive + - unspecified + nullable: true + type: string + tax_code: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_code' + description: >- + A [tax code](https://stripe.com/docs/tax/tax-categories) ID. The + Shipping tax code is `txcd_92010001`. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_code' + type: + description: The type of calculation to use on the shipping rate. + enum: + - fixed_amount + type: string + required: + - active + - created + - id + - livemode + - metadata + - object + - type + title: ShippingRate + type: object + x-expandableFields: + - delivery_estimate + - fixed_amount + - tax_code + x-resourceId: shipping_rate + shipping_rate_currency_option: + description: '' + properties: + amount: + description: A non-negative integer in cents representing how much to charge. + type: integer + tax_behavior: + description: >- + Specifies whether the rate is considered inclusive of taxes or + exclusive of taxes. One of `inclusive`, `exclusive`, or + `unspecified`. + enum: + - exclusive + - inclusive + - unspecified + type: string + required: + - amount + - tax_behavior + title: ShippingRateCurrencyOption + type: object + x-expandableFields: [] + shipping_rate_delivery_estimate: + description: '' + properties: + maximum: + anyOf: + - $ref: '#/components/schemas/shipping_rate_delivery_estimate_bound' + description: >- + The upper bound of the estimated range. If empty, represents no + upper bound i.e., infinite. + nullable: true + minimum: + anyOf: + - $ref: '#/components/schemas/shipping_rate_delivery_estimate_bound' + description: >- + The lower bound of the estimated range. If empty, represents no + lower bound. + nullable: true + title: ShippingRateDeliveryEstimate + type: object + x-expandableFields: + - maximum + - minimum + shipping_rate_delivery_estimate_bound: + description: '' + properties: + unit: + description: A unit of time. + enum: + - business_day + - day + - hour + - month + - week + type: string + value: + description: Must be greater than 0. + type: integer + required: + - unit + - value + title: ShippingRateDeliveryEstimateBound + type: object + x-expandableFields: [] + shipping_rate_fixed_amount: + description: '' + properties: + amount: + description: A non-negative integer in cents representing how much to charge. + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + currency_options: + additionalProperties: + $ref: '#/components/schemas/shipping_rate_currency_option' + description: >- + Shipping rates defined in each available currency option. Each key + must be a three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html) and a + [supported currency](https://stripe.com/docs/currencies). + type: object + required: + - amount + - currency + title: ShippingRateFixedAmount + type: object + x-expandableFields: + - currency_options + sigma.sigma_api_query: + description: A saved query object represents a query that can be executed for a run. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + name: + description: The name of the query. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - sigma.sigma_api_query + type: string + sql: + description: The sql statement for the query. + maxLength: 5000 + type: string + required: + - created + - id + - livemode + - name + - object + - sql + title: SigmaSigmaResourcesSigmaAPIQuery + type: object + x-expandableFields: [] + x-resourceId: sigma.sigma_api_query + sigma_scheduled_query_run_error: + description: '' + properties: + message: + description: Information about the run failure. + maxLength: 5000 + type: string + required: + - message + title: SigmaScheduledQueryRunError + type: object + x-expandableFields: [] + source: + description: >- + `Source` objects allow you to accept a variety of payment methods. They + + represent a customer's payment instrument, and can be used with the + Stripe API + + just like a `Card` object: once chargeable, they can be charged, or can + be + + attached to customers. + + + Stripe doesn't recommend using the deprecated [Sources + API](https://stripe.com/docs/api/sources). + + We recommend that you adopt the [PaymentMethods + API](https://stripe.com/docs/api/payment_methods). + + This newer API provides access to our latest features and payment method + types. + + + Related guides: [Sources API](https://stripe.com/docs/sources) and + [Sources & Customers](https://stripe.com/docs/sources/customers). + properties: + ach_credit_transfer: + $ref: '#/components/schemas/source_type_ach_credit_transfer' + ach_debit: + $ref: '#/components/schemas/source_type_ach_debit' + acss_debit: + $ref: '#/components/schemas/source_type_acss_debit' + alipay: + $ref: '#/components/schemas/source_type_alipay' + allow_redisplay: + description: >- + This field indicates whether this payment method can be shown again + to its customer in a checkout flow. Stripe products such as Checkout + and Elements use this field to determine whether a payment method + can be shown as a saved payment method in a checkout flow. The field + defaults to “unspecified”. + enum: + - always + - limited + - unspecified + nullable: true + type: string + amount: + description: >- + A positive integer in the smallest currency unit (that is, 100 cents + for $1.00, or 1 for ¥1, Japanese Yen being a zero-decimal currency) + representing the total amount associated with the source. This is + the amount for which the source will be chargeable once ready. + Required for `single_use` sources. + nullable: true + type: integer + au_becs_debit: + $ref: '#/components/schemas/source_type_au_becs_debit' + bancontact: + $ref: '#/components/schemas/source_type_bancontact' + card: + $ref: '#/components/schemas/source_type_card' + card_present: + $ref: '#/components/schemas/source_type_card_present' + client_secret: + description: >- + The client secret of the source. Used for client-side retrieval + using a publishable key. + maxLength: 5000 + type: string + code_verification: + $ref: '#/components/schemas/source_code_verification_flow' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO code for the + currency](https://stripe.com/docs/currencies) associated with the + source. This is the currency for which the source will be chargeable + once ready. Required for `single_use` sources. + format: currency + nullable: true + type: string + customer: + description: >- + The ID of the customer to which this source is attached. This will + not be present when the source has not been attached to a customer. + maxLength: 5000 + type: string + eps: + $ref: '#/components/schemas/source_type_eps' + flow: + description: >- + The authentication `flow` of the source. `flow` is one of + `redirect`, `receiver`, `code_verification`, `none`. + maxLength: 5000 + type: string + giropay: + $ref: '#/components/schemas/source_type_giropay' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + ideal: + $ref: '#/components/schemas/source_type_ideal' + klarna: + $ref: '#/components/schemas/source_type_klarna' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + multibanco: + $ref: '#/components/schemas/source_type_multibanco' + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - source + type: string + owner: + anyOf: + - $ref: '#/components/schemas/source_owner' + description: >- + Information about the owner of the payment instrument that may be + used or required by particular source types. + nullable: true + p24: + $ref: '#/components/schemas/source_type_p24' + receiver: + $ref: '#/components/schemas/source_receiver_flow' + redirect: + $ref: '#/components/schemas/source_redirect_flow' + sepa_debit: + $ref: '#/components/schemas/source_type_sepa_debit' + sofort: + $ref: '#/components/schemas/source_type_sofort' + source_order: + $ref: '#/components/schemas/source_order' + statement_descriptor: + description: >- + Extra information about a source. This will appear on your + customer's statement every time you charge the source. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + The status of the source, one of `canceled`, `chargeable`, + `consumed`, `failed`, or `pending`. Only `chargeable` sources can be + used to create a charge. + maxLength: 5000 + type: string + three_d_secure: + $ref: '#/components/schemas/source_type_three_d_secure' + type: + description: >- + The `type` of the source. The `type` is a payment method, one of + `ach_credit_transfer`, `ach_debit`, `alipay`, `bancontact`, `card`, + `card_present`, `eps`, `giropay`, `ideal`, `multibanco`, `klarna`, + `p24`, `sepa_debit`, `sofort`, `three_d_secure`, or `wechat`. An + additional hash is included on the source with a name matching this + value. It contains additional information specific to the [payment + method](https://stripe.com/docs/sources) used. + enum: + - ach_credit_transfer + - ach_debit + - acss_debit + - alipay + - au_becs_debit + - bancontact + - card + - card_present + - eps + - giropay + - ideal + - klarna + - multibanco + - p24 + - sepa_debit + - sofort + - three_d_secure + - wechat + type: string + x-stripeBypassValidation: true + usage: + description: >- + Either `reusable` or `single_use`. Whether this source should be + reusable or not. Some source types may or may not be reusable by + construction, while others may leave the option at creation. If an + incompatible value is passed, an error will be returned. + maxLength: 5000 + nullable: true + type: string + wechat: + $ref: '#/components/schemas/source_type_wechat' + required: + - client_secret + - created + - flow + - id + - livemode + - object + - status + - type + title: Source + type: object + x-expandableFields: + - code_verification + - owner + - receiver + - redirect + - source_order + x-resourceId: source + source_code_verification_flow: + description: '' + properties: + attempts_remaining: + description: >- + The number of attempts remaining to authenticate the source object + with a verification code. + type: integer + status: + description: >- + The status of the code verification, either `pending` (awaiting + verification, `attempts_remaining` should be greater than 0), + `succeeded` (successful verification) or `failed` (failed + verification, cannot be verified anymore as `attempts_remaining` + should be 0). + maxLength: 5000 + type: string + required: + - attempts_remaining + - status + title: SourceCodeVerificationFlow + type: object + x-expandableFields: [] + source_mandate_notification: + description: >- + Source mandate notifications should be created when a notification + related to + + a source mandate must be sent to the payer. They will trigger a webhook + or + + deliver an email to the customer. + properties: + acss_debit: + $ref: '#/components/schemas/source_mandate_notification_acss_debit_data' + amount: + description: >- + A positive integer in the smallest currency unit (that is, 100 cents + for $1.00, or 1 for ¥1, Japanese Yen being a zero-decimal currency) + representing the amount associated with the mandate notification. + The amount is expressed in the currency of the underlying source. + Required if the notification type is `debit_initiated`. + nullable: true + type: integer + bacs_debit: + $ref: '#/components/schemas/source_mandate_notification_bacs_debit_data' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - source_mandate_notification + type: string + reason: + description: >- + The reason of the mandate notification. Valid reasons are + `mandate_confirmed` or `debit_initiated`. + maxLength: 5000 + type: string + sepa_debit: + $ref: '#/components/schemas/source_mandate_notification_sepa_debit_data' + source: + $ref: '#/components/schemas/source' + status: + description: >- + The status of the mandate notification. Valid statuses are `pending` + or `submitted`. + maxLength: 5000 + type: string + type: + description: >- + The type of source this mandate notification is attached to. Should + be the source type identifier code for the payment method, such as + `three_d_secure`. + maxLength: 5000 + type: string + required: + - created + - id + - livemode + - object + - reason + - source + - status + - type + title: SourceMandateNotification + type: object + x-expandableFields: + - acss_debit + - bacs_debit + - sepa_debit + - source + x-resourceId: source_mandate_notification + source_mandate_notification_acss_debit_data: + description: '' + properties: + statement_descriptor: + description: The statement descriptor associate with the debit. + maxLength: 5000 + type: string + title: SourceMandateNotificationAcssDebitData + type: object + x-expandableFields: [] + source_mandate_notification_bacs_debit_data: + description: '' + properties: + last4: + description: Last 4 digits of the account number associated with the debit. + maxLength: 5000 + type: string + title: SourceMandateNotificationBacsDebitData + type: object + x-expandableFields: [] + source_mandate_notification_sepa_debit_data: + description: '' + properties: + creditor_identifier: + description: SEPA creditor ID. + maxLength: 5000 + type: string + last4: + description: Last 4 digits of the account number associated with the debit. + maxLength: 5000 + type: string + mandate_reference: + description: Mandate reference associated with the debit. + maxLength: 5000 + type: string + title: SourceMandateNotificationSepaDebitData + type: object + x-expandableFields: [] + source_order: + description: '' + properties: + amount: + description: >- + A positive integer in the smallest currency unit (that is, 100 cents + for $1.00, or 1 for ¥1, Japanese Yen being a zero-decimal currency) + representing the total amount for the order. + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + email: + description: The email address of the customer placing the order. + maxLength: 5000 + type: string + items: + description: List of items constituting the order. + items: + $ref: '#/components/schemas/source_order_item' + nullable: true + type: array + shipping: + $ref: '#/components/schemas/shipping' + required: + - amount + - currency + title: SourceOrder + type: object + x-expandableFields: + - items + - shipping + source_order_item: + description: '' + properties: + amount: + description: The amount (price) for this order item. + nullable: true + type: integer + currency: + description: This currency of this order item. Required when `amount` is present. + maxLength: 5000 + nullable: true + type: string + description: + description: Human-readable description for this order item. + maxLength: 5000 + nullable: true + type: string + parent: + description: >- + The ID of the associated object for this line item. Expandable if + not null (e.g., expandable to a SKU). + maxLength: 5000 + nullable: true + type: string + quantity: + description: >- + The quantity of this order item. When type is `sku`, this is the + number of instances of the SKU to be ordered. + type: integer + type: + description: 'The type of this order item. Must be `sku`, `tax`, or `shipping`.' + maxLength: 5000 + nullable: true + type: string + title: SourceOrderItem + type: object + x-expandableFields: [] + source_owner: + description: '' + properties: + address: + anyOf: + - $ref: '#/components/schemas/address' + description: Owner's address. + nullable: true + email: + description: Owner's email address. + maxLength: 5000 + nullable: true + type: string + name: + description: Owner's full name. + maxLength: 5000 + nullable: true + type: string + phone: + description: Owner's phone number (including extension). + maxLength: 5000 + nullable: true + type: string + verified_address: + anyOf: + - $ref: '#/components/schemas/address' + description: >- + Verified owner's address. Verified values are verified or provided + by the payment method directly (and if supported) at the time of + authorization or settlement. They cannot be set or mutated. + nullable: true + verified_email: + description: >- + Verified owner's email address. Verified values are verified or + provided by the payment method directly (and if supported) at the + time of authorization or settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + verified_name: + description: >- + Verified owner's full name. Verified values are verified or provided + by the payment method directly (and if supported) at the time of + authorization or settlement. They cannot be set or mutated. + maxLength: 5000 + nullable: true + type: string + verified_phone: + description: >- + Verified owner's phone number (including extension). Verified values + are verified or provided by the payment method directly (and if + supported) at the time of authorization or settlement. They cannot + be set or mutated. + maxLength: 5000 + nullable: true + type: string + title: SourceOwner + type: object + x-expandableFields: + - address + - verified_address + source_receiver_flow: + description: '' + properties: + address: + description: >- + The address of the receiver source. This is the value that should be + communicated to the customer to send their funds to. + maxLength: 5000 + nullable: true + type: string + amount_charged: + description: >- + The total amount that was moved to your balance. This is almost + always equal to the amount charged. In rare cases when customers + deposit excess funds and we are unable to refund those, those funds + get moved to your balance and show up in amount_charged as well. The + amount charged is expressed in the source's currency. + type: integer + amount_received: + description: >- + The total amount received by the receiver source. `amount_received = + amount_returned + amount_charged` should be true for consumed + sources unless customers deposit excess funds. The amount received + is expressed in the source's currency. + type: integer + amount_returned: + description: >- + The total amount that was returned to the customer. The amount + returned is expressed in the source's currency. + type: integer + refund_attributes_method: + description: >- + Type of refund attribute method, one of `email`, `manual`, or + `none`. + maxLength: 5000 + type: string + refund_attributes_status: + description: >- + Type of refund attribute status, one of `missing`, `requested`, or + `available`. + maxLength: 5000 + type: string + required: + - amount_charged + - amount_received + - amount_returned + - refund_attributes_method + - refund_attributes_status + title: SourceReceiverFlow + type: object + x-expandableFields: [] + source_redirect_flow: + description: '' + properties: + failure_reason: + description: >- + The failure reason for the redirect, either `user_abort` (the + customer aborted or dropped out of the redirect flow), `declined` + (the authentication failed or the transaction was declined), or + `processing_error` (the redirect failed due to a technical error). + Present only if the redirect status is `failed`. + maxLength: 5000 + nullable: true + type: string + return_url: + description: >- + The URL you provide to redirect the customer to after they + authenticated their payment. + maxLength: 5000 + type: string + status: + description: >- + The status of the redirect, either `pending` (ready to be used by + your customer to authenticate the transaction), `succeeded` + (succesful authentication, cannot be reused) or `not_required` + (redirect should not be used) or `failed` (failed authentication, + cannot be reused). + maxLength: 5000 + type: string + url: + description: >- + The URL provided to you to redirect a customer to as part of a + `redirect` authentication flow. + maxLength: 2048 + type: string + required: + - return_url + - status + - url + title: SourceRedirectFlow + type: object + x-expandableFields: [] + source_transaction: + description: |- + Some payment methods have no required amount that a customer must send. + Customers can be instructed to send any amount, and it can be made up of + multiple transactions. As such, sources can have multiple associated + transactions. + properties: + ach_credit_transfer: + $ref: '#/components/schemas/source_transaction_ach_credit_transfer_data' + amount: + description: >- + A positive integer in the smallest currency unit (that is, 100 cents + for $1.00, or 1 for ¥1, Japanese Yen being a zero-decimal currency) + representing the amount your customer has pushed to the receiver. + type: integer + chf_credit_transfer: + $ref: '#/components/schemas/source_transaction_chf_credit_transfer_data' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + gbp_credit_transfer: + $ref: '#/components/schemas/source_transaction_gbp_credit_transfer_data' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - source_transaction + type: string + paper_check: + $ref: '#/components/schemas/source_transaction_paper_check_data' + sepa_credit_transfer: + $ref: '#/components/schemas/source_transaction_sepa_credit_transfer_data' + source: + description: The ID of the source this transaction is attached to. + maxLength: 5000 + type: string + status: + description: >- + The status of the transaction, one of `succeeded`, `pending`, or + `failed`. + maxLength: 5000 + type: string + type: + description: The type of source this transaction is attached to. + enum: + - ach_credit_transfer + - ach_debit + - alipay + - bancontact + - card + - card_present + - eps + - giropay + - ideal + - klarna + - multibanco + - p24 + - sepa_debit + - sofort + - three_d_secure + - wechat + type: string + required: + - amount + - created + - currency + - id + - livemode + - object + - source + - status + - type + title: SourceTransaction + type: object + x-expandableFields: + - ach_credit_transfer + - chf_credit_transfer + - gbp_credit_transfer + - paper_check + - sepa_credit_transfer + x-resourceId: source_transaction + source_transaction_ach_credit_transfer_data: + description: '' + properties: + customer_data: + description: Customer data associated with the transfer. + maxLength: 5000 + type: string + fingerprint: + description: Bank account fingerprint associated with the transfer. + maxLength: 5000 + type: string + last4: + description: Last 4 digits of the account number associated with the transfer. + maxLength: 5000 + type: string + routing_number: + description: Routing number associated with the transfer. + maxLength: 5000 + type: string + title: SourceTransactionAchCreditTransferData + type: object + x-expandableFields: [] + source_transaction_chf_credit_transfer_data: + description: '' + properties: + reference: + description: Reference associated with the transfer. + maxLength: 5000 + type: string + sender_address_country: + description: Sender's country address. + maxLength: 5000 + type: string + sender_address_line1: + description: Sender's line 1 address. + maxLength: 5000 + type: string + sender_iban: + description: Sender's bank account IBAN. + maxLength: 5000 + type: string + sender_name: + description: Sender's name. + maxLength: 5000 + type: string + title: SourceTransactionChfCreditTransferData + type: object + x-expandableFields: [] + source_transaction_gbp_credit_transfer_data: + description: '' + properties: + fingerprint: + description: >- + Bank account fingerprint associated with the Stripe owned bank + account receiving the transfer. + maxLength: 5000 + type: string + funding_method: + description: >- + The credit transfer rails the sender used to push this transfer. The + possible rails are: Faster Payments, BACS, CHAPS, and wire + transfers. Currently only Faster Payments is supported. + maxLength: 5000 + type: string + last4: + description: Last 4 digits of sender account number associated with the transfer. + maxLength: 5000 + type: string + reference: + description: Sender entered arbitrary information about the transfer. + maxLength: 5000 + type: string + sender_account_number: + description: Sender account number associated with the transfer. + maxLength: 5000 + type: string + sender_name: + description: Sender name associated with the transfer. + maxLength: 5000 + type: string + sender_sort_code: + description: Sender sort code associated with the transfer. + maxLength: 5000 + type: string + title: SourceTransactionGbpCreditTransferData + type: object + x-expandableFields: [] + source_transaction_paper_check_data: + description: '' + properties: + available_at: + description: >- + Time at which the deposited funds will be available for use. + Measured in seconds since the Unix epoch. + maxLength: 5000 + type: string + invoices: + description: Comma-separated list of invoice IDs associated with the paper check. + maxLength: 5000 + type: string + title: SourceTransactionPaperCheckData + type: object + x-expandableFields: [] + source_transaction_sepa_credit_transfer_data: + description: '' + properties: + reference: + description: Reference associated with the transfer. + maxLength: 5000 + type: string + sender_iban: + description: Sender's bank account IBAN. + maxLength: 5000 + type: string + sender_name: + description: Sender's name. + maxLength: 5000 + type: string + title: SourceTransactionSepaCreditTransferData + type: object + x-expandableFields: [] + source_type_ach_credit_transfer: + properties: + account_number: + nullable: true + type: string + bank_name: + nullable: true + type: string + fingerprint: + nullable: true + type: string + refund_account_holder_name: + nullable: true + type: string + refund_account_holder_type: + nullable: true + type: string + refund_routing_number: + nullable: true + type: string + routing_number: + nullable: true + type: string + swift_code: + nullable: true + type: string + type: object + source_type_ach_debit: + properties: + bank_name: + nullable: true + type: string + country: + nullable: true + type: string + fingerprint: + nullable: true + type: string + last4: + nullable: true + type: string + routing_number: + nullable: true + type: string + type: + nullable: true + type: string + type: object + source_type_acss_debit: + properties: + bank_address_city: + nullable: true + type: string + bank_address_line_1: + nullable: true + type: string + bank_address_line_2: + nullable: true + type: string + bank_address_postal_code: + nullable: true + type: string + bank_name: + nullable: true + type: string + category: + nullable: true + type: string + country: + nullable: true + type: string + fingerprint: + nullable: true + type: string + last4: + nullable: true + type: string + routing_number: + nullable: true + type: string + type: object + source_type_alipay: + properties: + data_string: + nullable: true + type: string + native_url: + nullable: true + type: string + statement_descriptor: + nullable: true + type: string + type: object + source_type_au_becs_debit: + properties: + bsb_number: + nullable: true + type: string + fingerprint: + nullable: true + type: string + last4: + nullable: true + type: string + type: object + source_type_bancontact: + properties: + bank_code: + nullable: true + type: string + bank_name: + nullable: true + type: string + bic: + nullable: true + type: string + iban_last4: + nullable: true + type: string + preferred_language: + nullable: true + type: string + statement_descriptor: + nullable: true + type: string + type: object + source_type_card: + properties: + address_line1_check: + nullable: true + type: string + address_zip_check: + nullable: true + type: string + brand: + nullable: true + type: string + country: + nullable: true + type: string + cvc_check: + nullable: true + type: string + dynamic_last4: + nullable: true + type: string + exp_month: + nullable: true + type: integer + exp_year: + nullable: true + type: integer + fingerprint: + type: string + funding: + nullable: true + type: string + last4: + nullable: true + type: string + name: + nullable: true + type: string + three_d_secure: + type: string + tokenization_method: + nullable: true + type: string + type: object + source_type_card_present: + properties: + application_cryptogram: + type: string + application_preferred_name: + type: string + authorization_code: + nullable: true + type: string + authorization_response_code: + type: string + brand: + nullable: true + type: string + country: + nullable: true + type: string + cvm_type: + type: string + data_type: + nullable: true + type: string + dedicated_file_name: + type: string + emv_auth_data: + type: string + evidence_customer_signature: + nullable: true + type: string + evidence_transaction_certificate: + nullable: true + type: string + exp_month: + nullable: true + type: integer + exp_year: + nullable: true + type: integer + fingerprint: + type: string + funding: + nullable: true + type: string + last4: + nullable: true + type: string + pos_device_id: + nullable: true + type: string + pos_entry_mode: + type: string + read_method: + nullable: true + type: string + reader: + nullable: true + type: string + terminal_verification_results: + type: string + transaction_status_information: + type: string + type: object + source_type_eps: + properties: + reference: + nullable: true + type: string + statement_descriptor: + nullable: true + type: string + type: object + source_type_giropay: + properties: + bank_code: + nullable: true + type: string + bank_name: + nullable: true + type: string + bic: + nullable: true + type: string + statement_descriptor: + nullable: true + type: string + type: object + source_type_ideal: + properties: + bank: + nullable: true + type: string + bic: + nullable: true + type: string + iban_last4: + nullable: true + type: string + statement_descriptor: + nullable: true + type: string + type: object + source_type_klarna: + properties: + background_image_url: + type: string + client_token: + nullable: true + type: string + first_name: + type: string + last_name: + type: string + locale: + type: string + logo_url: + type: string + page_title: + type: string + pay_later_asset_urls_descriptive: + type: string + pay_later_asset_urls_standard: + type: string + pay_later_name: + type: string + pay_later_redirect_url: + type: string + pay_now_asset_urls_descriptive: + type: string + pay_now_asset_urls_standard: + type: string + pay_now_name: + type: string + pay_now_redirect_url: + type: string + pay_over_time_asset_urls_descriptive: + type: string + pay_over_time_asset_urls_standard: + type: string + pay_over_time_name: + type: string + pay_over_time_redirect_url: + type: string + payment_method_categories: + type: string + purchase_country: + type: string + purchase_type: + type: string + redirect_url: + type: string + shipping_delay: + type: integer + shipping_first_name: + type: string + shipping_last_name: + type: string + type: object + source_type_multibanco: + properties: + entity: + nullable: true + type: string + reference: + nullable: true + type: string + refund_account_holder_address_city: + nullable: true + type: string + refund_account_holder_address_country: + nullable: true + type: string + refund_account_holder_address_line1: + nullable: true + type: string + refund_account_holder_address_line2: + nullable: true + type: string + refund_account_holder_address_postal_code: + nullable: true + type: string + refund_account_holder_address_state: + nullable: true + type: string + refund_account_holder_name: + nullable: true + type: string + refund_iban: + nullable: true + type: string + type: object + source_type_p24: + properties: + reference: + nullable: true + type: string + type: object + source_type_sepa_debit: + properties: + bank_code: + nullable: true + type: string + branch_code: + nullable: true + type: string + country: + nullable: true + type: string + fingerprint: + nullable: true + type: string + last4: + nullable: true + type: string + mandate_reference: + nullable: true + type: string + mandate_url: + nullable: true + type: string + type: object + source_type_sofort: + properties: + bank_code: + nullable: true + type: string + bank_name: + nullable: true + type: string + bic: + nullable: true + type: string + country: + nullable: true + type: string + iban_last4: + nullable: true + type: string + preferred_language: + nullable: true + type: string + statement_descriptor: + nullable: true + type: string + type: object + source_type_three_d_secure: + properties: + address_line1_check: + nullable: true + type: string + address_zip_check: + nullable: true + type: string + authenticated: + nullable: true + type: boolean + brand: + nullable: true + type: string + card: + nullable: true + type: string + country: + nullable: true + type: string + customer: + nullable: true + type: string + cvc_check: + nullable: true + type: string + dynamic_last4: + nullable: true + type: string + exp_month: + nullable: true + type: integer + exp_year: + nullable: true + type: integer + fingerprint: + type: string + funding: + nullable: true + type: string + last4: + nullable: true + type: string + name: + nullable: true + type: string + three_d_secure: + type: string + tokenization_method: + nullable: true + type: string + type: object + source_type_wechat: + properties: + prepay_id: + type: string + qr_code_url: + nullable: true + type: string + statement_descriptor: + type: string + type: object + subscription: + description: >- + Subscriptions allow you to charge a customer on a recurring basis. + + + Related guide: [Creating + subscriptions](https://stripe.com/docs/billing/subscriptions/creating) + properties: + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + description: ID of the Connect Application that created the subscription. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + application_fee_percent: + description: >- + A non-negative decimal between 0 and 100, with at most two decimal + places. This represents the percentage of the subscription invoice + total that will be transferred to the application owner's Stripe + account. + nullable: true + type: number + automatic_tax: + $ref: '#/components/schemas/subscription_automatic_tax' + billing_cycle_anchor: + description: >- + The reference point that aligns future [billing + cycle](https://stripe.com/docs/subscriptions/billing-cycle) dates. + It sets the day of week for `week` intervals, the day of month for + `month` and `year` intervals, and the month of year for `year` + intervals. The timestamp is in UTC format. + format: unix-time + type: integer + billing_cycle_anchor_config: + anyOf: + - $ref: >- + #/components/schemas/subscriptions_resource_billing_cycle_anchor_config + description: The fixed values used to calculate the `billing_cycle_anchor`. + nullable: true + billing_thresholds: + anyOf: + - $ref: '#/components/schemas/subscription_billing_thresholds' + description: >- + Define thresholds at which an invoice will be sent, and the + subscription advanced to a new billing period + nullable: true + cancel_at: + description: >- + A date in the future at which the subscription will automatically + get canceled + format: unix-time + nullable: true + type: integer + cancel_at_period_end: + description: >- + Whether this subscription will (if `status=active`) or did (if + `status=canceled`) cancel at the end of the current billing period. + This field will be removed in a future API version. Please use + `cancel_at` instead. + type: boolean + canceled_at: + description: >- + If the subscription has been canceled, the date of that + cancellation. If the subscription was canceled with + `cancel_at_period_end`, `canceled_at` will reflect the time of the + most recent update request, not the end of the subscription period + when the subscription is automatically moved to a canceled state. + format: unix-time + nullable: true + type: integer + cancellation_details: + anyOf: + - $ref: '#/components/schemas/cancellation_details' + description: Details about why this subscription was cancelled + nullable: true + collection_method: + description: >- + Either `charge_automatically`, or `send_invoice`. When charging + automatically, Stripe will attempt to pay this subscription at the + end of the cycle using the default source attached to the customer. + When sending an invoice, Stripe will email your customer an invoice + with payment instructions and mark the subscription as `active`. + enum: + - charge_automatically + - send_invoice + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: ID of the customer who owns the subscription. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + days_until_due: + description: >- + Number of days a customer has to pay invoices generated by this + subscription. This value will be `null` for subscriptions where + `collection_method=charge_automatically`. + nullable: true + type: integer + default_payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + ID of the default payment method for the subscription. It must + belong to the customer associated with the subscription. This takes + precedence over `default_source`. If neither are set, invoices will + use the customer's + [invoice_settings.default_payment_method](https://stripe.com/docs/api/customers/object#customer_object-invoice_settings-default_payment_method) + or + [default_source](https://stripe.com/docs/api/customers/object#customer_object-default_source). + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + default_source: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + description: >- + ID of the default payment source for the subscription. It must + belong to the customer associated with the subscription and be in a + chargeable state. If `default_payment_method` is also set, + `default_payment_method` will take precedence. If neither are set, + invoices will use the customer's + [invoice_settings.default_payment_method](https://stripe.com/docs/api/customers/object#customer_object-invoice_settings-default_payment_method) + or + [default_source](https://stripe.com/docs/api/customers/object#customer_object-default_source). + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + x-stripeBypassValidation: true + default_tax_rates: + description: >- + The tax rates that will apply to any subscription item that does not + have `tax_rates` set. Invoices created will have their + `default_tax_rates` populated from the subscription. + items: + $ref: '#/components/schemas/tax_rate' + nullable: true + type: array + description: + description: >- + The subscription's description, meant to be displayable to the + customer. Use this field to optionally store an explanation of the + subscription for rendering in Stripe surfaces and certain local + payment methods UIs. + maxLength: 500 + nullable: true + type: string + discounts: + description: >- + The discounts applied to the subscription. Subscription item + discounts are applied before subscription discounts. Use + `expand[]=discounts` to expand each discount. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + type: array + ended_at: + description: 'If the subscription has ended, the date the subscription ended.' + format: unix-time + nullable: true + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + invoice_settings: + $ref: >- + #/components/schemas/subscriptions_resource_subscription_invoice_settings + items: + description: 'List of subscription items, each with an attached price.' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/subscription_item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: SubscriptionItemList + type: object + x-expandableFields: + - data + latest_invoice: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/invoice' + description: The most recent invoice this subscription has generated. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/invoice' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + next_pending_invoice_item_invoice: + description: >- + Specifies the approximate timestamp on which any pending invoice + items will be billed according to the schedule provided at + `pending_invoice_item_interval`. + format: unix-time + nullable: true + type: integer + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - subscription + type: string + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account (if any) the charge was made on behalf of for charges + associated with this subscription. See the [Connect + documentation](https://stripe.com/docs/connect/subscriptions#on-behalf-of) + for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + pause_collection: + anyOf: + - $ref: '#/components/schemas/subscriptions_resource_pause_collection' + description: >- + If specified, payment collection for this subscription will be + paused. Note that the subscription status will be unchanged and will + not be updated to `paused`. Learn more about [pausing + collection](https://stripe.com/docs/billing/subscriptions/pause-payment). + nullable: true + payment_settings: + anyOf: + - $ref: '#/components/schemas/subscriptions_resource_payment_settings' + description: Payment settings passed on to invoices created by the subscription. + nullable: true + pending_invoice_item_interval: + anyOf: + - $ref: '#/components/schemas/subscription_pending_invoice_item_interval' + description: >- + Specifies an interval for how often to bill for any pending invoice + items. It is analogous to calling [Create an + invoice](https://stripe.com/docs/api#create_invoice) for the given + subscription at the specified interval. + nullable: true + pending_setup_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/setup_intent' + description: >- + You can use this + [SetupIntent](https://stripe.com/docs/api/setup_intents) to collect + user authentication when creating a subscription without immediate + payment or updating a subscription's payment method, allowing you to + optimize for off-session payments. Learn more in the [SCA Migration + Guide](https://stripe.com/docs/billing/migration/strong-customer-authentication#scenario-2). + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/setup_intent' + pending_update: + anyOf: + - $ref: '#/components/schemas/subscriptions_resource_pending_update' + description: >- + If specified, [pending + updates](https://stripe.com/docs/billing/subscriptions/pending-updates) + that will be applied to the subscription once the `latest_invoice` + has been paid. + nullable: true + schedule: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/subscription_schedule' + description: The schedule attached to the subscription + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/subscription_schedule' + start_date: + description: >- + Date when the subscription was first created. The date might differ + from the `created` date due to backdating. + format: unix-time + type: integer + status: + description: >- + Possible values are `incomplete`, `incomplete_expired`, `trialing`, + `active`, `past_due`, `canceled`, `unpaid`, or `paused`. + + + For `collection_method=charge_automatically` a subscription moves + into `incomplete` if the initial payment attempt fails. A + subscription in this status can only have metadata and + default_source updated. Once the first invoice is paid, the + subscription moves into an `active` status. If the first invoice is + not paid within 23 hours, the subscription transitions to + `incomplete_expired`. This is a terminal status, the open invoice + will be voided and no further invoices will be generated. + + + A subscription that is currently in a trial period is `trialing` and + moves to `active` when the trial period is over. + + + A subscription can only enter a `paused` status [when a trial ends + without a payment + method](https://stripe.com/docs/billing/subscriptions/trials#create-free-trials-without-payment). + A `paused` subscription doesn't generate invoices and can be resumed + after your customer adds their payment method. The `paused` status + is different from [pausing + collection](https://stripe.com/docs/billing/subscriptions/pause-payment), + which still generates invoices and leaves the subscription's status + unchanged. + + + If subscription `collection_method=charge_automatically`, it becomes + `past_due` when payment is required but cannot be paid (due to + failed payment or awaiting additional user actions). Once Stripe has + exhausted all payment retry attempts, the subscription will become + `canceled` or `unpaid` (depending on your subscriptions settings). + + + If subscription `collection_method=send_invoice` it becomes + `past_due` when its invoice is not paid by the due date, and + `canceled` or `unpaid` if it is still not paid by an additional + deadline after that. Note that when a subscription has a status of + `unpaid`, no subsequent invoices will be attempted (invoices will be + created, but then immediately automatically closed). After receiving + updated payment information from a customer, you may choose to + reopen and pay their closed invoices. + enum: + - active + - canceled + - incomplete + - incomplete_expired + - past_due + - paused + - trialing + - unpaid + type: string + test_clock: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/test_helpers.test_clock' + description: ID of the test clock this subscription belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/test_helpers.test_clock' + transfer_data: + anyOf: + - $ref: '#/components/schemas/subscription_transfer_data' + description: >- + The account (if any) the subscription's payments will be attributed + to for tax reporting, and where funds from each payment will be + transferred to for each of the subscription's invoices. + nullable: true + trial_end: + description: 'If the subscription has a trial, the end of that trial.' + format: unix-time + nullable: true + type: integer + trial_settings: + anyOf: + - $ref: >- + #/components/schemas/subscriptions_trials_resource_trial_settings + description: Settings related to subscription trials. + nullable: true + trial_start: + description: >- + If the subscription has a trial, the beginning of that trial. For + subsequent trials, this date remains as the start of the first ever + trial on the subscription. + format: unix-time + nullable: true + type: integer + required: + - automatic_tax + - billing_cycle_anchor + - cancel_at_period_end + - collection_method + - created + - currency + - customer + - discounts + - id + - invoice_settings + - items + - livemode + - metadata + - object + - start_date + - status + title: Subscription + type: object + x-expandableFields: + - application + - automatic_tax + - billing_cycle_anchor_config + - billing_thresholds + - cancellation_details + - customer + - default_payment_method + - default_source + - default_tax_rates + - discounts + - invoice_settings + - items + - latest_invoice + - on_behalf_of + - pause_collection + - payment_settings + - pending_invoice_item_interval + - pending_setup_intent + - pending_update + - schedule + - test_clock + - transfer_data + - trial_settings + x-resourceId: subscription + subscription_automatic_tax: + description: '' + properties: + disabled_reason: + description: 'If Stripe disabled automatic tax, this enum describes why.' + enum: + - requires_location_inputs + nullable: true + type: string + enabled: + description: Whether Stripe automatically computes tax on this subscription. + type: boolean + liability: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The account that's liable for tax. If set, the business address and + tax registrations required to perform the tax calculation are loaded + from this account. The tax transaction is returned in the report of + the connected account. + nullable: true + required: + - enabled + title: SubscriptionAutomaticTax + type: object + x-expandableFields: + - liability + subscription_billing_thresholds: + description: '' + properties: + amount_gte: + description: >- + Monetary threshold that triggers the subscription to create an + invoice + nullable: true + type: integer + reset_billing_cycle_anchor: + description: >- + Indicates if the `billing_cycle_anchor` should be reset when a + threshold is reached. If true, `billing_cycle_anchor` will be + updated to the date/time the threshold was last reached; otherwise, + the value will remain unchanged. This value may not be `true` if the + subscription contains items with plans that have + `aggregate_usage=last_ever`. + nullable: true + type: boolean + title: SubscriptionBillingThresholds + type: object + x-expandableFields: [] + subscription_item: + description: >- + Subscription items allow you to create customer subscriptions with more + than + + one plan, making it easy to represent complex billing relationships. + properties: + billing_thresholds: + anyOf: + - $ref: '#/components/schemas/subscription_item_billing_thresholds' + description: >- + Define thresholds at which an invoice will be sent, and the related + subscription advanced to a new billing period + nullable: true + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + type: integer + current_period_end: + description: The end time of this subscription item's current billing period. + format: unix-time + type: integer + current_period_start: + description: The start time of this subscription item's current billing period. + format: unix-time + type: integer + discounts: + description: >- + The discounts applied to the subscription item. Subscription item + discounts are applied before subscription discounts. Use + `expand[]=discounts` to expand each discount. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/discount' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/discount' + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - subscription_item + type: string + price: + $ref: '#/components/schemas/price' + quantity: + description: >- + The [quantity](https://stripe.com/docs/subscriptions/quantities) of + the plan to which the customer should be subscribed. + type: integer + subscription: + description: The `subscription` this `subscription_item` belongs to. + maxLength: 5000 + type: string + tax_rates: + description: >- + The tax rates which apply to this `subscription_item`. When set, the + `default_tax_rates` on the subscription do not apply to this + `subscription_item`. + items: + $ref: '#/components/schemas/tax_rate' + nullable: true + type: array + required: + - created + - current_period_end + - current_period_start + - discounts + - id + - metadata + - object + - price + - subscription + title: SubscriptionItem + type: object + x-expandableFields: + - billing_thresholds + - discounts + - price + - tax_rates + x-resourceId: subscription_item + subscription_item_billing_thresholds: + description: '' + properties: + usage_gte: + description: Usage threshold that triggers the subscription to create an invoice + nullable: true + type: integer + title: SubscriptionItemBillingThresholds + type: object + x-expandableFields: [] + subscription_payment_method_options_card: + description: '' + properties: + mandate_options: + $ref: '#/components/schemas/invoice_mandate_options_card' + network: + description: >- + Selected network to process this Subscription on. Depends on the + available networks of the card attached to the Subscription. Can be + only set confirm-time. + enum: + - amex + - cartes_bancaires + - diners + - discover + - eftpos_au + - girocard + - interac + - jcb + - link + - mastercard + - unionpay + - unknown + - visa + nullable: true + type: string + request_three_d_secure: + description: >- + We strongly recommend that you rely on our SCA Engine to + automatically prompt your customers for authentication based on risk + level and [other + requirements](https://stripe.com/docs/strong-customer-authentication). + However, if you wish to request 3D Secure based on logic from your + own fraud engine, provide this option. Read our guide on [manually + requesting 3D + Secure](https://stripe.com/docs/payments/3d-secure/authentication-flow#manual-three-ds) + for more information on how this configuration interacts with Radar + and our SCA Engine. + enum: + - any + - automatic + - challenge + nullable: true + type: string + title: subscription_payment_method_options_card + type: object + x-expandableFields: + - mandate_options + subscription_pending_invoice_item_interval: + description: '' + properties: + interval: + description: >- + Specifies invoicing frequency. Either `day`, `week`, `month` or + `year`. + enum: + - day + - month + - week + - year + type: string + interval_count: + description: >- + The number of intervals between invoices. For example, + `interval=month` and `interval_count=3` bills every 3 months. + Maximum of one year interval allowed (1 year, 12 months, or 52 + weeks). + type: integer + required: + - interval + - interval_count + title: SubscriptionPendingInvoiceItemInterval + type: object + x-expandableFields: [] + subscription_schedule: + description: >- + A subscription schedule allows you to create and manage the lifecycle of + a subscription by predefining expected changes. + + + Related guide: [Subscription + schedules](https://stripe.com/docs/billing/subscriptions/subscription-schedules) + properties: + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + description: ID of the Connect Application that created the schedule. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + - $ref: '#/components/schemas/deleted_application' + canceled_at: + description: >- + Time at which the subscription schedule was canceled. Measured in + seconds since the Unix epoch. + format: unix-time + nullable: true + type: integer + completed_at: + description: >- + Time at which the subscription schedule was completed. Measured in + seconds since the Unix epoch. + format: unix-time + nullable: true + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + current_phase: + anyOf: + - $ref: '#/components/schemas/subscription_schedule_current_phase' + description: >- + Object representing the start and end dates for the current phase of + the subscription schedule, if it is `active`. + nullable: true + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + description: ID of the customer who owns the subscription schedule. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + - $ref: '#/components/schemas/deleted_customer' + default_settings: + $ref: >- + #/components/schemas/subscription_schedules_resource_default_settings + end_behavior: + description: >- + Behavior of the subscription schedule and underlying subscription + when it ends. Possible values are `release` or `cancel` with the + default being `release`. `release` will end the subscription + schedule and keep the underlying subscription running. `cancel` will + end the subscription schedule and cancel the underlying + subscription. + enum: + - cancel + - none + - release + - renew + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - subscription_schedule + type: string + phases: + description: Configuration for the subscription schedule's phases. + items: + $ref: '#/components/schemas/subscription_schedule_phase_configuration' + type: array + released_at: + description: >- + Time at which the subscription schedule was released. Measured in + seconds since the Unix epoch. + format: unix-time + nullable: true + type: integer + released_subscription: + description: >- + ID of the subscription once managed by the subscription schedule (if + it is released). + maxLength: 5000 + nullable: true + type: string + status: + description: >- + The present status of the subscription schedule. Possible values are + `not_started`, `active`, `completed`, `released`, and `canceled`. + You can read more about the different states in our [behavior + guide](https://stripe.com/docs/billing/subscriptions/subscription-schedules). + enum: + - active + - canceled + - completed + - not_started + - released + type: string + subscription: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/subscription' + description: ID of the subscription managed by the subscription schedule. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/subscription' + test_clock: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/test_helpers.test_clock' + description: ID of the test clock this subscription schedule belongs to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/test_helpers.test_clock' + required: + - created + - customer + - default_settings + - end_behavior + - id + - livemode + - object + - phases + - status + title: SubscriptionSchedule + type: object + x-expandableFields: + - application + - current_phase + - customer + - default_settings + - phases + - subscription + - test_clock + x-resourceId: subscription_schedule + subscription_schedule_add_invoice_item: + description: >- + An Add Invoice Item describes the prices and quantities that will be + added as pending invoice items when entering a phase. + properties: + discounts: + description: The stackable discounts that will be applied to the item. + items: + $ref: '#/components/schemas/discounts_resource_stackable_discount' + type: array + price: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/price' + - $ref: '#/components/schemas/deleted_price' + description: ID of the price used to generate the invoice item. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/price' + - $ref: '#/components/schemas/deleted_price' + quantity: + description: The quantity of the invoice item. + nullable: true + type: integer + tax_rates: + description: >- + The tax rates which apply to the item. When set, the + `default_tax_rates` do not apply to this item. + items: + $ref: '#/components/schemas/tax_rate' + nullable: true + type: array + required: + - discounts + - price + title: SubscriptionScheduleAddInvoiceItem + type: object + x-expandableFields: + - discounts + - price + - tax_rates + subscription_schedule_configuration_item: + description: A phase item describes the price and quantity of a phase. + properties: + billing_thresholds: + anyOf: + - $ref: '#/components/schemas/subscription_item_billing_thresholds' + description: >- + Define thresholds at which an invoice will be sent, and the related + subscription advanced to a new billing period + nullable: true + discounts: + description: >- + The discounts applied to the subscription item. Subscription item + discounts are applied before subscription discounts. Use + `expand[]=discounts` to expand each discount. + items: + $ref: '#/components/schemas/discounts_resource_stackable_discount' + type: array + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an item. Metadata on this item will update the + underlying subscription item's `metadata` when the phase is entered. + nullable: true + type: object + price: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/price' + - $ref: '#/components/schemas/deleted_price' + description: ID of the price to which the customer should be subscribed. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/price' + - $ref: '#/components/schemas/deleted_price' + quantity: + description: Quantity of the plan to which the customer should be subscribed. + type: integer + tax_rates: + description: >- + The tax rates which apply to this `phase_item`. When set, the + `default_tax_rates` on the phase do not apply to this `phase_item`. + items: + $ref: '#/components/schemas/tax_rate' + nullable: true + type: array + required: + - discounts + - price + title: SubscriptionScheduleConfigurationItem + type: object + x-expandableFields: + - billing_thresholds + - discounts + - price + - tax_rates + subscription_schedule_current_phase: + description: '' + properties: + end_date: + description: The end of this phase of the subscription schedule. + format: unix-time + type: integer + start_date: + description: The start of this phase of the subscription schedule. + format: unix-time + type: integer + required: + - end_date + - start_date + title: SubscriptionScheduleCurrentPhase + type: object + x-expandableFields: [] + subscription_schedule_phase_configuration: + description: >- + A phase describes the plans, coupon, and trialing status of a + subscription for a predefined time period. + properties: + add_invoice_items: + description: >- + A list of prices and quantities that will generate invoice items + appended to the next invoice for this phase. + items: + $ref: '#/components/schemas/subscription_schedule_add_invoice_item' + type: array + application_fee_percent: + description: >- + A non-negative decimal between 0 and 100, with at most two decimal + places. This represents the percentage of the subscription invoice + total that will be transferred to the application owner's Stripe + account during this phase of the schedule. + nullable: true + type: number + automatic_tax: + $ref: '#/components/schemas/schedules_phase_automatic_tax' + billing_cycle_anchor: + description: >- + Possible values are `phase_start` or `automatic`. If `phase_start` + then billing cycle anchor of the subscription is set to the start of + the phase when entering the phase. If `automatic` then the billing + cycle anchor is automatically modified as needed when entering the + phase. For more information, see the billing cycle + [documentation](https://stripe.com/docs/billing/subscriptions/billing-cycle). + enum: + - automatic + - phase_start + nullable: true + type: string + billing_thresholds: + anyOf: + - $ref: '#/components/schemas/subscription_billing_thresholds' + description: >- + Define thresholds at which an invoice will be sent, and the + subscription advanced to a new billing period + nullable: true + collection_method: + description: >- + Either `charge_automatically`, or `send_invoice`. When charging + automatically, Stripe will attempt to pay the underlying + subscription at the end of each billing cycle using the default + source attached to the customer. When sending an invoice, Stripe + will email your customer an invoice with payment instructions and + mark the subscription as `active`. + enum: + - charge_automatically + - send_invoice + nullable: true + type: string + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + default_payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + ID of the default payment method for the subscription schedule. It + must belong to the customer associated with the subscription + schedule. If not set, invoices will use the default payment method + in the customer's invoice settings. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + default_tax_rates: + description: >- + The default tax rates to apply to the subscription during this phase + of the subscription schedule. + items: + $ref: '#/components/schemas/tax_rate' + nullable: true + type: array + description: + description: >- + Subscription description, meant to be displayable to the customer. + Use this field to optionally store an explanation of the + subscription for rendering in Stripe surfaces and certain local + payment methods UIs. + maxLength: 5000 + nullable: true + type: string + discounts: + description: >- + The stackable discounts that will be applied to the subscription on + this phase. Subscription item discounts are applied before + subscription discounts. + items: + $ref: '#/components/schemas/discounts_resource_stackable_discount' + type: array + end_date: + description: The end of this phase of the subscription schedule. + format: unix-time + type: integer + invoice_settings: + anyOf: + - $ref: >- + #/components/schemas/invoice_setting_subscription_schedule_phase_setting + description: The invoice settings applicable during this phase. + nullable: true + items: + description: >- + Subscription items to configure the subscription to during this + phase of the subscription schedule. + items: + $ref: '#/components/schemas/subscription_schedule_configuration_item' + type: array + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to a phase. Metadata on a schedule's phase will + update the underlying subscription's `metadata` when the phase is + entered. Updating the underlying subscription's `metadata` directly + will not affect the current phase's `metadata`. + nullable: true + type: object + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account (if any) the charge was made on behalf of for charges + associated with the schedule's subscription. See the Connect + documentation for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + proration_behavior: + description: >- + When transitioning phases, controls how prorations are handled (if + any). Possible values are `create_prorations`, `none`, and + `always_invoice`. + enum: + - always_invoice + - create_prorations + - none + type: string + start_date: + description: The start of this phase of the subscription schedule. + format: unix-time + type: integer + transfer_data: + anyOf: + - $ref: '#/components/schemas/subscription_transfer_data' + description: >- + The account (if any) the associated subscription's payments will be + attributed to for tax reporting, and where funds from each payment + will be transferred to for each of the subscription's invoices. + nullable: true + trial_end: + description: When the trial ends within the phase. + format: unix-time + nullable: true + type: integer + required: + - add_invoice_items + - currency + - discounts + - end_date + - items + - proration_behavior + - start_date + title: SubscriptionSchedulePhaseConfiguration + type: object + x-expandableFields: + - add_invoice_items + - automatic_tax + - billing_thresholds + - default_payment_method + - default_tax_rates + - discounts + - invoice_settings + - items + - on_behalf_of + - transfer_data + subscription_schedules_resource_default_settings: + description: '' + properties: + application_fee_percent: + description: >- + A non-negative decimal between 0 and 100, with at most two decimal + places. This represents the percentage of the subscription invoice + total that will be transferred to the application owner's Stripe + account during this phase of the schedule. + nullable: true + type: number + automatic_tax: + $ref: >- + #/components/schemas/subscription_schedules_resource_default_settings_automatic_tax + billing_cycle_anchor: + description: >- + Possible values are `phase_start` or `automatic`. If `phase_start` + then billing cycle anchor of the subscription is set to the start of + the phase when entering the phase. If `automatic` then the billing + cycle anchor is automatically modified as needed when entering the + phase. For more information, see the billing cycle + [documentation](https://stripe.com/docs/billing/subscriptions/billing-cycle). + enum: + - automatic + - phase_start + type: string + billing_thresholds: + anyOf: + - $ref: '#/components/schemas/subscription_billing_thresholds' + description: >- + Define thresholds at which an invoice will be sent, and the + subscription advanced to a new billing period + nullable: true + collection_method: + description: >- + Either `charge_automatically`, or `send_invoice`. When charging + automatically, Stripe will attempt to pay the underlying + subscription at the end of each billing cycle using the default + source attached to the customer. When sending an invoice, Stripe + will email your customer an invoice with payment instructions and + mark the subscription as `active`. + enum: + - charge_automatically + - send_invoice + nullable: true + type: string + default_payment_method: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_method' + description: >- + ID of the default payment method for the subscription schedule. If + not set, invoices will use the default payment method in the + customer's invoice settings. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_method' + description: + description: >- + Subscription description, meant to be displayable to the customer. + Use this field to optionally store an explanation of the + subscription for rendering in Stripe surfaces and certain local + payment methods UIs. + maxLength: 5000 + nullable: true + type: string + invoice_settings: + $ref: '#/components/schemas/invoice_setting_subscription_schedule_setting' + on_behalf_of: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account (if any) the charge was made on behalf of for charges + associated with the schedule's subscription. See the Connect + documentation for details. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + transfer_data: + anyOf: + - $ref: '#/components/schemas/subscription_transfer_data' + description: >- + The account (if any) the associated subscription's payments will be + attributed to for tax reporting, and where funds from each payment + will be transferred to for each of the subscription's invoices. + nullable: true + required: + - billing_cycle_anchor + - invoice_settings + title: SubscriptionSchedulesResourceDefaultSettings + type: object + x-expandableFields: + - automatic_tax + - billing_thresholds + - default_payment_method + - invoice_settings + - on_behalf_of + - transfer_data + subscription_schedules_resource_default_settings_automatic_tax: + description: '' + properties: + disabled_reason: + description: 'If Stripe disabled automatic tax, this enum describes why.' + enum: + - requires_location_inputs + nullable: true + type: string + enabled: + description: >- + Whether Stripe automatically computes tax on invoices created during + this phase. + type: boolean + liability: + anyOf: + - $ref: '#/components/schemas/connect_account_reference' + description: >- + The account that's liable for tax. If set, the business address and + tax registrations required to perform the tax calculation are loaded + from this account. The tax transaction is returned in the report of + the connected account. + nullable: true + required: + - enabled + title: SubscriptionSchedulesResourceDefaultSettingsAutomaticTax + type: object + x-expandableFields: + - liability + subscription_transfer_data: + description: '' + properties: + amount_percent: + description: >- + A non-negative decimal between 0 and 100, with at most two decimal + places. This represents the percentage of the subscription invoice + total that will be transferred to the destination account. By + default, the entire amount is transferred to the destination. + nullable: true + type: number + destination: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account where funds from the payment will be transferred to upon + payment success. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + required: + - destination + title: SubscriptionTransferData + type: object + x-expandableFields: + - destination + subscriptions_resource_billing_cycle_anchor_config: + description: '' + properties: + day_of_month: + description: The day of the month of the billing_cycle_anchor. + type: integer + hour: + description: The hour of the day of the billing_cycle_anchor. + nullable: true + type: integer + minute: + description: The minute of the hour of the billing_cycle_anchor. + nullable: true + type: integer + month: + description: The month to start full cycle billing periods. + nullable: true + type: integer + second: + description: The second of the minute of the billing_cycle_anchor. + nullable: true + type: integer + required: + - day_of_month + title: SubscriptionsResourceBillingCycleAnchorConfig + type: object + x-expandableFields: [] + subscriptions_resource_pause_collection: + description: >- + The Pause Collection settings determine how we will pause collection for + this subscription and for how long the subscription + + should be paused. + properties: + behavior: + description: >- + The payment collection behavior for this subscription while paused. + One of `keep_as_draft`, `mark_uncollectible`, or `void`. + enum: + - keep_as_draft + - mark_uncollectible + - void + type: string + resumes_at: + description: >- + The time after which the subscription will resume collecting + payments. + format: unix-time + nullable: true + type: integer + required: + - behavior + title: SubscriptionsResourcePauseCollection + type: object + x-expandableFields: [] + subscriptions_resource_payment_method_options: + description: '' + properties: + acss_debit: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_acss_debit' + description: >- + This sub-hash contains details about the Canadian pre-authorized + debit payment method options to pass to invoices created by the + subscription. + nullable: true + bancontact: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_bancontact' + description: >- + This sub-hash contains details about the Bancontact payment method + options to pass to invoices created by the subscription. + nullable: true + card: + anyOf: + - $ref: '#/components/schemas/subscription_payment_method_options_card' + description: >- + This sub-hash contains details about the Card payment method options + to pass to invoices created by the subscription. + nullable: true + customer_balance: + anyOf: + - $ref: >- + #/components/schemas/invoice_payment_method_options_customer_balance + description: >- + This sub-hash contains details about the Bank transfer payment + method options to pass to invoices created by the subscription. + nullable: true + konbini: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_konbini' + description: >- + This sub-hash contains details about the Konbini payment method + options to pass to invoices created by the subscription. + nullable: true + sepa_debit: + anyOf: + - $ref: '#/components/schemas/invoice_payment_method_options_sepa_debit' + description: >- + This sub-hash contains details about the SEPA Direct Debit payment + method options to pass to invoices created by the subscription. + nullable: true + us_bank_account: + anyOf: + - $ref: >- + #/components/schemas/invoice_payment_method_options_us_bank_account + description: >- + This sub-hash contains details about the ACH direct debit payment + method options to pass to invoices created by the subscription. + nullable: true + title: SubscriptionsResourcePaymentMethodOptions + type: object + x-expandableFields: + - acss_debit + - bancontact + - card + - customer_balance + - konbini + - sepa_debit + - us_bank_account + subscriptions_resource_payment_settings: + description: '' + properties: + payment_method_options: + anyOf: + - $ref: >- + #/components/schemas/subscriptions_resource_payment_method_options + description: >- + Payment-method-specific configuration to provide to invoices created + by the subscription. + nullable: true + payment_method_types: + description: >- + The list of payment method types to provide to every invoice created + by the subscription. If not set, Stripe attempts to automatically + determine the types to use by looking at the invoice’s default + payment method, the subscription’s default payment method, the + customer’s default payment method, and your [invoice template + settings](https://dashboard.stripe.com/settings/billing/invoice). + items: + enum: + - ach_credit_transfer + - ach_debit + - acss_debit + - affirm + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - boleto + - card + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - jp_credit_transfer + - kakao_pay + - klarna + - konbini + - kr_card + - link + - multibanco + - naver_pay + - nz_bank_account + - p24 + - payco + - paynow + - paypal + - promptpay + - revolut_pay + - sepa_credit_transfer + - sepa_debit + - sofort + - swish + - us_bank_account + - wechat_pay + type: string + x-stripeBypassValidation: true + nullable: true + type: array + save_default_payment_method: + description: >- + Configure whether Stripe updates + `subscription.default_payment_method` when payment succeeds. + Defaults to `off`. + enum: + - 'off' + - on_subscription + nullable: true + type: string + title: SubscriptionsResourcePaymentSettings + type: object + x-expandableFields: + - payment_method_options + subscriptions_resource_pending_update: + description: >- + Pending Updates store the changes pending from a previous update that + will be applied + + to the Subscription upon successful payment. + properties: + billing_cycle_anchor: + description: >- + If the update is applied, determines the date of the first full + invoice, and, for plans with `month` or `year` intervals, the day of + the month for subsequent invoices. The timestamp is in UTC format. + format: unix-time + nullable: true + type: integer + expires_at: + description: >- + The point after which the changes reflected by this update will be + discarded and no longer applied. + format: unix-time + type: integer + subscription_items: + description: >- + List of subscription items, each with an attached plan, that will be + set if the update is applied. + items: + $ref: '#/components/schemas/subscription_item' + nullable: true + type: array + trial_end: + description: >- + Unix timestamp representing the end of the trial period the customer + will get before being charged for the first time, if the update is + applied. + format: unix-time + nullable: true + type: integer + trial_from_plan: + description: >- + Indicates if a plan's `trial_period_days` should be applied to the + subscription. Setting `trial_end` per subscription is preferred, and + this defaults to `false`. Setting this flag to `true` together with + `trial_end` is not allowed. See [Using trial periods on + subscriptions](https://stripe.com/docs/billing/subscriptions/trials) + to learn more. + nullable: true + type: boolean + required: + - expires_at + title: SubscriptionsResourcePendingUpdate + type: object + x-expandableFields: + - subscription_items + subscriptions_resource_subscription_invoice_settings: + description: '' + properties: + account_tax_ids: + description: >- + The account tax IDs associated with the subscription. Will be set on + invoices generated by the subscription. + items: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/tax_id' + - $ref: '#/components/schemas/deleted_tax_id' + nullable: true + type: array + issuer: + $ref: '#/components/schemas/connect_account_reference' + required: + - issuer + title: SubscriptionsResourceSubscriptionInvoiceSettings + type: object + x-expandableFields: + - account_tax_ids + - issuer + subscriptions_trials_resource_end_behavior: + description: Defines how a subscription behaves when a free trial ends. + properties: + missing_payment_method: + description: >- + Indicates how the subscription should change when the trial ends if + the user did not provide a payment method. + enum: + - cancel + - create_invoice + - pause + type: string + required: + - missing_payment_method + title: SubscriptionsTrialsResourceEndBehavior + type: object + x-expandableFields: [] + subscriptions_trials_resource_trial_settings: + description: Configures how this subscription behaves during the trial period. + properties: + end_behavior: + $ref: '#/components/schemas/subscriptions_trials_resource_end_behavior' + required: + - end_behavior + title: SubscriptionsTrialsResourceTrialSettings + type: object + x-expandableFields: + - end_behavior + tax.calculation: + description: >- + A Tax Calculation allows you to calculate the tax to collect from your + customer. + + + Related guide: [Calculate tax in your custom payment + flow](https://stripe.com/docs/tax/custom) + properties: + amount_total: + description: >- + Total amount after taxes in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + customer: + description: >- + The ID of an existing + [Customer](https://stripe.com/docs/api/customers/object) used for + the resource. + maxLength: 5000 + nullable: true + type: string + customer_details: + $ref: '#/components/schemas/tax_product_resource_customer_details' + expires_at: + description: Timestamp of date at which the tax calculation will expire. + format: unix-time + nullable: true + type: integer + id: + description: Unique identifier for the calculation. + maxLength: 5000 + nullable: true + type: string + line_items: + description: The list of items the customer is purchasing. + nullable: true + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/tax.calculation_line_item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: '^/v1/tax/calculations/[^/]+/line_items' + type: string + required: + - data + - has_more + - object + - url + title: TaxProductResourceTaxCalculationLineItemList + type: object + x-expandableFields: + - data + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax.calculation + type: string + ship_from_details: + anyOf: + - $ref: '#/components/schemas/tax_product_resource_ship_from_details' + description: 'The details of the ship from location, such as the address.' + nullable: true + shipping_cost: + anyOf: + - $ref: >- + #/components/schemas/tax_product_resource_tax_calculation_shipping_cost + description: The shipping cost details for the calculation. + nullable: true + tax_amount_exclusive: + description: The amount of tax to be collected on top of the line item prices. + type: integer + tax_amount_inclusive: + description: The amount of tax already included in the line item prices. + type: integer + tax_breakdown: + description: Breakdown of individual tax amounts that add up to the total. + items: + $ref: '#/components/schemas/tax_product_resource_tax_breakdown' + type: array + tax_date: + description: >- + Timestamp of date at which the tax rules and rates in effect applies + for the calculation. + format: unix-time + type: integer + required: + - amount_total + - currency + - customer_details + - livemode + - object + - tax_amount_exclusive + - tax_amount_inclusive + - tax_breakdown + - tax_date + title: TaxProductResourceTaxCalculation + type: object + x-expandableFields: + - customer_details + - line_items + - ship_from_details + - shipping_cost + - tax_breakdown + x-resourceId: tax.calculation + tax.calculation_line_item: + description: '' + properties: + amount: + description: >- + The line item amount in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). If + `tax_behavior=inclusive`, then this amount includes taxes. + Otherwise, taxes were calculated on top of this amount. + type: integer + amount_tax: + description: >- + The amount of tax calculated for this line item, in the [smallest + currency unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax.calculation_line_item + type: string + product: + description: >- + The ID of an existing + [Product](https://stripe.com/docs/api/products/object). + maxLength: 5000 + nullable: true + type: string + quantity: + description: >- + The number of units of the item being purchased. For reversals, this + is the quantity reversed. + type: integer + reference: + description: A custom identifier for this line item. + maxLength: 5000 + type: string + tax_behavior: + description: >- + Specifies whether the `amount` includes taxes. If + `tax_behavior=inclusive`, then the amount includes taxes. + enum: + - exclusive + - inclusive + type: string + tax_breakdown: + description: Detailed account of taxes relevant to this line item. + items: + $ref: '#/components/schemas/tax_product_resource_line_item_tax_breakdown' + nullable: true + type: array + tax_code: + description: >- + The [tax code](https://stripe.com/docs/tax/tax-categories) ID used + for this resource. + maxLength: 5000 + type: string + required: + - amount + - amount_tax + - id + - livemode + - object + - quantity + - reference + - tax_behavior + - tax_code + title: TaxProductResourceTaxCalculationLineItem + type: object + x-expandableFields: + - tax_breakdown + x-resourceId: tax.calculation_line_item + tax.registration: + description: >- + A Tax `Registration` lets us know that your business is registered to + collect tax on payments within a region, enabling you to [automatically + collect tax](https://stripe.com/docs/tax). + + + Stripe doesn't register on your behalf with the relevant authorities + when you create a Tax `Registration` object. For more information on how + to register to collect tax, see [our + guide](https://stripe.com/docs/tax/registering). + + + Related guide: [Using the Registrations + API](https://stripe.com/docs/tax/registrations-api) + properties: + active_from: + description: >- + Time at which the registration becomes active. Measured in seconds + since the Unix epoch. + format: unix-time + type: integer + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + type: string + country_options: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + expires_at: + description: >- + If set, the registration stops being active at this time. If not + set, the registration will be active indefinitely. Measured in + seconds since the Unix epoch. + format: unix-time + nullable: true + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax.registration + type: string + status: + description: >- + The status of the registration. This field is present for + convenience and can be deduced from `active_from` and `expires_at`. + enum: + - active + - expired + - scheduled + type: string + required: + - active_from + - country + - country_options + - created + - id + - livemode + - object + - status + title: TaxProductRegistrationsResourceTaxRegistration + type: object + x-expandableFields: + - country_options + x-resourceId: tax.registration + tax.settings: + description: >- + You can use Tax `Settings` to manage configurations used by Stripe Tax + calculations. + + + Related guide: [Using the Settings + API](https://stripe.com/docs/tax/settings-api) + properties: + defaults: + $ref: '#/components/schemas/tax_product_resource_tax_settings_defaults' + head_office: + anyOf: + - $ref: >- + #/components/schemas/tax_product_resource_tax_settings_head_office + description: The place where your business is located. + nullable: true + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax.settings + type: string + status: + description: The status of the Tax `Settings`. + enum: + - active + - pending + type: string + status_details: + $ref: >- + #/components/schemas/tax_product_resource_tax_settings_status_details + required: + - defaults + - livemode + - object + - status + - status_details + title: TaxProductResourceTaxSettings + type: object + x-expandableFields: + - defaults + - head_office + - status_details + x-resourceId: tax.settings + tax.transaction: + description: >- + A Tax Transaction records the tax collected from or refunded to your + customer. + + + Related guide: [Calculate tax in your custom payment + flow](https://stripe.com/docs/tax/custom#tax-transaction) + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + customer: + description: >- + The ID of an existing + [Customer](https://stripe.com/docs/api/customers/object) used for + the resource. + maxLength: 5000 + nullable: true + type: string + customer_details: + $ref: '#/components/schemas/tax_product_resource_customer_details' + id: + description: Unique identifier for the transaction. + maxLength: 5000 + type: string + line_items: + description: 'The tax collected or refunded, by line item.' + nullable: true + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/tax.transaction_line_item' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: '^/v1/tax/transactions/[^/]+/line_items' + type: string + required: + - data + - has_more + - object + - url + title: TaxProductResourceTaxTransactionLineItemList + type: object + x-expandableFields: + - data + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax.transaction + type: string + posted_at: + description: >- + The Unix timestamp representing when the tax liability is assumed or + reduced. + format: unix-time + type: integer + reference: + description: 'A custom unique identifier, such as ''myOrder_123''.' + maxLength: 5000 + type: string + reversal: + anyOf: + - $ref: >- + #/components/schemas/tax_product_resource_tax_transaction_resource_reversal + description: 'If `type=reversal`, contains information about what was reversed.' + nullable: true + ship_from_details: + anyOf: + - $ref: '#/components/schemas/tax_product_resource_ship_from_details' + description: 'The details of the ship from location, such as the address.' + nullable: true + shipping_cost: + anyOf: + - $ref: >- + #/components/schemas/tax_product_resource_tax_transaction_shipping_cost + description: The shipping cost details for the transaction. + nullable: true + tax_date: + description: >- + Timestamp of date at which the tax rules and rates in effect applies + for the calculation. + format: unix-time + type: integer + type: + description: 'If `reversal`, this transaction reverses an earlier transaction.' + enum: + - reversal + - transaction + type: string + required: + - created + - currency + - customer_details + - id + - livemode + - object + - posted_at + - reference + - tax_date + - type + title: TaxProductResourceTaxTransaction + type: object + x-expandableFields: + - customer_details + - line_items + - reversal + - ship_from_details + - shipping_cost + x-resourceId: tax.transaction + tax.transaction_line_item: + description: '' + properties: + amount: + description: >- + The line item amount in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). If + `tax_behavior=inclusive`, then this amount includes taxes. + Otherwise, taxes were calculated on top of this amount. + type: integer + amount_tax: + description: >- + The amount of tax calculated for this line item, in the [smallest + currency unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax.transaction_line_item + type: string + product: + description: >- + The ID of an existing + [Product](https://stripe.com/docs/api/products/object). + maxLength: 5000 + nullable: true + type: string + quantity: + description: >- + The number of units of the item being purchased. For reversals, this + is the quantity reversed. + type: integer + reference: + description: A custom identifier for this line item in the transaction. + maxLength: 5000 + type: string + reversal: + anyOf: + - $ref: >- + #/components/schemas/tax_product_resource_tax_transaction_line_item_resource_reversal + description: 'If `type=reversal`, contains information about what was reversed.' + nullable: true + tax_behavior: + description: >- + Specifies whether the `amount` includes taxes. If + `tax_behavior=inclusive`, then the amount includes taxes. + enum: + - exclusive + - inclusive + type: string + tax_code: + description: >- + The [tax code](https://stripe.com/docs/tax/tax-categories) ID used + for this resource. + maxLength: 5000 + type: string + type: + description: 'If `reversal`, this line item reverses an earlier transaction.' + enum: + - reversal + - transaction + type: string + required: + - amount + - amount_tax + - id + - livemode + - object + - quantity + - reference + - tax_behavior + - tax_code + - type + title: TaxProductResourceTaxTransactionLineItem + type: object + x-expandableFields: + - reversal + x-resourceId: tax.transaction_line_item + tax_code: + description: >- + [Tax codes](https://stripe.com/docs/tax/tax-categories) classify goods + and services for tax purposes. + properties: + description: + description: >- + A detailed description of which types of products the tax code + represents. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + name: + description: A short name for the tax code. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax_code + type: string + required: + - description + - id + - name + - object + title: TaxProductResourceTaxCode + type: object + x-expandableFields: [] + x-resourceId: tax_code + tax_deducted_at_source: + description: '' + properties: + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax_deducted_at_source + type: string + period_end: + description: >- + The end of the invoicing period. This TDS applies to Stripe fees + collected during this invoicing period. + format: unix-time + type: integer + period_start: + description: >- + The start of the invoicing period. This TDS applies to Stripe fees + collected during this invoicing period. + format: unix-time + type: integer + tax_deduction_account_number: + description: The TAN that was supplied to Stripe when TDS was assessed + maxLength: 5000 + type: string + required: + - id + - object + - period_end + - period_start + - tax_deduction_account_number + title: TaxDeductedAtSource + type: object + x-expandableFields: [] + tax_i_ds_owner: + description: '' + properties: + account: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: The account being referenced when `type` is `account`. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + application: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/application' + description: >- + The Connect Application being referenced when `type` is + `application`. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/application' + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: The customer being referenced when `type` is `customer`. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + type: + description: Type of owner referenced. + enum: + - account + - application + - customer + - self + type: string + required: + - type + title: TaxIDsOwner + type: object + x-expandableFields: + - account + - application + - customer + tax_id: + description: >- + You can add one or multiple tax IDs to a + [customer](https://stripe.com/docs/api/customers) or account. + + Customer and account tax IDs get displayed on related invoices and + credit notes. + + + Related guides: [Customer tax identification + numbers](https://stripe.com/docs/billing/taxes/tax-ids), [Account tax + IDs](https://stripe.com/docs/invoicing/connect#account-tax-ids) + properties: + country: + description: Two-letter ISO code representing the country of the tax ID. + maxLength: 5000 + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: ID of the customer. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax_id + type: string + owner: + anyOf: + - $ref: '#/components/schemas/tax_i_ds_owner' + description: The account or customer the tax ID belongs to. + nullable: true + type: + description: >- + Type of the tax ID, one of `ad_nrt`, `ae_trn`, `al_tin`, `am_tin`, + `ao_tin`, `ar_cuit`, `au_abn`, `au_arn`, `aw_tin`, `az_tin`, + `ba_tin`, `bb_tin`, `bd_bin`, `bf_ifu`, `bg_uic`, `bh_vat`, + `bj_ifu`, `bo_tin`, `br_cnpj`, `br_cpf`, `bs_tin`, `by_tin`, + `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, + `ca_qst`, `cd_nif`, `ch_uid`, `ch_vat`, `cl_tin`, `cm_niu`, + `cn_tin`, `co_nit`, `cr_tin`, `cv_nif`, `de_stn`, `do_rcn`, + `ec_ruc`, `eg_tin`, `es_cif`, `et_tin`, `eu_oss_vat`, `eu_vat`, + `gb_vat`, `ge_vat`, `gn_nif`, `hk_br`, `hr_oib`, `hu_tin`, + `id_npwp`, `il_vat`, `in_gst`, `is_vat`, `jp_cn`, `jp_rn`, `jp_trn`, + `ke_pin`, `kg_tin`, `kh_tin`, `kr_brn`, `kz_bin`, `la_tin`, + `li_uid`, `li_vat`, `ma_vat`, `md_vat`, `me_pib`, `mk_vat`, + `mr_nif`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `ng_tin`, + `no_vat`, `no_voec`, `np_pan`, `nz_gst`, `om_vat`, `pe_ruc`, + `ph_tin`, `ro_tin`, `rs_pib`, `ru_inn`, `ru_kpp`, `sa_vat`, + `sg_gst`, `sg_uen`, `si_tin`, `sn_ninea`, `sr_fin`, `sv_nit`, + `th_vat`, `tj_tin`, `tr_tin`, `tw_vat`, `tz_vat`, `ua_vat`, + `ug_tin`, `us_ein`, `uy_ruc`, `uz_tin`, `uz_vat`, `ve_rif`, + `vn_tin`, `za_vat`, `zm_tin`, or `zw_tin`. Note that some legacy tax + IDs have type `unknown` + enum: + - ad_nrt + - ae_trn + - al_tin + - am_tin + - ao_tin + - ar_cuit + - au_abn + - au_arn + - aw_tin + - az_tin + - ba_tin + - bb_tin + - bd_bin + - bf_ifu + - bg_uic + - bh_vat + - bj_ifu + - bo_tin + - br_cnpj + - br_cpf + - bs_tin + - by_tin + - ca_bn + - ca_gst_hst + - ca_pst_bc + - ca_pst_mb + - ca_pst_sk + - ca_qst + - cd_nif + - ch_uid + - ch_vat + - cl_tin + - cm_niu + - cn_tin + - co_nit + - cr_tin + - cv_nif + - de_stn + - do_rcn + - ec_ruc + - eg_tin + - es_cif + - et_tin + - eu_oss_vat + - eu_vat + - gb_vat + - ge_vat + - gn_nif + - hk_br + - hr_oib + - hu_tin + - id_npwp + - il_vat + - in_gst + - is_vat + - jp_cn + - jp_rn + - jp_trn + - ke_pin + - kg_tin + - kh_tin + - kr_brn + - kz_bin + - la_tin + - li_uid + - li_vat + - ma_vat + - md_vat + - me_pib + - mk_vat + - mr_nif + - mx_rfc + - my_frp + - my_itn + - my_sst + - ng_tin + - no_vat + - no_voec + - np_pan + - nz_gst + - om_vat + - pe_ruc + - ph_tin + - ro_tin + - rs_pib + - ru_inn + - ru_kpp + - sa_vat + - sg_gst + - sg_uen + - si_tin + - sn_ninea + - sr_fin + - sv_nit + - th_vat + - tj_tin + - tr_tin + - tw_vat + - tz_vat + - ua_vat + - ug_tin + - unknown + - us_ein + - uy_ruc + - uz_tin + - uz_vat + - ve_rif + - vn_tin + - za_vat + - zm_tin + - zw_tin + type: string + value: + description: Value of the tax ID. + maxLength: 5000 + type: string + verification: + anyOf: + - $ref: '#/components/schemas/tax_id_verification' + description: Tax ID verification information. + nullable: true + required: + - created + - id + - livemode + - object + - type + - value + title: tax_id + type: object + x-expandableFields: + - customer + - owner + - verification + x-resourceId: tax_id + tax_id_verification: + description: '' + properties: + status: + description: >- + Verification status, one of `pending`, `verified`, `unverified`, or + `unavailable`. + enum: + - pending + - unavailable + - unverified + - verified + type: string + verified_address: + description: Verified address. + maxLength: 5000 + nullable: true + type: string + verified_name: + description: Verified name. + maxLength: 5000 + nullable: true + type: string + required: + - status + title: tax_id_verification + type: object + x-expandableFields: [] + tax_product_registrations_resource_country_options: + description: '' + properties: + ae: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default_inbound_goods + al: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + am: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + ao: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + at: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + au: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default_inbound_goods + aw: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + az: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + ba: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + bb: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + bd: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + be: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + bf: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + bg: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + bh: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + bj: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + bs: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + by: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + ca: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_canada + cd: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + ch: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default_inbound_goods + cl: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + cm: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + co: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + cr: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + cv: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + cy: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + cz: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + de: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + dk: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + ec: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + ee: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + eg: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + es: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + et: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + fi: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + fr: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + gb: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default_inbound_goods + ge: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + gn: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + gr: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + hr: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + hu: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + id: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + ie: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + in: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + is: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + it: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + jp: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default_inbound_goods + ke: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + kg: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + kh: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + kr: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + kz: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + la: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + lt: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + lu: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + lv: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + ma: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + md: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + me: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + mk: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + mr: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + mt: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + mx: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + my: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + ng: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + nl: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + 'no': + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default_inbound_goods + np: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + nz: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default_inbound_goods + om: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + pe: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + ph: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + pl: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + pt: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + ro: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + rs: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + ru: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + sa: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + se: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + sg: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default_inbound_goods + si: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + sk: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_europe + sn: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + sr: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + th: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + tj: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + tr: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + tz: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + ug: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + us: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_united_states + uy: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + uz: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + vn: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + za: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + zm: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_simplified + zw: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_default + title: TaxProductRegistrationsResourceCountryOptions + type: object + x-expandableFields: + - ae + - al + - am + - ao + - at + - au + - aw + - az + - ba + - bb + - bd + - be + - bf + - bg + - bh + - bj + - bs + - by + - ca + - cd + - ch + - cl + - cm + - co + - cr + - cv + - cy + - cz + - de + - dk + - ec + - ee + - eg + - es + - et + - fi + - fr + - gb + - ge + - gn + - gr + - hr + - hu + - id + - ie + - in + - is + - it + - jp + - ke + - kg + - kh + - kr + - kz + - la + - lt + - lu + - lv + - ma + - md + - me + - mk + - mr + - mt + - mx + - my + - ng + - nl + - 'no' + - np + - nz + - om + - pe + - ph + - pl + - pt + - ro + - rs + - ru + - sa + - se + - sg + - si + - sk + - sn + - sr + - th + - tj + - tr + - tz + - ug + - us + - uy + - uz + - vn + - za + - zm + - zw + tax_product_registrations_resource_country_options_ca_province_standard: + description: '' + properties: + province: + description: >- + Two-letter CA province code ([ISO + 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2)). + maxLength: 5000 + type: string + required: + - province + title: TaxProductRegistrationsResourceCountryOptionsCaProvinceStandard + type: object + x-expandableFields: [] + tax_product_registrations_resource_country_options_canada: + description: '' + properties: + province_standard: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_ca_province_standard + type: + description: Type of registration in Canada. + enum: + - province_standard + - simplified + - standard + type: string + required: + - type + title: TaxProductRegistrationsResourceCountryOptionsCanada + type: object + x-expandableFields: + - province_standard + tax_product_registrations_resource_country_options_default: + description: '' + properties: + type: + description: Type of registration in `country`. + enum: + - standard + type: string + required: + - type + title: TaxProductRegistrationsResourceCountryOptionsDefault + type: object + x-expandableFields: [] + tax_product_registrations_resource_country_options_default_inbound_goods: + description: '' + properties: + type: + description: Type of registration in `country`. + enum: + - standard + type: string + required: + - type + title: TaxProductRegistrationsResourceCountryOptionsDefaultInboundGoods + type: object + x-expandableFields: [] + tax_product_registrations_resource_country_options_eu_standard: + description: '' + properties: + place_of_supply_scheme: + description: Place of supply scheme used in an EU standard registration. + enum: + - small_seller + - standard + type: string + x-stripeBypassValidation: true + required: + - place_of_supply_scheme + title: TaxProductRegistrationsResourceCountryOptionsEuStandard + type: object + x-expandableFields: [] + tax_product_registrations_resource_country_options_europe: + description: '' + properties: + standard: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_eu_standard + type: + description: Type of registration in an EU country. + enum: + - ioss + - oss_non_union + - oss_union + - standard + type: string + required: + - type + title: TaxProductRegistrationsResourceCountryOptionsEurope + type: object + x-expandableFields: + - standard + tax_product_registrations_resource_country_options_simplified: + description: '' + properties: + type: + description: Type of registration in `country`. + enum: + - simplified + type: string + required: + - type + title: TaxProductRegistrationsResourceCountryOptionsSimplified + type: object + x-expandableFields: [] + tax_product_registrations_resource_country_options_united_states: + description: '' + properties: + local_amusement_tax: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_us_local_amusement_tax + local_lease_tax: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_us_local_lease_tax + state: + description: >- + Two-letter US state code ([ISO + 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2)). + maxLength: 5000 + type: string + state_sales_tax: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_us_state_sales_tax + type: + description: Type of registration in the US. + enum: + - local_amusement_tax + - local_lease_tax + - state_communications_tax + - state_retail_delivery_fee + - state_sales_tax + type: string + x-stripeBypassValidation: true + required: + - state + - type + title: TaxProductRegistrationsResourceCountryOptionsUnitedStates + type: object + x-expandableFields: + - local_amusement_tax + - local_lease_tax + - state_sales_tax + tax_product_registrations_resource_country_options_us_local_amusement_tax: + description: '' + properties: + jurisdiction: + description: >- + A [FIPS + code](https://www.census.gov/library/reference/code-lists/ansi.html) + representing the local jurisdiction. + maxLength: 5000 + type: string + required: + - jurisdiction + title: TaxProductRegistrationsResourceCountryOptionsUsLocalAmusementTax + type: object + x-expandableFields: [] + tax_product_registrations_resource_country_options_us_local_lease_tax: + description: '' + properties: + jurisdiction: + description: >- + A [FIPS + code](https://www.census.gov/library/reference/code-lists/ansi.html) + representing the local jurisdiction. + maxLength: 5000 + type: string + required: + - jurisdiction + title: TaxProductRegistrationsResourceCountryOptionsUsLocalLeaseTax + type: object + x-expandableFields: [] + tax_product_registrations_resource_country_options_us_state_sales_tax: + description: '' + properties: + elections: + description: Elections for the state sales tax registration. + items: + $ref: >- + #/components/schemas/tax_product_registrations_resource_country_options_us_state_sales_tax_election + type: array + title: TaxProductRegistrationsResourceCountryOptionsUsStateSalesTax + type: object + x-expandableFields: + - elections + tax_product_registrations_resource_country_options_us_state_sales_tax_election: + description: '' + properties: + jurisdiction: + description: >- + A [FIPS + code](https://www.census.gov/library/reference/code-lists/ansi.html) + representing the local jurisdiction. + maxLength: 5000 + type: string + type: + description: The type of the election for the state sales tax registration. + enum: + - local_use_tax + - simplified_sellers_use_tax + - single_local_use_tax + type: string + required: + - type + title: TaxProductRegistrationsResourceCountryOptionsUsStateSalesTaxElection + type: object + x-expandableFields: [] + tax_product_resource_customer_details: + description: '' + properties: + address: + anyOf: + - $ref: '#/components/schemas/tax_product_resource_postal_address' + description: >- + The customer's postal address (for example, home or business + location). + nullable: true + address_source: + description: The type of customer address provided. + enum: + - billing + - shipping + nullable: true + type: string + ip_address: + description: The customer's IP address (IPv4 or IPv6). + maxLength: 5000 + nullable: true + type: string + tax_ids: + description: 'The customer''s tax IDs (for example, EU VAT numbers).' + items: + $ref: >- + #/components/schemas/tax_product_resource_customer_details_resource_tax_id + type: array + taxability_override: + description: The taxability override used for taxation. + enum: + - customer_exempt + - none + - reverse_charge + type: string + required: + - tax_ids + - taxability_override + title: TaxProductResourceCustomerDetails + type: object + x-expandableFields: + - address + - tax_ids + tax_product_resource_customer_details_resource_tax_id: + description: '' + properties: + type: + description: >- + The type of the tax ID, one of `ad_nrt`, `ar_cuit`, `eu_vat`, + `bo_tin`, `br_cnpj`, `br_cpf`, `cn_tin`, `co_nit`, `cr_tin`, + `do_rcn`, `ec_ruc`, `eu_oss_vat`, `hr_oib`, `pe_ruc`, `ro_tin`, + `rs_pib`, `sv_nit`, `uy_ruc`, `ve_rif`, `vn_tin`, `gb_vat`, + `nz_gst`, `au_abn`, `au_arn`, `in_gst`, `no_vat`, `no_voec`, + `za_vat`, `ch_vat`, `mx_rfc`, `sg_uen`, `ru_inn`, `ru_kpp`, `ca_bn`, + `hk_br`, `es_cif`, `tw_vat`, `th_vat`, `jp_cn`, `jp_rn`, `jp_trn`, + `li_uid`, `li_vat`, `my_itn`, `us_ein`, `kr_brn`, `ca_qst`, + `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `my_sst`, + `sg_gst`, `ae_trn`, `cl_tin`, `sa_vat`, `id_npwp`, `my_frp`, + `il_vat`, `ge_vat`, `ua_vat`, `is_vat`, `bg_uic`, `hu_tin`, + `si_tin`, `ke_pin`, `tr_tin`, `eg_tin`, `ph_tin`, `al_tin`, + `bh_vat`, `kz_bin`, `ng_tin`, `om_vat`, `de_stn`, `ch_uid`, + `tz_vat`, `uz_vat`, `uz_tin`, `md_vat`, `ma_vat`, `by_tin`, + `ao_tin`, `bs_tin`, `bb_tin`, `cd_nif`, `mr_nif`, `me_pib`, + `zw_tin`, `ba_tin`, `gn_nif`, `mk_vat`, `sr_fin`, `sn_ninea`, + `am_tin`, `np_pan`, `tj_tin`, `ug_tin`, `zm_tin`, `kh_tin`, + `aw_tin`, `az_tin`, `bd_bin`, `bj_ifu`, `et_tin`, `kg_tin`, + `la_tin`, `cm_niu`, `cv_nif`, `bf_ifu`, or `unknown` + enum: + - ad_nrt + - ae_trn + - al_tin + - am_tin + - ao_tin + - ar_cuit + - au_abn + - au_arn + - aw_tin + - az_tin + - ba_tin + - bb_tin + - bd_bin + - bf_ifu + - bg_uic + - bh_vat + - bj_ifu + - bo_tin + - br_cnpj + - br_cpf + - bs_tin + - by_tin + - ca_bn + - ca_gst_hst + - ca_pst_bc + - ca_pst_mb + - ca_pst_sk + - ca_qst + - cd_nif + - ch_uid + - ch_vat + - cl_tin + - cm_niu + - cn_tin + - co_nit + - cr_tin + - cv_nif + - de_stn + - do_rcn + - ec_ruc + - eg_tin + - es_cif + - et_tin + - eu_oss_vat + - eu_vat + - gb_vat + - ge_vat + - gn_nif + - hk_br + - hr_oib + - hu_tin + - id_npwp + - il_vat + - in_gst + - is_vat + - jp_cn + - jp_rn + - jp_trn + - ke_pin + - kg_tin + - kh_tin + - kr_brn + - kz_bin + - la_tin + - li_uid + - li_vat + - ma_vat + - md_vat + - me_pib + - mk_vat + - mr_nif + - mx_rfc + - my_frp + - my_itn + - my_sst + - ng_tin + - no_vat + - no_voec + - np_pan + - nz_gst + - om_vat + - pe_ruc + - ph_tin + - ro_tin + - rs_pib + - ru_inn + - ru_kpp + - sa_vat + - sg_gst + - sg_uen + - si_tin + - sn_ninea + - sr_fin + - sv_nit + - th_vat + - tj_tin + - tr_tin + - tw_vat + - tz_vat + - ua_vat + - ug_tin + - unknown + - us_ein + - uy_ruc + - uz_tin + - uz_vat + - ve_rif + - vn_tin + - za_vat + - zm_tin + - zw_tin + type: string + value: + description: The value of the tax ID. + maxLength: 5000 + type: string + required: + - type + - value + title: TaxProductResourceCustomerDetailsResourceTaxId + type: object + x-expandableFields: [] + tax_product_resource_jurisdiction: + description: '' + properties: + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + type: string + display_name: + description: A human-readable name for the jurisdiction imposing the tax. + maxLength: 5000 + type: string + level: + description: Indicates the level of the jurisdiction imposing the tax. + enum: + - city + - country + - county + - district + - state + type: string + state: + description: >- + [ISO 3166-2 subdivision + code](https://en.wikipedia.org/wiki/ISO_3166-2), without country + prefix. For example, "NY" for New York, United States. + maxLength: 5000 + nullable: true + type: string + required: + - country + - display_name + - level + title: TaxProductResourceJurisdiction + type: object + x-expandableFields: [] + tax_product_resource_line_item_tax_breakdown: + description: '' + properties: + amount: + description: >- + The amount of tax, in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + jurisdiction: + $ref: '#/components/schemas/tax_product_resource_jurisdiction' + sourcing: + description: >- + Indicates whether the jurisdiction was determined by the origin + (merchant's address) or destination (customer's address). + enum: + - destination + - origin + type: string + x-stripeBypassValidation: true + tax_rate_details: + anyOf: + - $ref: >- + #/components/schemas/tax_product_resource_line_item_tax_rate_details + description: >- + Details regarding the rate for this tax. This field will be `null` + when the tax is not imposed, for example if the product is exempt + from tax. + nullable: true + taxability_reason: + description: >- + The reasoning behind this tax, for example, if the product is tax + exempt. The possible values for this field may be extended as new + tax rules are supported. + enum: + - customer_exempt + - not_collecting + - not_subject_to_tax + - not_supported + - portion_product_exempt + - portion_reduced_rated + - portion_standard_rated + - product_exempt + - product_exempt_holiday + - proportionally_rated + - reduced_rated + - reverse_charge + - standard_rated + - taxable_basis_reduced + - zero_rated + type: string + taxable_amount: + description: >- + The amount on which tax is calculated, in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + required: + - amount + - jurisdiction + - sourcing + - taxability_reason + - taxable_amount + title: TaxProductResourceLineItemTaxBreakdown + type: object + x-expandableFields: + - jurisdiction + - tax_rate_details + tax_product_resource_line_item_tax_rate_details: + description: '' + properties: + display_name: + description: >- + A localized display name for tax type, intended to be + human-readable. For example, "Local Sales and Use Tax", "Value-added + tax (VAT)", or "Umsatzsteuer (USt.)". + maxLength: 5000 + type: string + percentage_decimal: + description: >- + The tax rate percentage as a string. For example, 8.5% is + represented as "8.5". + maxLength: 5000 + type: string + tax_type: + description: 'The tax type, such as `vat` or `sales_tax`.' + enum: + - amusement_tax + - communications_tax + - gst + - hst + - igst + - jct + - lease_tax + - pst + - qst + - retail_delivery_fee + - rst + - sales_tax + - service_tax + - vat + type: string + x-stripeBypassValidation: true + required: + - display_name + - percentage_decimal + - tax_type + title: TaxProductResourceLineItemTaxRateDetails + type: object + x-expandableFields: [] + tax_product_resource_postal_address: + description: '' + properties: + city: + description: 'City, district, suburb, town, or village.' + maxLength: 5000 + nullable: true + type: string + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + type: string + line1: + description: 'Address line 1 (e.g., street, PO Box, or company name).' + maxLength: 5000 + nullable: true + type: string + line2: + description: 'Address line 2 (e.g., apartment, suite, unit, or building).' + maxLength: 5000 + nullable: true + type: string + postal_code: + description: ZIP or postal code. + maxLength: 5000 + nullable: true + type: string + state: + description: >- + State/province as an [ISO + 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) subdivision code, + without country prefix. Example: "NY" or "TX". + maxLength: 5000 + nullable: true + type: string + required: + - country + title: TaxProductResourcePostalAddress + type: object + x-expandableFields: [] + tax_product_resource_ship_from_details: + description: '' + properties: + address: + $ref: '#/components/schemas/tax_product_resource_postal_address' + required: + - address + title: TaxProductResourceShipFromDetails + type: object + x-expandableFields: + - address + tax_product_resource_tax_breakdown: + description: '' + properties: + amount: + description: >- + The amount of tax, in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + inclusive: + description: >- + Specifies whether the tax amount is included in the line item + amount. + type: boolean + tax_rate_details: + $ref: '#/components/schemas/tax_product_resource_tax_rate_details' + taxability_reason: + description: >- + The reasoning behind this tax, for example, if the product is tax + exempt. We might extend the possible values for this field to + support new tax rules. + enum: + - customer_exempt + - not_collecting + - not_subject_to_tax + - not_supported + - portion_product_exempt + - portion_reduced_rated + - portion_standard_rated + - product_exempt + - product_exempt_holiday + - proportionally_rated + - reduced_rated + - reverse_charge + - standard_rated + - taxable_basis_reduced + - zero_rated + type: string + taxable_amount: + description: >- + The amount on which tax is calculated, in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + required: + - amount + - inclusive + - tax_rate_details + - taxability_reason + - taxable_amount + title: TaxProductResourceTaxBreakdown + type: object + x-expandableFields: + - tax_rate_details + tax_product_resource_tax_calculation_shipping_cost: + description: '' + properties: + amount: + description: >- + The shipping amount in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). If + `tax_behavior=inclusive`, then this amount includes taxes. + Otherwise, taxes were calculated on top of this amount. + type: integer + amount_tax: + description: >- + The amount of tax calculated for shipping, in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + shipping_rate: + description: >- + The ID of an existing + [ShippingRate](https://stripe.com/docs/api/shipping_rates/object). + maxLength: 5000 + type: string + tax_behavior: + description: >- + Specifies whether the `amount` includes taxes. If + `tax_behavior=inclusive`, then the amount includes taxes. + enum: + - exclusive + - inclusive + type: string + tax_breakdown: + description: Detailed account of taxes relevant to shipping cost. + items: + $ref: '#/components/schemas/tax_product_resource_line_item_tax_breakdown' + type: array + tax_code: + description: >- + The [tax code](https://stripe.com/docs/tax/tax-categories) ID used + for shipping. + maxLength: 5000 + type: string + required: + - amount + - amount_tax + - tax_behavior + - tax_code + title: TaxProductResourceTaxCalculationShippingCost + type: object + x-expandableFields: + - tax_breakdown + tax_product_resource_tax_rate_details: + description: '' + properties: + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + nullable: true + type: string + flat_amount: + anyOf: + - $ref: '#/components/schemas/tax_rate_flat_amount' + description: >- + The amount of the tax rate when the `rate_type` is `flat_amount`. + Tax rates with `rate_type` `percentage` can vary based on the + transaction, resulting in this field being `null`. This field + exposes the amount and currency of the flat tax rate. + nullable: true + percentage_decimal: + description: >- + The tax rate percentage as a string. For example, 8.5% is + represented as `"8.5"`. + maxLength: 5000 + type: string + rate_type: + description: >- + Indicates the type of tax rate applied to the taxable amount. This + value can be `null` when no tax applies to the location. This field + is only present for TaxRates created by Stripe Tax. + enum: + - flat_amount + - percentage + nullable: true + type: string + state: + description: 'State, county, province, or region.' + maxLength: 5000 + nullable: true + type: string + tax_type: + description: 'The tax type, such as `vat` or `sales_tax`.' + enum: + - amusement_tax + - communications_tax + - gst + - hst + - igst + - jct + - lease_tax + - pst + - qst + - retail_delivery_fee + - rst + - sales_tax + - service_tax + - vat + nullable: true + type: string + x-stripeBypassValidation: true + required: + - percentage_decimal + title: TaxProductResourceTaxRateDetails + type: object + x-expandableFields: + - flat_amount + tax_product_resource_tax_settings_defaults: + description: '' + properties: + tax_behavior: + description: >- + Default [tax + behavior](https://stripe.com/docs/tax/products-prices-tax-categories-tax-behavior#tax-behavior) + used to specify whether the price is considered inclusive of taxes + or exclusive of taxes. If the item's price has a tax behavior set, + it will take precedence over the default tax behavior. + enum: + - exclusive + - inclusive + - inferred_by_currency + nullable: true + type: string + tax_code: + description: >- + Default [tax code](https://stripe.com/docs/tax/tax-categories) used + to classify your products and prices. + maxLength: 5000 + nullable: true + type: string + title: TaxProductResourceTaxSettingsDefaults + type: object + x-expandableFields: [] + tax_product_resource_tax_settings_head_office: + description: '' + properties: + address: + $ref: '#/components/schemas/address' + required: + - address + title: TaxProductResourceTaxSettingsHeadOffice + type: object + x-expandableFields: + - address + tax_product_resource_tax_settings_status_details: + description: '' + properties: + active: + $ref: >- + #/components/schemas/tax_product_resource_tax_settings_status_details_resource_active + pending: + $ref: >- + #/components/schemas/tax_product_resource_tax_settings_status_details_resource_pending + title: TaxProductResourceTaxSettingsStatusDetails + type: object + x-expandableFields: + - active + - pending + tax_product_resource_tax_settings_status_details_resource_active: + description: '' + properties: {} + title: TaxProductResourceTaxSettingsStatusDetailsResourceActive + type: object + x-expandableFields: [] + tax_product_resource_tax_settings_status_details_resource_pending: + description: '' + properties: + missing_fields: + description: >- + The list of missing fields that are required to perform + calculations. It includes the entry `head_office` when the status is + `pending`. It is recommended to set the optional values even if they + aren't listed as required for calculating taxes. Calculations can + fail if missing fields aren't explicitly provided on every call. + items: + maxLength: 5000 + type: string + nullable: true + type: array + title: TaxProductResourceTaxSettingsStatusDetailsResourcePending + type: object + x-expandableFields: [] + tax_product_resource_tax_transaction_line_item_resource_reversal: + description: '' + properties: + original_line_item: + description: The `id` of the line item to reverse in the original transaction. + maxLength: 5000 + type: string + required: + - original_line_item + title: TaxProductResourceTaxTransactionLineItemResourceReversal + type: object + x-expandableFields: [] + tax_product_resource_tax_transaction_resource_reversal: + description: '' + properties: + original_transaction: + description: The `id` of the reversed `Transaction` object. + maxLength: 5000 + nullable: true + type: string + title: TaxProductResourceTaxTransactionResourceReversal + type: object + x-expandableFields: [] + tax_product_resource_tax_transaction_shipping_cost: + description: '' + properties: + amount: + description: >- + The shipping amount in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). If + `tax_behavior=inclusive`, then this amount includes taxes. + Otherwise, taxes were calculated on top of this amount. + type: integer + amount_tax: + description: >- + The amount of tax calculated for shipping, in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + shipping_rate: + description: >- + The ID of an existing + [ShippingRate](https://stripe.com/docs/api/shipping_rates/object). + maxLength: 5000 + type: string + tax_behavior: + description: >- + Specifies whether the `amount` includes taxes. If + `tax_behavior=inclusive`, then the amount includes taxes. + enum: + - exclusive + - inclusive + type: string + tax_code: + description: >- + The [tax code](https://stripe.com/docs/tax/tax-categories) ID used + for shipping. + maxLength: 5000 + type: string + required: + - amount + - amount_tax + - tax_behavior + - tax_code + title: TaxProductResourceTaxTransactionShippingCost + type: object + x-expandableFields: [] + tax_rate: + description: >- + Tax rates can be applied to [invoices](/invoicing/taxes/tax-rates), + [subscriptions](/billing/taxes/tax-rates) and [Checkout + Sessions](/payments/checkout/use-manual-tax-rates) to collect tax. + + + Related guide: [Tax rates](/billing/taxes/tax-rates) + properties: + active: + description: >- + Defaults to `true`. When set to `false`, this tax rate cannot be + used with new applications or Checkout Sessions, but will still work + for subscriptions and invoices that already have it set. + type: boolean + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + description: + description: >- + An arbitrary string attached to the tax rate for your internal use + only. It will not be visible to your customers. + maxLength: 5000 + nullable: true + type: string + display_name: + description: >- + The display name of the tax rates as it will appear to your customer + on their receipt email, PDF, and the hosted invoice page. + maxLength: 5000 + type: string + effective_percentage: + description: >- + Actual/effective tax rate percentage out of 100. For tax + calculations with automatic_tax[enabled]=true, + + this percentage reflects the rate actually used to calculate tax + based on the product's taxability + + and whether the user is registered to collect taxes in the + corresponding jurisdiction. + nullable: true + type: number + flat_amount: + anyOf: + - $ref: '#/components/schemas/tax_rate_flat_amount' + description: >- + The amount of the tax rate when the `rate_type` is `flat_amount`. + Tax rates with `rate_type` `percentage` can vary based on the + transaction, resulting in this field being `null`. This field + exposes the amount and currency of the flat tax rate. + nullable: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + inclusive: + description: This specifies if the tax rate is inclusive or exclusive. + type: boolean + jurisdiction: + description: >- + The jurisdiction for the tax rate. You can use this label field for + tax reporting purposes. It also appears on your customer’s invoice. + maxLength: 5000 + nullable: true + type: string + jurisdiction_level: + description: >- + The level of the jurisdiction that imposes this tax rate. Will be + `null` for manually defined tax rates. + enum: + - city + - country + - county + - district + - multiple + - state + nullable: true + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - tax_rate + type: string + percentage: + description: >- + Tax rate percentage out of 100. For tax calculations with + automatic_tax[enabled]=true, this percentage includes the statutory + tax rate of non-taxable jurisdictions. + type: number + rate_type: + description: >- + Indicates the type of tax rate applied to the taxable amount. This + value can be `null` when no tax applies to the location. This field + is only present for TaxRates created by Stripe Tax. + enum: + - flat_amount + - percentage + nullable: true + type: string + state: + description: >- + [ISO 3166-2 subdivision + code](https://en.wikipedia.org/wiki/ISO_3166-2), without country + prefix. For example, "NY" for New York, United States. + maxLength: 5000 + nullable: true + type: string + tax_type: + description: 'The high-level tax type, such as `vat` or `sales_tax`.' + enum: + - amusement_tax + - communications_tax + - gst + - hst + - igst + - jct + - lease_tax + - pst + - qst + - retail_delivery_fee + - rst + - sales_tax + - service_tax + - vat + nullable: true + type: string + x-stripeBypassValidation: true + required: + - active + - created + - display_name + - id + - inclusive + - livemode + - object + - percentage + title: TaxRate + type: object + x-expandableFields: + - flat_amount + x-resourceId: tax_rate + tax_rate_flat_amount: + description: >- + The amount of the tax rate when the `rate_type`` is `flat_amount`. Tax + rates with `rate_type` `percentage` can vary based on the transaction, + resulting in this field being `null`. This field exposes the amount and + currency of the flat tax rate. + properties: + amount: + description: >- + Amount of the tax when the `rate_type` is `flat_amount`. This + positive integer represents how much to charge in the smallest + currency unit (e.g., 100 cents to charge $1.00 or 100 to charge + ¥100, a zero-decimal currency). The amount value supports up to + eight digits (e.g., a value of 99999999 for a USD charge of + $999,999.99). + type: integer + currency: + description: 'Three-letter ISO currency code, in lowercase.' + maxLength: 5000 + type: string + required: + - amount + - currency + title: TaxRateFlatAmount + type: object + x-expandableFields: [] + terminal.configuration: + description: >- + A Configurations object represents how features should be configured for + terminal readers. + properties: + bbpos_wisepos_e: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_device_type_specific_config + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + is_account_default: + description: Whether this Configuration is the default for your account + nullable: true + type: boolean + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + name: + description: >- + String indicating the name of the Configuration object, set by the + user + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - terminal.configuration + type: string + offline: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_offline_config + reboot_window: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_reboot_window + stripe_s700: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_device_type_specific_config + tipping: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_tipping + verifone_p400: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_device_type_specific_config + wifi: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_wifi_config + required: + - id + - livemode + - object + title: TerminalConfigurationConfiguration + type: object + x-expandableFields: + - bbpos_wisepos_e + - offline + - reboot_window + - stripe_s700 + - tipping + - verifone_p400 + - wifi + x-resourceId: terminal.configuration + terminal.connection_token: + description: >- + A Connection Token is used by the Stripe Terminal SDK to connect to a + reader. + + + Related guide: [Fleet + management](https://stripe.com/docs/terminal/fleet/locations) + properties: + location: + description: >- + The id of the location that this connection token is scoped to. Note + that location scoping only applies to internet-connected readers. + For more details, see [the docs on scoping connection + tokens](https://docs.stripe.com/terminal/fleet/locations-and-zones?dashboard-or-api=api#connection-tokens). + maxLength: 5000 + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - terminal.connection_token + type: string + secret: + description: Your application should pass this token to the Stripe Terminal SDK. + maxLength: 5000 + type: string + required: + - object + - secret + title: TerminalConnectionToken + type: object + x-expandableFields: [] + x-resourceId: terminal.connection_token + terminal.location: + description: >- + A Location represents a grouping of readers. + + + Related guide: [Fleet + management](https://stripe.com/docs/terminal/fleet/locations) + properties: + address: + $ref: '#/components/schemas/address' + configuration_overrides: + description: >- + The ID of a configuration that will be used to customize all readers + in this location. + maxLength: 5000 + type: string + display_name: + description: The display name of the location. + maxLength: 5000 + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - terminal.location + type: string + required: + - address + - display_name + - id + - livemode + - metadata + - object + title: TerminalLocationLocation + type: object + x-expandableFields: + - address + x-resourceId: terminal.location + terminal.reader: + description: >- + A Reader represents a physical device for accepting payment details. + + + Related guide: [Connecting to a + reader](https://stripe.com/docs/terminal/payments/connect-reader) + properties: + action: + anyOf: + - $ref: >- + #/components/schemas/terminal_reader_reader_resource_reader_action + description: The most recent action performed by the reader. + nullable: true + device_sw_version: + description: The current software version of the reader. + maxLength: 5000 + nullable: true + type: string + device_type: + description: Device type of the reader. + enum: + - bbpos_chipper2x + - bbpos_wisepad3 + - bbpos_wisepos_e + - mobile_phone_reader + - simulated_stripe_s700 + - simulated_wisepos_e + - stripe_m2 + - stripe_s700 + - verifone_P400 + type: string + x-stripeBypassValidation: true + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + ip_address: + description: The local IP address of the reader. + maxLength: 5000 + nullable: true + type: string + label: + description: Custom label given to the reader for easier identification. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + location: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/terminal.location' + description: The location identifier of the reader. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/terminal.location' + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - terminal.reader + type: string + serial_number: + description: Serial number of the reader. + maxLength: 5000 + type: string + status: + description: >- + The networking status of the reader. We do not recommend using this + field in flows that may block taking payments. + enum: + - offline + - online + nullable: true + type: string + required: + - device_type + - id + - label + - livemode + - metadata + - object + - serial_number + title: TerminalReaderReader + type: object + x-expandableFields: + - action + - location + x-resourceId: terminal.reader + terminal_configuration_configuration_resource_currency_specific_config: + description: '' + properties: + fixed_amounts: + description: Fixed amounts displayed when collecting a tip + items: + type: integer + nullable: true + type: array + percentages: + description: Percentages displayed when collecting a tip + items: + type: integer + nullable: true + type: array + smart_tip_threshold: + description: >- + Below this amount, fixed amounts will be displayed; above it, + percentages will be displayed + type: integer + title: TerminalConfigurationConfigurationResourceCurrencySpecificConfig + type: object + x-expandableFields: [] + terminal_configuration_configuration_resource_device_type_specific_config: + description: '' + properties: + splashscreen: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/file' + description: A File ID representing an image to display on the reader + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/file' + title: TerminalConfigurationConfigurationResourceDeviceTypeSpecificConfig + type: object + x-expandableFields: + - splashscreen + terminal_configuration_configuration_resource_enterprise_peap_wifi: + description: '' + properties: + ca_certificate_file: + description: A File ID representing a PEM file containing the server certificate + maxLength: 5000 + type: string + password: + description: Password for connecting to the WiFi network + maxLength: 5000 + type: string + ssid: + description: Name of the WiFi network + maxLength: 5000 + type: string + username: + description: Username for connecting to the WiFi network + maxLength: 5000 + type: string + required: + - password + - ssid + - username + title: TerminalConfigurationConfigurationResourceEnterprisePEAPWifi + type: object + x-expandableFields: [] + terminal_configuration_configuration_resource_enterprise_tls_wifi: + description: '' + properties: + ca_certificate_file: + description: A File ID representing a PEM file containing the server certificate + maxLength: 5000 + type: string + client_certificate_file: + description: A File ID representing a PEM file containing the client certificate + maxLength: 5000 + type: string + private_key_file: + description: >- + A File ID representing a PEM file containing the client RSA private + key + maxLength: 5000 + type: string + private_key_file_password: + description: Password for the private key file + maxLength: 5000 + type: string + ssid: + description: Name of the WiFi network + maxLength: 5000 + type: string + required: + - client_certificate_file + - private_key_file + - ssid + title: TerminalConfigurationConfigurationResourceEnterpriseTLSWifi + type: object + x-expandableFields: [] + terminal_configuration_configuration_resource_offline_config: + description: '' + properties: + enabled: + description: >- + Determines whether to allow transactions to be collected while + reader is offline. Defaults to false. + nullable: true + type: boolean + title: TerminalConfigurationConfigurationResourceOfflineConfig + type: object + x-expandableFields: [] + terminal_configuration_configuration_resource_personal_psk_wifi: + description: '' + properties: + password: + description: Password for connecting to the WiFi network + maxLength: 5000 + type: string + ssid: + description: Name of the WiFi network + maxLength: 5000 + type: string + required: + - password + - ssid + title: TerminalConfigurationConfigurationResourcePersonalPSKWifi + type: object + x-expandableFields: [] + terminal_configuration_configuration_resource_reboot_window: + description: '' + properties: + end_hour: + description: >- + Integer between 0 to 23 that represents the end hour of the reboot + time window. The value must be different than the start_hour. + type: integer + start_hour: + description: >- + Integer between 0 to 23 that represents the start hour of the reboot + time window. + type: integer + required: + - end_hour + - start_hour + title: TerminalConfigurationConfigurationResourceRebootWindow + type: object + x-expandableFields: [] + terminal_configuration_configuration_resource_tipping: + description: '' + properties: + aud: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + cad: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + chf: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + czk: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + dkk: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + eur: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + gbp: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + hkd: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + jpy: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + myr: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + nok: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + nzd: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + pln: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + sek: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + sgd: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + usd: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_currency_specific_config + title: TerminalConfigurationConfigurationResourceTipping + type: object + x-expandableFields: + - aud + - cad + - chf + - czk + - dkk + - eur + - gbp + - hkd + - jpy + - myr + - nok + - nzd + - pln + - sek + - sgd + - usd + terminal_configuration_configuration_resource_wifi_config: + description: '' + properties: + enterprise_eap_peap: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_enterprise_peap_wifi + enterprise_eap_tls: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_enterprise_tls_wifi + personal_psk: + $ref: >- + #/components/schemas/terminal_configuration_configuration_resource_personal_psk_wifi + type: + description: >- + Security type of the WiFi network. The hash with the corresponding + name contains the credentials for this security type. + enum: + - enterprise_eap_peap + - enterprise_eap_tls + - personal_psk + type: string + required: + - type + title: TerminalConfigurationConfigurationResourceWifiConfig + type: object + x-expandableFields: + - enterprise_eap_peap + - enterprise_eap_tls + - personal_psk + terminal_reader_reader_resource_cart: + description: Represents a cart to be displayed on the reader + properties: + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + line_items: + description: List of line items in the cart. + items: + $ref: '#/components/schemas/terminal_reader_reader_resource_line_item' + type: array + tax: + description: >- + Tax amount for the entire cart. A positive integer in the [smallest + currency unit](https://stripe.com/docs/currencies#zero-decimal). + nullable: true + type: integer + total: + description: >- + Total amount for the entire cart, including tax. A positive integer + in the [smallest currency + unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + required: + - currency + - line_items + - total + title: TerminalReaderReaderResourceCart + type: object + x-expandableFields: + - line_items + terminal_reader_reader_resource_choice: + description: Choice to be selected on a Reader + properties: + id: + description: The id to be selected + maxLength: 5000 + nullable: true + type: string + style: + description: The button style for the choice + enum: + - primary + - secondary + nullable: true + type: string + text: + description: The text to be selected + maxLength: 5000 + type: string + required: + - text + title: TerminalReaderReaderResourceChoice + type: object + x-expandableFields: [] + terminal_reader_reader_resource_collect_inputs_action: + description: Represents a reader action to collect customer inputs + properties: + inputs: + description: List of inputs to be collected. + items: + $ref: '#/components/schemas/terminal_reader_reader_resource_input' + type: array + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + required: + - inputs + title: TerminalReaderReaderResourceCollectInputsAction + type: object + x-expandableFields: + - inputs + terminal_reader_reader_resource_custom_text: + description: >- + Represents custom text to be displayed when collecting the input using a + reader + properties: + description: + description: Customize the default description for this input + maxLength: 5000 + nullable: true + type: string + skip_button: + description: Customize the default label for this input's skip button + maxLength: 5000 + nullable: true + type: string + submit_button: + description: Customize the default label for this input's submit button + maxLength: 5000 + nullable: true + type: string + title: + description: Customize the default title for this input + maxLength: 5000 + nullable: true + type: string + title: TerminalReaderReaderResourceCustomText + type: object + x-expandableFields: [] + terminal_reader_reader_resource_email: + description: Information about a email being collected using a reader + properties: + value: + description: The collected email address + maxLength: 5000 + nullable: true + type: string + title: TerminalReaderReaderResourceEmail + type: object + x-expandableFields: [] + terminal_reader_reader_resource_input: + description: Represents an input to be collected using the reader + properties: + custom_text: + anyOf: + - $ref: '#/components/schemas/terminal_reader_reader_resource_custom_text' + description: Default text of input being collected. + nullable: true + email: + $ref: '#/components/schemas/terminal_reader_reader_resource_email' + numeric: + $ref: '#/components/schemas/terminal_reader_reader_resource_numeric' + phone: + $ref: '#/components/schemas/terminal_reader_reader_resource_phone' + required: + description: 'Indicate that this input is required, disabling the skip button.' + nullable: true + type: boolean + selection: + $ref: '#/components/schemas/terminal_reader_reader_resource_selection' + signature: + $ref: '#/components/schemas/terminal_reader_reader_resource_signature' + skipped: + description: Indicate that this input was skipped by the user. + type: boolean + text: + $ref: '#/components/schemas/terminal_reader_reader_resource_text' + toggles: + description: >- + List of toggles being collected. Values are present if collection is + complete. + items: + $ref: '#/components/schemas/terminal_reader_reader_resource_toggle' + nullable: true + type: array + type: + description: Type of input being collected. + enum: + - email + - numeric + - phone + - selection + - signature + - text + type: string + required: + - type + title: TerminalReaderReaderResourceInput + type: object + x-expandableFields: + - custom_text + - email + - numeric + - phone + - selection + - signature + - text + - toggles + terminal_reader_reader_resource_line_item: + description: Represents a line item to be displayed on the reader + properties: + amount: + description: >- + The amount of the line item. A positive integer in the [smallest + currency unit](https://stripe.com/docs/currencies#zero-decimal). + type: integer + description: + description: Description of the line item. + maxLength: 5000 + type: string + quantity: + description: The quantity of the line item. + type: integer + required: + - amount + - description + - quantity + title: TerminalReaderReaderResourceLineItem + type: object + x-expandableFields: [] + terminal_reader_reader_resource_numeric: + description: Information about a number being collected using a reader + properties: + value: + description: The collected number + maxLength: 5000 + nullable: true + type: string + title: TerminalReaderReaderResourceNumeric + type: object + x-expandableFields: [] + terminal_reader_reader_resource_phone: + description: Information about a phone number being collected using a reader + properties: + value: + description: The collected phone number + maxLength: 5000 + nullable: true + type: string + title: TerminalReaderReaderResourcePhone + type: object + x-expandableFields: [] + terminal_reader_reader_resource_process_config: + description: Represents a per-transaction override of a reader configuration + properties: + enable_customer_cancellation: + description: Enable customer initiated cancellation when processing this payment. + type: boolean + return_url: + description: >- + If the customer does not abandon authenticating the payment, they + will be redirected to this specified URL after completion. + maxLength: 5000 + type: string + skip_tipping: + description: Override showing a tipping selection screen on this transaction. + type: boolean + tipping: + $ref: '#/components/schemas/terminal_reader_reader_resource_tipping_config' + title: TerminalReaderReaderResourceProcessConfig + type: object + x-expandableFields: + - tipping + terminal_reader_reader_resource_process_payment_intent_action: + description: Represents a reader action to process a payment intent + properties: + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: Most recent PaymentIntent processed by the reader. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + process_config: + $ref: '#/components/schemas/terminal_reader_reader_resource_process_config' + required: + - payment_intent + title: TerminalReaderReaderResourceProcessPaymentIntentAction + type: object + x-expandableFields: + - payment_intent + - process_config + terminal_reader_reader_resource_process_setup_config: + description: Represents a per-setup override of a reader configuration + properties: + enable_customer_cancellation: + description: >- + Enable customer initiated cancellation when processing this + SetupIntent. + type: boolean + title: TerminalReaderReaderResourceProcessSetupConfig + type: object + x-expandableFields: [] + terminal_reader_reader_resource_process_setup_intent_action: + description: Represents a reader action to process a setup intent + properties: + generated_card: + description: >- + ID of a card PaymentMethod generated from the card_present + PaymentMethod that may be attached to a Customer for future + transactions. Only present if it was possible to generate a card + PaymentMethod. + maxLength: 5000 + type: string + process_config: + $ref: >- + #/components/schemas/terminal_reader_reader_resource_process_setup_config + setup_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/setup_intent' + description: Most recent SetupIntent processed by the reader. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/setup_intent' + required: + - setup_intent + title: TerminalReaderReaderResourceProcessSetupIntentAction + type: object + x-expandableFields: + - process_config + - setup_intent + terminal_reader_reader_resource_reader_action: + description: Represents an action performed by the reader + properties: + collect_inputs: + $ref: >- + #/components/schemas/terminal_reader_reader_resource_collect_inputs_action + failure_code: + description: 'Failure code, only set if status is `failed`.' + maxLength: 5000 + nullable: true + type: string + failure_message: + description: 'Detailed failure message, only set if status is `failed`.' + maxLength: 5000 + nullable: true + type: string + process_payment_intent: + $ref: >- + #/components/schemas/terminal_reader_reader_resource_process_payment_intent_action + process_setup_intent: + $ref: >- + #/components/schemas/terminal_reader_reader_resource_process_setup_intent_action + refund_payment: + $ref: >- + #/components/schemas/terminal_reader_reader_resource_refund_payment_action + set_reader_display: + $ref: >- + #/components/schemas/terminal_reader_reader_resource_set_reader_display_action + status: + description: Status of the action performed by the reader. + enum: + - failed + - in_progress + - succeeded + type: string + type: + description: Type of action performed by the reader. + enum: + - collect_inputs + - process_payment_intent + - process_setup_intent + - refund_payment + - set_reader_display + type: string + x-stripeBypassValidation: true + required: + - status + - type + title: TerminalReaderReaderResourceReaderAction + type: object + x-expandableFields: + - collect_inputs + - process_payment_intent + - process_setup_intent + - refund_payment + - set_reader_display + terminal_reader_reader_resource_refund_payment_action: + description: Represents a reader action to refund a payment + properties: + amount: + description: The amount being refunded. + type: integer + charge: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: Charge that is being refunded. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + payment_intent: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/payment_intent' + description: Payment intent that is being refunded. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/payment_intent' + reason: + description: The reason for the refund. + enum: + - duplicate + - fraudulent + - requested_by_customer + type: string + refund: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/refund' + description: Unique identifier for the refund object. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/refund' + refund_application_fee: + description: >- + Boolean indicating whether the application fee should be refunded + when refunding this charge. If a full charge refund is given, the + full application fee will be refunded. Otherwise, the application + fee will be refunded in an amount proportional to the amount of the + charge refunded. An application fee can be refunded only by the + application that created the charge. + type: boolean + refund_payment_config: + $ref: >- + #/components/schemas/terminal_reader_reader_resource_refund_payment_config + reverse_transfer: + description: >- + Boolean indicating whether the transfer should be reversed when + refunding this charge. The transfer will be reversed proportionally + to the amount being refunded (either the entire or partial amount). + A transfer can be reversed only by the application that created the + charge. + type: boolean + title: TerminalReaderReaderResourceRefundPaymentAction + type: object + x-expandableFields: + - charge + - payment_intent + - refund + - refund_payment_config + terminal_reader_reader_resource_refund_payment_config: + description: Represents a per-transaction override of a reader configuration + properties: + enable_customer_cancellation: + description: Enable customer initiated cancellation when refunding this payment. + type: boolean + title: TerminalReaderReaderResourceRefundPaymentConfig + type: object + x-expandableFields: [] + terminal_reader_reader_resource_selection: + description: Information about a selection being collected using a reader + properties: + choices: + description: List of possible choices to be selected + items: + $ref: '#/components/schemas/terminal_reader_reader_resource_choice' + type: array + id: + description: The id of the selected choice + maxLength: 5000 + nullable: true + type: string + text: + description: The text of the selected choice + maxLength: 5000 + nullable: true + type: string + required: + - choices + title: TerminalReaderReaderResourceSelection + type: object + x-expandableFields: + - choices + terminal_reader_reader_resource_set_reader_display_action: + description: Represents a reader action to set the reader display + properties: + cart: + anyOf: + - $ref: '#/components/schemas/terminal_reader_reader_resource_cart' + description: Cart object to be displayed by the reader. + nullable: true + type: + description: Type of information to be displayed by the reader. + enum: + - cart + type: string + required: + - type + title: TerminalReaderReaderResourceSetReaderDisplayAction + type: object + x-expandableFields: + - cart + terminal_reader_reader_resource_signature: + description: Information about a signature being collected using a reader + properties: + value: + description: The File ID of a collected signature image + maxLength: 5000 + nullable: true + type: string + title: TerminalReaderReaderResourceSignature + type: object + x-expandableFields: [] + terminal_reader_reader_resource_text: + description: Information about text being collected using a reader + properties: + value: + description: The collected text value + maxLength: 5000 + nullable: true + type: string + title: TerminalReaderReaderResourceText + type: object + x-expandableFields: [] + terminal_reader_reader_resource_tipping_config: + description: Represents a per-transaction tipping configuration + properties: + amount_eligible: + description: >- + Amount used to calculate tip suggestions on tipping selection screen + for this transaction. Must be a positive integer in the smallest + currency unit (e.g., 100 cents to represent $1.00 or 100 to + represent ¥100, a zero-decimal currency). + type: integer + title: TerminalReaderReaderResourceTippingConfig + type: object + x-expandableFields: [] + terminal_reader_reader_resource_toggle: + description: Information about an input's toggle + properties: + default_value: + description: The toggle's default value + enum: + - disabled + - enabled + nullable: true + type: string + description: + description: The toggle's description text + maxLength: 5000 + nullable: true + type: string + title: + description: The toggle's title text + maxLength: 5000 + nullable: true + type: string + value: + description: The toggle's collected value + enum: + - disabled + - enabled + nullable: true + type: string + title: TerminalReaderReaderResourceToggle + type: object + x-expandableFields: [] + test_helpers.test_clock: + description: >- + A test clock enables deterministic control over objects in testmode. + With a test clock, you can create + + objects at a frozen time in the past or future, and advance to a + specific future time to observe webhooks and state changes. After the + clock advances, + + you can either validate the current state of your scenario (and test + your assumptions), change the current state of your scenario (and test + more complex scenarios), or keep advancing forward in time. + properties: + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + deletes_after: + description: Time at which this clock is scheduled to auto delete. + format: unix-time + type: integer + frozen_time: + description: Time at which all objects belonging to this clock are frozen. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + name: + description: The custom name supplied at creation. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - test_helpers.test_clock + type: string + status: + description: The status of the Test Clock. + enum: + - advancing + - internal_failure + - ready + type: string + status_details: + $ref: >- + #/components/schemas/billing_clocks_resource_status_details_status_details + required: + - created + - deletes_after + - frozen_time + - id + - livemode + - object + - status + - status_details + title: TestClock + type: object + x-expandableFields: + - status_details + x-resourceId: test_helpers.test_clock + three_d_secure_details: + description: '' + properties: + authentication_flow: + description: >- + For authenticated transactions: how the customer was authenticated + by + + the issuing bank. + enum: + - challenge + - frictionless + nullable: true + type: string + electronic_commerce_indicator: + description: |- + The Electronic Commerce Indicator (ECI). A protocol-level field + indicating what degree of authentication was performed. + enum: + - '01' + - '02' + - '05' + - '06' + - '07' + nullable: true + type: string + x-stripeBypassValidation: true + result: + description: Indicates the outcome of 3D Secure authentication. + enum: + - attempt_acknowledged + - authenticated + - exempted + - failed + - not_supported + - processing_error + nullable: true + type: string + result_reason: + description: |- + Additional information about why 3D Secure succeeded or failed based + on the `result`. + enum: + - abandoned + - bypassed + - canceled + - card_not_enrolled + - network_not_supported + - protocol_error + - rejected + nullable: true + type: string + transaction_id: + description: |- + The 3D Secure 1 XID or 3D Secure 2 Directory Server Transaction ID + (dsTransId) for this payment. + maxLength: 5000 + nullable: true + type: string + version: + description: The version of 3D Secure that was used. + enum: + - 1.0.2 + - 2.1.0 + - 2.2.0 + nullable: true + type: string + x-stripeBypassValidation: true + title: three_d_secure_details + type: object + x-expandableFields: [] + three_d_secure_details_charge: + description: '' + properties: + authentication_flow: + description: >- + For authenticated transactions: how the customer was authenticated + by + + the issuing bank. + enum: + - challenge + - frictionless + nullable: true + type: string + electronic_commerce_indicator: + description: |- + The Electronic Commerce Indicator (ECI). A protocol-level field + indicating what degree of authentication was performed. + enum: + - '01' + - '02' + - '05' + - '06' + - '07' + nullable: true + type: string + x-stripeBypassValidation: true + exemption_indicator: + description: >- + The exemption requested via 3DS and accepted by the issuer at + authentication time. + enum: + - low_risk + - none + nullable: true + type: string + exemption_indicator_applied: + description: >- + Whether Stripe requested the value of `exemption_indicator` in the + transaction. This will depend on + + the outcome of Stripe's internal risk assessment. + type: boolean + result: + description: Indicates the outcome of 3D Secure authentication. + enum: + - attempt_acknowledged + - authenticated + - exempted + - failed + - not_supported + - processing_error + nullable: true + type: string + result_reason: + description: |- + Additional information about why 3D Secure succeeded or failed based + on the `result`. + enum: + - abandoned + - bypassed + - canceled + - card_not_enrolled + - network_not_supported + - protocol_error + - rejected + nullable: true + type: string + transaction_id: + description: |- + The 3D Secure 1 XID or 3D Secure 2 Directory Server Transaction ID + (dsTransId) for this payment. + maxLength: 5000 + nullable: true + type: string + version: + description: The version of 3D Secure that was used. + enum: + - 1.0.2 + - 2.1.0 + - 2.2.0 + nullable: true + type: string + x-stripeBypassValidation: true + title: three_d_secure_details_charge + type: object + x-expandableFields: [] + three_d_secure_usage: + description: '' + properties: + supported: + description: Whether 3D Secure is supported on this card. + type: boolean + required: + - supported + title: three_d_secure_usage + type: object + x-expandableFields: [] + thresholds_resource_usage_alert_filter: + description: '' + properties: + customer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/customer' + description: Limit the scope of the alert to this customer ID + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/customer' + type: + enum: + - customer + type: string + required: + - type + title: ThresholdsResourceUsageAlertFilter + type: object + x-expandableFields: + - customer + thresholds_resource_usage_threshold_config: + description: >- + The usage threshold alert configuration enables setting up alerts for + when a certain usage threshold on a specific meter is crossed. + properties: + filters: + description: >- + The filters allow limiting the scope of this usage alert. You can + only specify up to one filter at this time. + items: + $ref: '#/components/schemas/thresholds_resource_usage_alert_filter' + nullable: true + type: array + gte: + description: The value at which this alert will trigger. + type: integer + meter: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/billing.meter' + description: 'The [Billing Meter](/api/billing/meter) ID whose usage is monitored.' + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/billing.meter' + recurrence: + description: Defines how the alert will behave. + enum: + - one_time + type: string + x-stripeBypassValidation: true + required: + - gte + - meter + - recurrence + title: ThresholdsResourceUsageThresholdConfig + type: object + x-expandableFields: + - filters + - meter + token: + description: >- + Tokenization is the process Stripe uses to collect sensitive card or + bank + + account details, or personally identifiable information (PII), directly + from + + your customers in a secure manner. A token representing this information + is + + returned to your server to use. Use our + + [recommended payments integrations](https://stripe.com/docs/payments) to + perform this process + + on the client-side. This guarantees that no sensitive card data touches + your server, + + and allows your integration to operate in a PCI-compliant way. + + + If you can't use client-side tokenization, you can also create tokens + using + + the API with either your publishable or secret API key. If + + your integration uses this method, you're responsible for any PCI + compliance + + that it might require, and you must keep your secret API key safe. + Unlike with + + client-side tokenization, your customer's information isn't sent + directly to + + Stripe, so we can't determine how it's handled or stored. + + + You can't store or use tokens more than once. To store card or bank + account + + information for later use, create + [Customer](https://stripe.com/docs/api#customers) + + objects or [External accounts](/api#external_accounts). + + [Radar](https://stripe.com/docs/radar), our integrated solution for + automatic fraud protection, + + performs best with integrations that use client-side tokenization. + properties: + bank_account: + $ref: '#/components/schemas/bank_account' + card: + $ref: '#/components/schemas/card' + client_ip: + description: IP address of the client that generates the token. + maxLength: 5000 + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - token + type: string + type: + description: 'Type of the token: `account`, `bank_account`, `card`, or `pii`.' + maxLength: 5000 + type: string + used: + description: >- + Determines if you have already used this token (you can only use + tokens once). + type: boolean + required: + - created + - id + - livemode + - object + - type + - used + title: Token + type: object + x-expandableFields: + - bank_account + - card + x-resourceId: token + token_card_networks: + description: '' + properties: + preferred: + description: >- + The preferred network for co-branded cards. Can be + `cartes_bancaires`, `mastercard`, `visa` or `invalid_preference` if + requested network is not valid for the card. + maxLength: 5000 + nullable: true + type: string + title: token_card_networks + type: object + x-expandableFields: [] + topup: + description: >- + To top up your Stripe balance, you create a top-up object. You can + retrieve + + individual top-ups, as well as list all top-ups. Top-ups are identified + by a + + unique, random ID. + + + Related guide: [Topping up your platform + account](https://stripe.com/docs/connect/top-ups) + properties: + amount: + description: Amount transferred. + type: integer + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + ID of the balance transaction that describes the impact of this + top-up on your account balance. May not be specified depending on + status of top-up. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + maxLength: 5000 + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + expected_availability_date: + description: >- + Date the funds are expected to arrive in your Stripe account for + payouts. This factors in delays like weekends or bank holidays. May + not be specified depending on status of top-up. + nullable: true + type: integer + failure_code: + description: >- + Error code explaining reason for top-up failure if available (see + [the errors section](https://stripe.com/docs/api#errors) for a list + of codes). + maxLength: 5000 + nullable: true + type: string + failure_message: + description: >- + Message to user further explaining reason for top-up failure if + available. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - topup + type: string + source: + anyOf: + - $ref: '#/components/schemas/source' + description: >- + The source field is deprecated. It might not always be present in + the API response. + nullable: true + statement_descriptor: + description: >- + Extra information about a top-up. This will appear on your source's + bank statement. It must contain at least one letter. + maxLength: 5000 + nullable: true + type: string + status: + description: >- + The status of the top-up is either `canceled`, `failed`, `pending`, + `reversed`, or `succeeded`. + enum: + - canceled + - failed + - pending + - reversed + - succeeded + type: string + transfer_group: + description: A string that identifies this top-up as part of a group. + maxLength: 5000 + nullable: true + type: string + required: + - amount + - created + - currency + - id + - livemode + - metadata + - object + - status + title: Topup + type: object + x-expandableFields: + - balance_transaction + - source + x-resourceId: topup + transfer: + description: >- + A `Transfer` object is created when you move funds between Stripe + accounts as + + part of Connect. + + + Before April 6, 2017, transfers also represented movement of funds from + a + + Stripe account to a card or bank account. This behavior has since been + split + + out into a [Payout](https://stripe.com/docs/api#payout_object) object, + with corresponding payout endpoints. For more + + information, read about the + + [transfer/payout split](https://stripe.com/docs/transfer-payout-split). + + + Related guide: [Creating separate charges and + transfers](https://stripe.com/docs/connect/separate-charges-and-transfers) + properties: + amount: + description: Amount in cents (or local equivalent) to be transferred. + type: integer + amount_reversed: + description: >- + Amount in cents (or local equivalent) reversed (can be less than the + amount attribute on the transfer if a partial reversal was issued). + type: integer + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + Balance transaction that describes the impact of this transfer on + your account balance. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + created: + description: Time that this record of the transfer was first created. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + destination: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: ID of the Stripe account the transfer was sent to. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + destination_payment: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: >- + If the destination is a Stripe account, this will be the ID of the + payment that the destination account received for the transfer. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - transfer + type: string + reversals: + description: A list of reversals that have been applied to the transfer. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/transfer_reversal' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: TransferReversalList + type: object + x-expandableFields: + - data + reversed: + description: >- + Whether the transfer has been fully reversed. If the transfer is + only partially reversed, this attribute will still be false. + type: boolean + source_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/charge' + description: >- + ID of the charge that was used to fund the transfer. If null, the + transfer was funded from the available balance. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/charge' + source_type: + description: >- + The source balance this transfer came from. One of `card`, `fpx`, or + `bank_account`. + maxLength: 5000 + type: string + transfer_group: + description: >- + A string that identifies this transaction as part of a group. See + the [Connect + documentation](https://stripe.com/docs/connect/separate-charges-and-transfers#transfer-options) + for details. + maxLength: 5000 + nullable: true + type: string + required: + - amount + - amount_reversed + - created + - currency + - id + - livemode + - metadata + - object + - reversals + - reversed + title: Transfer + type: object + x-expandableFields: + - balance_transaction + - destination + - destination_payment + - reversals + - source_transaction + x-resourceId: transfer + transfer_data: + description: '' + properties: + amount: + description: >- + The amount transferred to the destination account. This transfer + will occur automatically after the payment succeeds. If no amount is + specified, by default the entire payment amount is transferred to + the destination account. + The amount must be less than or equal to the [amount](https://stripe.com/docs/api/payment_intents/object#payment_intent_object-amount), and must be a positive integer + representing how much to transfer in the smallest currency unit (e.g., 100 cents to charge $1.00). + type: integer + destination: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/account' + description: >- + The account (if any) that the payment is attributed to for tax + reporting, and where funds from the payment are transferred to after + payment success. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/account' + required: + - destination + title: transfer_data + type: object + x-expandableFields: + - destination + transfer_reversal: + description: >- + [Stripe Connect](https://stripe.com/docs/connect) platforms can reverse + transfers made to a + + connected account, either entirely or partially, and can also specify + whether + + to refund any related application fees. Transfer reversals add to the + + platform's balance and subtract from the destination account's balance. + + + Reversing a transfer that was made for a [destination + + charge](/docs/connect/destination-charges) is allowed only up to the + amount of + + the charge. It is possible to reverse a + + [transfer_group](https://stripe.com/docs/connect/separate-charges-and-transfers#transfer-options) + + transfer only if the destination account has enough balance to cover the + + reversal. + + + Related guide: [Reverse + transfers](https://stripe.com/docs/connect/separate-charges-and-transfers#reverse-transfers) + properties: + amount: + description: 'Amount, in cents (or local equivalent).' + type: integer + balance_transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/balance_transaction' + description: >- + Balance transaction that describes the impact on your account + balance. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/balance_transaction' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + destination_payment_refund: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/refund' + description: Linked payment refund for the transfer reversal. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/refund' + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - transfer_reversal + type: string + source_refund: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/refund' + description: ID of the refund responsible for the transfer reversal. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/refund' + transfer: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/transfer' + description: ID of the transfer that was reversed. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/transfer' + required: + - amount + - created + - currency + - id + - object + - transfer + title: TransferReversal + type: object + x-expandableFields: + - balance_transaction + - destination_payment_refund + - source_refund + - transfer + x-resourceId: transfer_reversal + transfer_schedule: + description: '' + properties: + delay_days: + description: >- + The number of days charges for the account will be held before being + paid out. + type: integer + interval: + description: >- + How frequently funds will be paid out. One of `manual` (payouts only + created via API call), `daily`, `weekly`, or `monthly`. + maxLength: 5000 + type: string + monthly_anchor: + description: >- + The day of the month funds will be paid out. Only shown if + `interval` is monthly. Payouts scheduled between the 29th and 31st + of the month are sent on the last day of shorter months. + type: integer + weekly_anchor: + description: >- + The day of the week funds will be paid out, of the style 'monday', + 'tuesday', etc. Only shown if `interval` is weekly. + maxLength: 5000 + type: string + required: + - delay_days + - interval + title: TransferSchedule + type: object + x-expandableFields: [] + transform_quantity: + description: '' + properties: + divide_by: + description: Divide usage by this number. + type: integer + round: + description: 'After division, either round the result `up` or `down`.' + enum: + - down + - up + type: string + required: + - divide_by + - round + title: TransformQuantity + type: object + x-expandableFields: [] + transform_usage: + description: '' + properties: + divide_by: + description: Divide usage by this number. + type: integer + round: + description: 'After division, either round the result `up` or `down`.' + enum: + - down + - up + type: string + required: + - divide_by + - round + title: TransformUsage + type: object + x-expandableFields: [] + treasury.credit_reversal: + description: >- + You can reverse some + [ReceivedCredits](https://stripe.com/docs/api#received_credits) + depending on their network and source flow. Reversing a ReceivedCredit + leads to the creation of a new object known as a CreditReversal. + properties: + amount: + description: Amount (in cents) transferred. + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + financial_account: + description: The FinancialAccount to reverse funds from. + maxLength: 5000 + type: string + hosted_regulatory_receipt_url: + description: >- + A [hosted transaction + receipt](https://stripe.com/docs/treasury/moving-money/regulatory-receipts) + URL that is provided when money movement is considered regulated + under Stripe's money transmission licenses. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + network: + description: The rails used to reverse the funds. + enum: + - ach + - stripe + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.credit_reversal + type: string + received_credit: + description: The ReceivedCredit being reversed. + maxLength: 5000 + type: string + status: + description: Status of the CreditReversal + enum: + - canceled + - posted + - processing + type: string + status_transitions: + $ref: >- + #/components/schemas/treasury_received_credits_resource_status_transitions + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - amount + - created + - currency + - financial_account + - id + - livemode + - metadata + - network + - object + - received_credit + - status + - status_transitions + title: TreasuryReceivedCreditsResourceCreditReversal + type: object + x-expandableFields: + - status_transitions + - transaction + x-resourceId: treasury.credit_reversal + treasury.debit_reversal: + description: >- + You can reverse some + [ReceivedDebits](https://stripe.com/docs/api#received_debits) depending + on their network and source flow. Reversing a ReceivedDebit leads to the + creation of a new object known as a DebitReversal. + properties: + amount: + description: Amount (in cents) transferred. + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + financial_account: + description: The FinancialAccount to reverse funds from. + maxLength: 5000 + nullable: true + type: string + hosted_regulatory_receipt_url: + description: >- + A [hosted transaction + receipt](https://stripe.com/docs/treasury/moving-money/regulatory-receipts) + URL that is provided when money movement is considered regulated + under Stripe's money transmission licenses. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + linked_flows: + anyOf: + - $ref: >- + #/components/schemas/treasury_received_debits_resource_debit_reversal_linked_flows + description: Other flows linked to a DebitReversal. + nullable: true + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + network: + description: The rails used to reverse the funds. + enum: + - ach + - card + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.debit_reversal + type: string + received_debit: + description: The ReceivedDebit being reversed. + maxLength: 5000 + type: string + status: + description: Status of the DebitReversal + enum: + - failed + - processing + - succeeded + type: string + status_transitions: + $ref: >- + #/components/schemas/treasury_received_debits_resource_status_transitions + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - amount + - created + - currency + - id + - livemode + - metadata + - network + - object + - received_debit + - status + - status_transitions + title: TreasuryReceivedDebitsResourceDebitReversal + type: object + x-expandableFields: + - linked_flows + - status_transitions + - transaction + x-resourceId: treasury.debit_reversal + treasury.financial_account: + description: >- + Stripe Treasury provides users with a container for money called a + FinancialAccount that is separate from their Payments balance. + + FinancialAccounts serve as the source and destination of Treasury’s + money movement APIs. + properties: + active_features: + description: The array of paths to active Features in the Features hash. + items: + enum: + - card_issuing + - deposit_insurance + - financial_addresses.aba + - financial_addresses.aba.forwarding + - inbound_transfers.ach + - intra_stripe_flows + - outbound_payments.ach + - outbound_payments.us_domestic_wire + - outbound_transfers.ach + - outbound_transfers.us_domestic_wire + - remote_deposit_capture + type: string + x-stripeBypassValidation: true + type: array + balance: + $ref: '#/components/schemas/treasury_financial_accounts_resource_balance' + country: + description: >- + Two-letter country code ([ISO 3166-1 + alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). + maxLength: 5000 + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + features: + $ref: '#/components/schemas/treasury.financial_account_features' + financial_addresses: + description: The set of credentials that resolve to a FinancialAccount. + items: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_financial_address + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + is_default: + type: boolean + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + nullable: true + type: object + nickname: + description: The nickname for the FinancialAccount. + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.financial_account + type: string + pending_features: + description: The array of paths to pending Features in the Features hash. + items: + enum: + - card_issuing + - deposit_insurance + - financial_addresses.aba + - financial_addresses.aba.forwarding + - inbound_transfers.ach + - intra_stripe_flows + - outbound_payments.ach + - outbound_payments.us_domestic_wire + - outbound_transfers.ach + - outbound_transfers.us_domestic_wire + - remote_deposit_capture + type: string + x-stripeBypassValidation: true + type: array + platform_restrictions: + anyOf: + - $ref: >- + #/components/schemas/treasury_financial_accounts_resource_platform_restrictions + description: >- + The set of functionalities that the platform can restrict on the + FinancialAccount. + nullable: true + restricted_features: + description: The array of paths to restricted Features in the Features hash. + items: + enum: + - card_issuing + - deposit_insurance + - financial_addresses.aba + - financial_addresses.aba.forwarding + - inbound_transfers.ach + - intra_stripe_flows + - outbound_payments.ach + - outbound_payments.us_domestic_wire + - outbound_transfers.ach + - outbound_transfers.us_domestic_wire + - remote_deposit_capture + type: string + x-stripeBypassValidation: true + type: array + status: + description: Status of this FinancialAccount. + enum: + - closed + - open + type: string + x-stripeBypassValidation: true + status_details: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_status_details + supported_currencies: + description: >- + The currencies the FinancialAccount can hold a balance in. + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. + items: + type: string + type: array + required: + - balance + - country + - created + - financial_addresses + - id + - livemode + - object + - status + - status_details + - supported_currencies + title: TreasuryFinancialAccountsResourceFinancialAccount + type: object + x-expandableFields: + - balance + - features + - financial_addresses + - platform_restrictions + - status_details + x-resourceId: treasury.financial_account + treasury.financial_account_features: + description: >- + Encodes whether a FinancialAccount has access to a particular Feature, + with a `status` enum and associated `status_details`. + + Stripe or the platform can control Features via the requested field. + properties: + card_issuing: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggle_settings + deposit_insurance: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggle_settings + financial_addresses: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_financial_addresses_features + inbound_transfers: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_inbound_transfers + intra_stripe_flows: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggle_settings + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.financial_account_features + type: string + outbound_payments: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_outbound_payments + outbound_transfers: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_outbound_transfers + required: + - object + title: TreasuryFinancialAccountsResourceFinancialAccountFeatures + type: object + x-expandableFields: + - card_issuing + - deposit_insurance + - financial_addresses + - inbound_transfers + - intra_stripe_flows + - outbound_payments + - outbound_transfers + x-resourceId: treasury.financial_account_features + treasury.inbound_transfer: + description: >- + Use + [InboundTransfers](https://docs.stripe.com/docs/treasury/moving-money/financial-accounts/into/inbound-transfers) + to add funds to your + [FinancialAccount](https://stripe.com/docs/api#financial_accounts) via a + PaymentMethod that is owned by you. The funds will be transferred via an + ACH debit. + + + Related guide: [Moving money with Treasury using InboundTransfer + objects](https://docs.stripe.com/docs/treasury/moving-money/financial-accounts/into/inbound-transfers) + properties: + amount: + description: Amount (in cents) transferred. + type: integer + cancelable: + description: Returns `true` if the InboundTransfer is able to be canceled. + type: boolean + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + failure_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_inbound_transfers_resource_failure_details + description: >- + Details about this InboundTransfer's failure. Only set when status + is `failed`. + nullable: true + financial_account: + description: The FinancialAccount that received the funds. + maxLength: 5000 + type: string + hosted_regulatory_receipt_url: + description: >- + A [hosted transaction + receipt](https://stripe.com/docs/treasury/moving-money/regulatory-receipts) + URL that is provided when money movement is considered regulated + under Stripe's money transmission licenses. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + linked_flows: + $ref: >- + #/components/schemas/treasury_inbound_transfers_resource_inbound_transfer_resource_linked_flows + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.inbound_transfer + type: string + origin_payment_method: + description: The origin payment method to be debited for an InboundTransfer. + maxLength: 5000 + nullable: true + type: string + origin_payment_method_details: + anyOf: + - $ref: '#/components/schemas/inbound_transfers' + description: Details about the PaymentMethod for an InboundTransfer. + nullable: true + returned: + description: >- + Returns `true` if the funds for an InboundTransfer were returned + after the InboundTransfer went to the `succeeded` state. + nullable: true + type: boolean + statement_descriptor: + description: >- + Statement descriptor shown when funds are debited from the source. + Not all payment networks support `statement_descriptor`. + maxLength: 5000 + type: string + status: + description: >- + Status of the InboundTransfer: `processing`, `succeeded`, `failed`, + and `canceled`. An InboundTransfer is `processing` if it is created + and pending. The status changes to `succeeded` once the funds have + been "confirmed" and a `transaction` is created and posted. The + status changes to `failed` if the transfer fails. + enum: + - canceled + - failed + - processing + - succeeded + type: string + x-stripeBypassValidation: true + status_transitions: + $ref: >- + #/components/schemas/treasury_inbound_transfers_resource_inbound_transfer_resource_status_transitions + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - amount + - cancelable + - created + - currency + - financial_account + - id + - linked_flows + - livemode + - metadata + - object + - statement_descriptor + - status + - status_transitions + title: TreasuryInboundTransfersResourceInboundTransfer + type: object + x-expandableFields: + - failure_details + - linked_flows + - origin_payment_method_details + - status_transitions + - transaction + x-resourceId: treasury.inbound_transfer + treasury.outbound_payment: + description: >- + Use + [OutboundPayments](https://docs.stripe.com/docs/treasury/moving-money/financial-accounts/out-of/outbound-payments) + to send funds to another party's external bank account or + [FinancialAccount](https://stripe.com/docs/api#financial_accounts). To + send money to an account belonging to the same user, use an + [OutboundTransfer](https://stripe.com/docs/api#outbound_transfers). + + + Simulate OutboundPayment state changes with the + `/v1/test_helpers/treasury/outbound_payments` endpoints. These methods + can only be called on test mode objects. + + + Related guide: [Moving money with Treasury using OutboundPayment + objects](https://docs.stripe.com/docs/treasury/moving-money/financial-accounts/out-of/outbound-payments) + properties: + amount: + description: Amount (in cents) transferred. + type: integer + cancelable: + description: 'Returns `true` if the object can be canceled, and `false` otherwise.' + type: boolean + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + customer: + description: >- + ID of the [customer](https://stripe.com/docs/api/customers) to whom + an OutboundPayment is sent. + maxLength: 5000 + nullable: true + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + destination_payment_method: + description: >- + The PaymentMethod via which an OutboundPayment is sent. This field + can be empty if the OutboundPayment was created using + `destination_payment_method_data`. + maxLength: 5000 + nullable: true + type: string + destination_payment_method_details: + anyOf: + - $ref: '#/components/schemas/outbound_payments_payment_method_details' + description: Details about the PaymentMethod for an OutboundPayment. + nullable: true + end_user_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_outbound_payments_resource_outbound_payment_resource_end_user_details + description: Details about the end user. + nullable: true + expected_arrival_date: + description: >- + The date when funds are expected to arrive in the destination + account. + format: unix-time + type: integer + financial_account: + description: The FinancialAccount that funds were pulled from. + maxLength: 5000 + type: string + hosted_regulatory_receipt_url: + description: >- + A [hosted transaction + receipt](https://stripe.com/docs/treasury/moving-money/regulatory-receipts) + URL that is provided when money movement is considered regulated + under Stripe's money transmission licenses. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.outbound_payment + type: string + returned_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_outbound_payments_resource_returned_status + description: >- + Details about a returned OutboundPayment. Only set when the status + is `returned`. + nullable: true + statement_descriptor: + description: >- + The description that appears on the receiving end for an + OutboundPayment (for example, bank statement for external bank + transfer). + maxLength: 5000 + type: string + status: + description: >- + Current status of the OutboundPayment: `processing`, `failed`, + `posted`, `returned`, `canceled`. An OutboundPayment is `processing` + if it has been created and is pending. The status changes to + `posted` once the OutboundPayment has been "confirmed" and funds + have left the account, or to `failed` or `canceled`. If an + OutboundPayment fails to arrive at its destination, its status will + change to `returned`. + enum: + - canceled + - failed + - posted + - processing + - returned + type: string + status_transitions: + $ref: >- + #/components/schemas/treasury_outbound_payments_resource_outbound_payment_resource_status_transitions + tracking_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_outbound_payments_resource_outbound_payment_resource_tracking_details + description: Details about network-specific tracking information if available. + nullable: true + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - amount + - cancelable + - created + - currency + - expected_arrival_date + - financial_account + - id + - livemode + - metadata + - object + - statement_descriptor + - status + - status_transitions + - transaction + title: TreasuryOutboundPaymentsResourceOutboundPayment + type: object + x-expandableFields: + - destination_payment_method_details + - end_user_details + - returned_details + - status_transitions + - tracking_details + - transaction + x-resourceId: treasury.outbound_payment + treasury.outbound_transfer: + description: >- + Use + [OutboundTransfers](https://docs.stripe.com/docs/treasury/moving-money/financial-accounts/out-of/outbound-transfers) + to transfer funds from a + [FinancialAccount](https://stripe.com/docs/api#financial_accounts) to a + PaymentMethod belonging to the same entity. To send funds to a different + party, use + [OutboundPayments](https://stripe.com/docs/api#outbound_payments) + instead. You can send funds over ACH rails or through a domestic wire + transfer to a user's own external bank account. + + + Simulate OutboundTransfer state changes with the + `/v1/test_helpers/treasury/outbound_transfers` endpoints. These methods + can only be called on test mode objects. + + + Related guide: [Moving money with Treasury using OutboundTransfer + objects](https://docs.stripe.com/docs/treasury/moving-money/financial-accounts/out-of/outbound-transfers) + properties: + amount: + description: Amount (in cents) transferred. + type: integer + cancelable: + description: 'Returns `true` if the object can be canceled, and `false` otherwise.' + type: boolean + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + nullable: true + type: string + destination_payment_method: + description: >- + The PaymentMethod used as the payment instrument for an + OutboundTransfer. + maxLength: 5000 + nullable: true + type: string + destination_payment_method_details: + $ref: '#/components/schemas/outbound_transfers_payment_method_details' + expected_arrival_date: + description: >- + The date when funds are expected to arrive in the destination + account. + format: unix-time + type: integer + financial_account: + description: The FinancialAccount that funds were pulled from. + maxLength: 5000 + type: string + hosted_regulatory_receipt_url: + description: >- + A [hosted transaction + receipt](https://stripe.com/docs/treasury/moving-money/regulatory-receipts) + URL that is provided when money movement is considered regulated + under Stripe's money transmission licenses. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.outbound_transfer + type: string + returned_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_outbound_transfers_resource_returned_details + description: >- + Details about a returned OutboundTransfer. Only set when the status + is `returned`. + nullable: true + statement_descriptor: + description: >- + Information about the OutboundTransfer to be sent to the recipient + account. + maxLength: 5000 + type: string + status: + description: >- + Current status of the OutboundTransfer: `processing`, `failed`, + `canceled`, `posted`, `returned`. An OutboundTransfer is + `processing` if it has been created and is pending. The status + changes to `posted` once the OutboundTransfer has been "confirmed" + and funds have left the account, or to `failed` or `canceled`. If an + OutboundTransfer fails to arrive at its destination, its status will + change to `returned`. + enum: + - canceled + - failed + - posted + - processing + - returned + type: string + status_transitions: + $ref: >- + #/components/schemas/treasury_outbound_transfers_resource_status_transitions + tracking_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_outbound_transfers_resource_outbound_transfer_resource_tracking_details + description: Details about network-specific tracking information if available. + nullable: true + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - amount + - cancelable + - created + - currency + - destination_payment_method_details + - expected_arrival_date + - financial_account + - id + - livemode + - metadata + - object + - statement_descriptor + - status + - status_transitions + - transaction + title: TreasuryOutboundTransfersResourceOutboundTransfer + type: object + x-expandableFields: + - destination_payment_method_details + - returned_details + - status_transitions + - tracking_details + - transaction + x-resourceId: treasury.outbound_transfer + treasury.received_credit: + description: >- + ReceivedCredits represent funds sent to a + [FinancialAccount](https://stripe.com/docs/api#financial_accounts) (for + example, via ACH or wire). These money movements are not initiated from + the FinancialAccount. + properties: + amount: + description: Amount (in cents) transferred. + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + type: string + failure_code: + description: >- + Reason for the failure. A ReceivedCredit might fail because the + receiving FinancialAccount is closed or frozen. + enum: + - account_closed + - account_frozen + - international_transaction + - other + nullable: true + type: string + x-stripeBypassValidation: true + financial_account: + description: The FinancialAccount that received the funds. + maxLength: 5000 + nullable: true + type: string + hosted_regulatory_receipt_url: + description: >- + A [hosted transaction + receipt](https://stripe.com/docs/treasury/moving-money/regulatory-receipts) + URL that is provided when money movement is considered regulated + under Stripe's money transmission licenses. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + initiating_payment_method_details: + $ref: >- + #/components/schemas/treasury_shared_resource_initiating_payment_method_details_initiating_payment_method_details + linked_flows: + $ref: '#/components/schemas/treasury_received_credits_resource_linked_flows' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + network: + description: The rails used to send the funds. + enum: + - ach + - card + - stripe + - us_domestic_wire + type: string + x-stripeBypassValidation: true + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.received_credit + type: string + reversal_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_received_credits_resource_reversal_details + description: Details describing when a ReceivedCredit may be reversed. + nullable: true + status: + description: >- + Status of the ReceivedCredit. ReceivedCredits are created either + `succeeded` (approved) or `failed` (declined). If a ReceivedCredit + is declined, the failure reason can be found in the `failure_code` + field. + enum: + - failed + - succeeded + type: string + x-stripeBypassValidation: true + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - amount + - created + - currency + - description + - id + - initiating_payment_method_details + - linked_flows + - livemode + - network + - object + - status + title: TreasuryReceivedCreditsResourceReceivedCredit + type: object + x-expandableFields: + - initiating_payment_method_details + - linked_flows + - reversal_details + - transaction + x-resourceId: treasury.received_credit + treasury.received_debit: + description: >- + ReceivedDebits represent funds pulled from a + [FinancialAccount](https://stripe.com/docs/api#financial_accounts). + These are not initiated from the FinancialAccount. + properties: + amount: + description: Amount (in cents) transferred. + type: integer + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + type: string + failure_code: + description: >- + Reason for the failure. A ReceivedDebit might fail because the + FinancialAccount doesn't have sufficient funds, is closed, or is + frozen. + enum: + - account_closed + - account_frozen + - insufficient_funds + - international_transaction + - other + nullable: true + type: string + financial_account: + description: The FinancialAccount that funds were pulled from. + maxLength: 5000 + nullable: true + type: string + hosted_regulatory_receipt_url: + description: >- + A [hosted transaction + receipt](https://stripe.com/docs/treasury/moving-money/regulatory-receipts) + URL that is provided when money movement is considered regulated + under Stripe's money transmission licenses. + maxLength: 5000 + nullable: true + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + initiating_payment_method_details: + $ref: >- + #/components/schemas/treasury_shared_resource_initiating_payment_method_details_initiating_payment_method_details + linked_flows: + $ref: '#/components/schemas/treasury_received_debits_resource_linked_flows' + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + network: + description: The network used for the ReceivedDebit. + enum: + - ach + - card + - stripe + type: string + x-stripeBypassValidation: true + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.received_debit + type: string + reversal_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_received_debits_resource_reversal_details + description: Details describing when a ReceivedDebit might be reversed. + nullable: true + status: + description: >- + Status of the ReceivedDebit. ReceivedDebits are created with a + status of either `succeeded` (approved) or `failed` (declined). The + failure reason can be found under the `failure_code`. + enum: + - failed + - succeeded + type: string + x-stripeBypassValidation: true + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + nullable: true + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - amount + - created + - currency + - description + - id + - linked_flows + - livemode + - network + - object + - status + title: TreasuryReceivedDebitsResourceReceivedDebit + type: object + x-expandableFields: + - initiating_payment_method_details + - linked_flows + - reversal_details + - transaction + x-resourceId: treasury.received_debit + treasury.transaction: + description: >- + Transactions represent changes to a + [FinancialAccount's](https://stripe.com/docs/api#financial_accounts) + balance. + properties: + amount: + description: Amount (in cents) transferred. + type: integer + balance_impact: + $ref: '#/components/schemas/treasury_transactions_resource_balance_impact' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. + maxLength: 5000 + type: string + entries: + description: >- + A list of TransactionEntries that are part of this Transaction. This + cannot be expanded in any list endpoints. + nullable: true + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/treasury.transaction_entry' + type: array + has_more: + description: >- + True if this list has another page of items after this one that + can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/treasury/transaction_entries + type: string + required: + - data + - has_more + - object + - url + title: TreasuryTransactionsResourceTransactionEntryList + type: object + x-expandableFields: + - data + financial_account: + description: The FinancialAccount associated with this object. + maxLength: 5000 + type: string + flow: + description: ID of the flow that created the Transaction. + maxLength: 5000 + nullable: true + type: string + flow_details: + anyOf: + - $ref: '#/components/schemas/treasury_transactions_resource_flow_details' + description: Details of the flow that created the Transaction. + nullable: true + flow_type: + description: Type of the flow that created the Transaction. + enum: + - credit_reversal + - debit_reversal + - inbound_transfer + - issuing_authorization + - other + - outbound_payment + - outbound_transfer + - received_credit + - received_debit + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.transaction + type: string + status: + description: Status of the Transaction. + enum: + - open + - posted + - void + type: string + status_transitions: + $ref: >- + #/components/schemas/treasury_transactions_resource_abstract_transaction_resource_status_transitions + required: + - amount + - balance_impact + - created + - currency + - description + - financial_account + - flow_type + - id + - livemode + - object + - status + - status_transitions + title: TreasuryTransactionsResourceTransaction + type: object + x-expandableFields: + - balance_impact + - entries + - flow_details + - status_transitions + x-resourceId: treasury.transaction + treasury.transaction_entry: + description: >- + TransactionEntries represent individual units of money movements within + a single [Transaction](https://stripe.com/docs/api#transactions). + properties: + balance_impact: + $ref: '#/components/schemas/treasury_transactions_resource_balance_impact' + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + effective_at: + description: >- + When the TransactionEntry will impact the FinancialAccount's + balance. + format: unix-time + type: integer + financial_account: + description: The FinancialAccount associated with this object. + maxLength: 5000 + type: string + flow: + description: Token of the flow associated with the TransactionEntry. + maxLength: 5000 + nullable: true + type: string + flow_details: + anyOf: + - $ref: '#/components/schemas/treasury_transactions_resource_flow_details' + description: Details of the flow associated with the TransactionEntry. + nullable: true + flow_type: + description: Type of the flow associated with the TransactionEntry. + enum: + - credit_reversal + - debit_reversal + - inbound_transfer + - issuing_authorization + - other + - outbound_payment + - outbound_transfer + - received_credit + - received_debit + type: string + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - treasury.transaction_entry + type: string + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + type: + description: The specific money movement that generated the TransactionEntry. + enum: + - credit_reversal + - credit_reversal_posting + - debit_reversal + - inbound_transfer + - inbound_transfer_return + - issuing_authorization_hold + - issuing_authorization_release + - other + - outbound_payment + - outbound_payment_cancellation + - outbound_payment_failure + - outbound_payment_posting + - outbound_payment_return + - outbound_transfer + - outbound_transfer_cancellation + - outbound_transfer_failure + - outbound_transfer_posting + - outbound_transfer_return + - received_credit + - received_debit + type: string + required: + - balance_impact + - created + - currency + - effective_at + - financial_account + - flow_type + - id + - livemode + - object + - transaction + - type + title: TreasuryTransactionsResourceTransactionEntry + type: object + x-expandableFields: + - balance_impact + - flow_details + - transaction + x-resourceId: treasury.transaction_entry + treasury_financial_accounts_resource_aba_record: + description: ABA Records contain U.S. bank account details per the ABA format. + properties: + account_holder_name: + description: The name of the person or business that owns the bank account. + maxLength: 5000 + type: string + account_number: + description: The account number. + maxLength: 5000 + nullable: true + type: string + account_number_last4: + description: The last four characters of the account number. + maxLength: 5000 + type: string + bank_name: + description: Name of the bank. + maxLength: 5000 + type: string + routing_number: + description: Routing number for the account. + maxLength: 5000 + type: string + required: + - account_holder_name + - account_number_last4 + - bank_name + - routing_number + title: TreasuryFinancialAccountsResourceABARecord + type: object + x-expandableFields: [] + treasury_financial_accounts_resource_aba_toggle_settings: + description: Toggle settings for enabling/disabling the ABA address feature + properties: + requested: + description: Whether the FinancialAccount should have the Feature. + type: boolean + status: + description: Whether the Feature is operational. + enum: + - active + - pending + - restricted + type: string + status_details: + description: >- + Additional details; includes at least one entry when the status is + not `active`. + items: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggles_setting_status_details + type: array + required: + - requested + - status + - status_details + title: TreasuryFinancialAccountsResourceAbaToggleSettings + type: object + x-expandableFields: + - status_details + treasury_financial_accounts_resource_balance: + description: Balance information for the FinancialAccount + properties: + cash: + additionalProperties: + type: integer + description: Funds the user can spend right now. + type: object + inbound_pending: + additionalProperties: + type: integer + description: 'Funds not spendable yet, but will become available at a later time.' + type: object + outbound_pending: + additionalProperties: + type: integer + description: >- + Funds in the account, but not spendable because they are being held + for pending outbound flows. + type: object + required: + - cash + - inbound_pending + - outbound_pending + title: TreasuryFinancialAccountsResourceBalance + type: object + x-expandableFields: [] + treasury_financial_accounts_resource_closed_status_details: + description: '' + properties: + reasons: + description: The array that contains reasons for a FinancialAccount closure. + items: + enum: + - account_rejected + - closed_by_platform + - other + type: string + type: array + required: + - reasons + title: TreasuryFinancialAccountsResourceClosedStatusDetails + type: object + x-expandableFields: [] + treasury_financial_accounts_resource_financial_address: + description: >- + FinancialAddresses contain identifying information that resolves to a + FinancialAccount. + properties: + aba: + $ref: '#/components/schemas/treasury_financial_accounts_resource_aba_record' + supported_networks: + description: The list of networks that the address supports + items: + enum: + - ach + - us_domestic_wire + type: string + x-stripeBypassValidation: true + type: array + type: + description: The type of financial address + enum: + - aba + type: string + x-stripeBypassValidation: true + required: + - type + title: TreasuryFinancialAccountsResourceFinancialAddress + type: object + x-expandableFields: + - aba + treasury_financial_accounts_resource_financial_addresses_features: + description: Settings related to Financial Addresses features on a Financial Account + properties: + aba: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_aba_toggle_settings + title: TreasuryFinancialAccountsResourceFinancialAddressesFeatures + type: object + x-expandableFields: + - aba + treasury_financial_accounts_resource_inbound_ach_toggle_settings: + description: Toggle settings for enabling/disabling an inbound ACH specific feature + properties: + requested: + description: Whether the FinancialAccount should have the Feature. + type: boolean + status: + description: Whether the Feature is operational. + enum: + - active + - pending + - restricted + type: string + status_details: + description: >- + Additional details; includes at least one entry when the status is + not `active`. + items: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggles_setting_status_details + type: array + required: + - requested + - status + - status_details + title: TreasuryFinancialAccountsResourceInboundAchToggleSettings + type: object + x-expandableFields: + - status_details + treasury_financial_accounts_resource_inbound_transfers: + description: >- + InboundTransfers contains inbound transfers features for a + FinancialAccount. + properties: + ach: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_inbound_ach_toggle_settings + title: TreasuryFinancialAccountsResourceInboundTransfers + type: object + x-expandableFields: + - ach + treasury_financial_accounts_resource_outbound_ach_toggle_settings: + description: Toggle settings for enabling/disabling an outbound ACH specific feature + properties: + requested: + description: Whether the FinancialAccount should have the Feature. + type: boolean + status: + description: Whether the Feature is operational. + enum: + - active + - pending + - restricted + type: string + status_details: + description: >- + Additional details; includes at least one entry when the status is + not `active`. + items: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggles_setting_status_details + type: array + required: + - requested + - status + - status_details + title: TreasuryFinancialAccountsResourceOutboundAchToggleSettings + type: object + x-expandableFields: + - status_details + treasury_financial_accounts_resource_outbound_payments: + description: Settings related to Outbound Payments features on a Financial Account + properties: + ach: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_outbound_ach_toggle_settings + us_domestic_wire: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggle_settings + title: TreasuryFinancialAccountsResourceOutboundPayments + type: object + x-expandableFields: + - ach + - us_domestic_wire + treasury_financial_accounts_resource_outbound_transfers: + description: >- + OutboundTransfers contains outbound transfers features for a + FinancialAccount. + properties: + ach: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_outbound_ach_toggle_settings + us_domestic_wire: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggle_settings + title: TreasuryFinancialAccountsResourceOutboundTransfers + type: object + x-expandableFields: + - ach + - us_domestic_wire + treasury_financial_accounts_resource_platform_restrictions: + description: >- + Restrictions that a Connect Platform has placed on this + FinancialAccount. + properties: + inbound_flows: + description: Restricts all inbound money movement. + enum: + - restricted + - unrestricted + nullable: true + type: string + outbound_flows: + description: Restricts all outbound money movement. + enum: + - restricted + - unrestricted + nullable: true + type: string + title: TreasuryFinancialAccountsResourcePlatformRestrictions + type: object + x-expandableFields: [] + treasury_financial_accounts_resource_status_details: + description: '' + properties: + closed: + anyOf: + - $ref: >- + #/components/schemas/treasury_financial_accounts_resource_closed_status_details + description: Details related to the closure of this FinancialAccount + nullable: true + title: TreasuryFinancialAccountsResourceStatusDetails + type: object + x-expandableFields: + - closed + treasury_financial_accounts_resource_toggle_settings: + description: Toggle settings for enabling/disabling a feature + properties: + requested: + description: Whether the FinancialAccount should have the Feature. + type: boolean + status: + description: Whether the Feature is operational. + enum: + - active + - pending + - restricted + type: string + status_details: + description: >- + Additional details; includes at least one entry when the status is + not `active`. + items: + $ref: >- + #/components/schemas/treasury_financial_accounts_resource_toggles_setting_status_details + type: array + required: + - requested + - status + - status_details + title: TreasuryFinancialAccountsResourceToggleSettings + type: object + x-expandableFields: + - status_details + treasury_financial_accounts_resource_toggles_setting_status_details: + description: Additional details on the FinancialAccount Features information. + properties: + code: + description: Represents the reason why the status is `pending` or `restricted`. + enum: + - activating + - capability_not_requested + - financial_account_closed + - rejected_other + - rejected_unsupported_business + - requirements_past_due + - requirements_pending_verification + - restricted_by_platform + - restricted_other + type: string + x-stripeBypassValidation: true + resolution: + description: >- + Represents what the user should do, if anything, to activate the + Feature. + enum: + - contact_stripe + - provide_information + - remove_restriction + nullable: true + type: string + x-stripeBypassValidation: true + restriction: + description: The `platform_restrictions` that are restricting this Feature. + enum: + - inbound_flows + - outbound_flows + type: string + x-stripeBypassValidation: true + required: + - code + title: TreasuryFinancialAccountsResourceTogglesSettingStatusDetails + type: object + x-expandableFields: [] + treasury_inbound_transfers_resource_failure_details: + description: '' + properties: + code: + description: Reason for the failure. + enum: + - account_closed + - account_frozen + - bank_account_restricted + - bank_ownership_changed + - debit_not_authorized + - incorrect_account_holder_address + - incorrect_account_holder_name + - incorrect_account_holder_tax_id + - insufficient_funds + - invalid_account_number + - invalid_currency + - no_account + - other + type: string + required: + - code + title: TreasuryInboundTransfersResourceFailureDetails + type: object + x-expandableFields: [] + treasury_inbound_transfers_resource_inbound_transfer_resource_linked_flows: + description: '' + properties: + received_debit: + description: >- + If funds for this flow were returned after the flow went to the + `succeeded` state, this field contains a reference to the + ReceivedDebit return. + maxLength: 5000 + nullable: true + type: string + title: TreasuryInboundTransfersResourceInboundTransferResourceLinkedFlows + type: object + x-expandableFields: [] + treasury_inbound_transfers_resource_inbound_transfer_resource_status_transitions: + description: '' + properties: + canceled_at: + description: >- + Timestamp describing when an InboundTransfer changed status to + `canceled`. + format: unix-time + nullable: true + type: integer + failed_at: + description: >- + Timestamp describing when an InboundTransfer changed status to + `failed`. + format: unix-time + nullable: true + type: integer + succeeded_at: + description: >- + Timestamp describing when an InboundTransfer changed status to + `succeeded`. + format: unix-time + nullable: true + type: integer + title: TreasuryInboundTransfersResourceInboundTransferResourceStatusTransitions + type: object + x-expandableFields: [] + treasury_outbound_payments_resource_ach_tracking_details: + description: '' + properties: + trace_id: + description: >- + ACH trace ID of the OutboundPayment for payments sent over the `ach` + network. + maxLength: 5000 + type: string + required: + - trace_id + title: TreasuryOutboundPaymentsResourceACHTrackingDetails + type: object + x-expandableFields: [] + treasury_outbound_payments_resource_outbound_payment_resource_end_user_details: + description: '' + properties: + ip_address: + description: >- + IP address of the user initiating the OutboundPayment. Set if + `present` is set to `true`. IP address collection is required for + risk and compliance reasons. This will be used to help determine if + the OutboundPayment is authorized or should be blocked. + maxLength: 5000 + nullable: true + type: string + present: + description: >- + `true` if the OutboundPayment creation request is being made on + behalf of an end user by a platform. Otherwise, `false`. + type: boolean + required: + - present + title: TreasuryOutboundPaymentsResourceOutboundPaymentResourceEndUserDetails + type: object + x-expandableFields: [] + treasury_outbound_payments_resource_outbound_payment_resource_status_transitions: + description: '' + properties: + canceled_at: + description: >- + Timestamp describing when an OutboundPayment changed status to + `canceled`. + format: unix-time + nullable: true + type: integer + failed_at: + description: >- + Timestamp describing when an OutboundPayment changed status to + `failed`. + format: unix-time + nullable: true + type: integer + posted_at: + description: >- + Timestamp describing when an OutboundPayment changed status to + `posted`. + format: unix-time + nullable: true + type: integer + returned_at: + description: >- + Timestamp describing when an OutboundPayment changed status to + `returned`. + format: unix-time + nullable: true + type: integer + title: TreasuryOutboundPaymentsResourceOutboundPaymentResourceStatusTransitions + type: object + x-expandableFields: [] + treasury_outbound_payments_resource_outbound_payment_resource_tracking_details: + description: '' + properties: + ach: + $ref: >- + #/components/schemas/treasury_outbound_payments_resource_ach_tracking_details + type: + description: The US bank account network used to send funds. + enum: + - ach + - us_domestic_wire + type: string + us_domestic_wire: + $ref: >- + #/components/schemas/treasury_outbound_payments_resource_us_domestic_wire_tracking_details + required: + - type + title: TreasuryOutboundPaymentsResourceOutboundPaymentResourceTrackingDetails + type: object + x-expandableFields: + - ach + - us_domestic_wire + treasury_outbound_payments_resource_returned_status: + description: '' + properties: + code: + description: Reason for the return. + enum: + - account_closed + - account_frozen + - bank_account_restricted + - bank_ownership_changed + - declined + - incorrect_account_holder_name + - invalid_account_number + - invalid_currency + - no_account + - other + type: string + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - code + - transaction + title: TreasuryOutboundPaymentsResourceReturnedStatus + type: object + x-expandableFields: + - transaction + treasury_outbound_payments_resource_us_domestic_wire_tracking_details: + description: '' + properties: + chips: + description: >- + CHIPS System Sequence Number (SSN) of the OutboundPayment for + payments sent over the `us_domestic_wire` network. + maxLength: 5000 + nullable: true + type: string + imad: + description: >- + IMAD of the OutboundPayment for payments sent over the + `us_domestic_wire` network. + maxLength: 5000 + nullable: true + type: string + omad: + description: >- + OMAD of the OutboundPayment for payments sent over the + `us_domestic_wire` network. + maxLength: 5000 + nullable: true + type: string + title: TreasuryOutboundPaymentsResourceUSDomesticWireTrackingDetails + type: object + x-expandableFields: [] + treasury_outbound_transfers_resource_ach_tracking_details: + description: '' + properties: + trace_id: + description: >- + ACH trace ID of the OutboundTransfer for transfers sent over the + `ach` network. + maxLength: 5000 + type: string + required: + - trace_id + title: TreasuryOutboundTransfersResourceACHTrackingDetails + type: object + x-expandableFields: [] + treasury_outbound_transfers_resource_outbound_transfer_resource_tracking_details: + description: '' + properties: + ach: + $ref: >- + #/components/schemas/treasury_outbound_transfers_resource_ach_tracking_details + type: + description: The US bank account network used to send funds. + enum: + - ach + - us_domestic_wire + type: string + us_domestic_wire: + $ref: >- + #/components/schemas/treasury_outbound_transfers_resource_us_domestic_wire_tracking_details + required: + - type + title: TreasuryOutboundTransfersResourceOutboundTransferResourceTrackingDetails + type: object + x-expandableFields: + - ach + - us_domestic_wire + treasury_outbound_transfers_resource_returned_details: + description: '' + properties: + code: + description: Reason for the return. + enum: + - account_closed + - account_frozen + - bank_account_restricted + - bank_ownership_changed + - declined + - incorrect_account_holder_name + - invalid_account_number + - invalid_currency + - no_account + - other + type: string + transaction: + anyOf: + - maxLength: 5000 + type: string + - $ref: '#/components/schemas/treasury.transaction' + description: The Transaction associated with this object. + x-expansionResources: + oneOf: + - $ref: '#/components/schemas/treasury.transaction' + required: + - code + - transaction + title: TreasuryOutboundTransfersResourceReturnedDetails + type: object + x-expandableFields: + - transaction + treasury_outbound_transfers_resource_status_transitions: + description: '' + properties: + canceled_at: + description: >- + Timestamp describing when an OutboundTransfer changed status to + `canceled` + format: unix-time + nullable: true + type: integer + failed_at: + description: >- + Timestamp describing when an OutboundTransfer changed status to + `failed` + format: unix-time + nullable: true + type: integer + posted_at: + description: >- + Timestamp describing when an OutboundTransfer changed status to + `posted` + format: unix-time + nullable: true + type: integer + returned_at: + description: >- + Timestamp describing when an OutboundTransfer changed status to + `returned` + format: unix-time + nullable: true + type: integer + title: TreasuryOutboundTransfersResourceStatusTransitions + type: object + x-expandableFields: [] + treasury_outbound_transfers_resource_us_domestic_wire_tracking_details: + description: '' + properties: + chips: + description: >- + CHIPS System Sequence Number (SSN) of the OutboundTransfer for + transfers sent over the `us_domestic_wire` network. + maxLength: 5000 + nullable: true + type: string + imad: + description: >- + IMAD of the OutboundTransfer for transfers sent over the + `us_domestic_wire` network. + maxLength: 5000 + nullable: true + type: string + omad: + description: >- + OMAD of the OutboundTransfer for transfers sent over the + `us_domestic_wire` network. + maxLength: 5000 + nullable: true + type: string + title: TreasuryOutboundTransfersResourceUSDomesticWireTrackingDetails + type: object + x-expandableFields: [] + treasury_received_credits_resource_linked_flows: + description: '' + properties: + credit_reversal: + description: >- + The CreditReversal created as a result of this ReceivedCredit being + reversed. + maxLength: 5000 + nullable: true + type: string + issuing_authorization: + description: >- + Set if the ReceivedCredit was created due to an [Issuing + Authorization](https://stripe.com/docs/api#issuing_authorizations) + object. + maxLength: 5000 + nullable: true + type: string + issuing_transaction: + description: >- + Set if the ReceivedCredit is also viewable as an [Issuing + transaction](https://stripe.com/docs/api#issuing_transactions) + object. + maxLength: 5000 + nullable: true + type: string + source_flow: + description: >- + ID of the source flow. Set if `network` is `stripe` and the source + flow is visible to the user. Examples of source flows include + OutboundPayments, payouts, or CreditReversals. + maxLength: 5000 + nullable: true + type: string + source_flow_details: + anyOf: + - $ref: >- + #/components/schemas/treasury_received_credits_resource_source_flows_details + description: The expandable object of the source flow. + nullable: true + source_flow_type: + description: >- + The type of flow that originated the ReceivedCredit (for example, + `outbound_payment`). + maxLength: 5000 + nullable: true + type: string + title: TreasuryReceivedCreditsResourceLinkedFlows + type: object + x-expandableFields: + - source_flow_details + treasury_received_credits_resource_reversal_details: + description: '' + properties: + deadline: + description: Time before which a ReceivedCredit can be reversed. + format: unix-time + nullable: true + type: integer + restricted_reason: + description: Set if a ReceivedCredit cannot be reversed. + enum: + - already_reversed + - deadline_passed + - network_restricted + - other + - source_flow_restricted + nullable: true + type: string + title: TreasuryReceivedCreditsResourceReversalDetails + type: object + x-expandableFields: [] + treasury_received_credits_resource_source_flows_details: + description: '' + properties: + credit_reversal: + $ref: '#/components/schemas/treasury.credit_reversal' + outbound_payment: + $ref: '#/components/schemas/treasury.outbound_payment' + outbound_transfer: + $ref: '#/components/schemas/treasury.outbound_transfer' + payout: + $ref: '#/components/schemas/payout' + type: + description: The type of the source flow that originated the ReceivedCredit. + enum: + - credit_reversal + - other + - outbound_payment + - outbound_transfer + - payout + type: string + required: + - type + title: TreasuryReceivedCreditsResourceSourceFlowsDetails + type: object + x-expandableFields: + - credit_reversal + - outbound_payment + - outbound_transfer + - payout + treasury_received_credits_resource_status_transitions: + description: '' + properties: + posted_at: + description: >- + Timestamp describing when the CreditReversal changed status to + `posted` + format: unix-time + nullable: true + type: integer + title: TreasuryReceivedCreditsResourceStatusTransitions + type: object + x-expandableFields: [] + treasury_received_debits_resource_debit_reversal_linked_flows: + description: '' + properties: + issuing_dispute: + description: >- + Set if there is an Issuing dispute associated with the + DebitReversal. + maxLength: 5000 + nullable: true + type: string + title: TreasuryReceivedDebitsResourceDebitReversalLinkedFlows + type: object + x-expandableFields: [] + treasury_received_debits_resource_linked_flows: + description: '' + properties: + debit_reversal: + description: >- + The DebitReversal created as a result of this ReceivedDebit being + reversed. + maxLength: 5000 + nullable: true + type: string + inbound_transfer: + description: >- + Set if the ReceivedDebit is associated with an InboundTransfer's + return of funds. + maxLength: 5000 + nullable: true + type: string + issuing_authorization: + description: >- + Set if the ReceivedDebit was created due to an [Issuing + Authorization](https://stripe.com/docs/api#issuing_authorizations) + object. + maxLength: 5000 + nullable: true + type: string + issuing_transaction: + description: >- + Set if the ReceivedDebit is also viewable as an [Issuing + Dispute](https://stripe.com/docs/api#issuing_disputes) object. + maxLength: 5000 + nullable: true + type: string + payout: + description: >- + Set if the ReceivedDebit was created due to a + [Payout](https://stripe.com/docs/api#payouts) object. + maxLength: 5000 + nullable: true + type: string + title: TreasuryReceivedDebitsResourceLinkedFlows + type: object + x-expandableFields: [] + treasury_received_debits_resource_reversal_details: + description: '' + properties: + deadline: + description: Time before which a ReceivedDebit can be reversed. + format: unix-time + nullable: true + type: integer + restricted_reason: + description: Set if a ReceivedDebit can't be reversed. + enum: + - already_reversed + - deadline_passed + - network_restricted + - other + - source_flow_restricted + nullable: true + type: string + title: TreasuryReceivedDebitsResourceReversalDetails + type: object + x-expandableFields: [] + treasury_received_debits_resource_status_transitions: + description: '' + properties: + completed_at: + description: >- + Timestamp describing when the DebitReversal changed status to + `completed`. + format: unix-time + nullable: true + type: integer + title: TreasuryReceivedDebitsResourceStatusTransitions + type: object + x-expandableFields: [] + treasury_shared_resource_billing_details: + description: '' + properties: + address: + $ref: '#/components/schemas/address' + email: + description: Email address. + maxLength: 5000 + nullable: true + type: string + name: + description: Full name. + maxLength: 5000 + nullable: true + type: string + required: + - address + title: TreasurySharedResourceBillingDetails + type: object + x-expandableFields: + - address + treasury_shared_resource_initiating_payment_method_details_initiating_payment_method_details: + description: '' + properties: + balance: + description: Set when `type` is `balance`. + enum: + - payments + type: string + billing_details: + $ref: '#/components/schemas/treasury_shared_resource_billing_details' + financial_account: + $ref: >- + #/components/schemas/received_payment_method_details_financial_account + issuing_card: + description: >- + Set when `type` is `issuing_card`. This is an [Issuing + Card](https://stripe.com/docs/api#issuing_cards) ID. + maxLength: 5000 + type: string + type: + description: >- + Polymorphic type matching the originating money movement's source. + This can be an external account, a Stripe balance, or a + FinancialAccount. + enum: + - balance + - financial_account + - issuing_card + - stripe + - us_bank_account + type: string + x-stripeBypassValidation: true + us_bank_account: + $ref: >- + #/components/schemas/treasury_shared_resource_initiating_payment_method_details_us_bank_account + required: + - billing_details + - type + title: >- + TreasurySharedResourceInitiatingPaymentMethodDetailsInitiatingPaymentMethodDetails + type: object + x-expandableFields: + - billing_details + - financial_account + - us_bank_account + treasury_shared_resource_initiating_payment_method_details_us_bank_account: + description: '' + properties: + bank_name: + description: Bank name. + maxLength: 5000 + nullable: true + type: string + last4: + description: The last four digits of the bank account number. + maxLength: 5000 + nullable: true + type: string + routing_number: + description: The routing number for the bank account. + maxLength: 5000 + nullable: true + type: string + title: TreasurySharedResourceInitiatingPaymentMethodDetailsUSBankAccount + type: object + x-expandableFields: [] + treasury_transactions_resource_abstract_transaction_resource_status_transitions: + description: '' + properties: + posted_at: + description: >- + Timestamp describing when the Transaction changed status to + `posted`. + format: unix-time + nullable: true + type: integer + void_at: + description: Timestamp describing when the Transaction changed status to `void`. + format: unix-time + nullable: true + type: integer + title: TreasuryTransactionsResourceAbstractTransactionResourceStatusTransitions + type: object + x-expandableFields: [] + treasury_transactions_resource_balance_impact: + description: Change to a FinancialAccount's balance + properties: + cash: + description: The change made to funds the user can spend right now. + type: integer + inbound_pending: + description: >- + The change made to funds that are not spendable yet, but will become + available at a later time. + type: integer + outbound_pending: + description: >- + The change made to funds in the account, but not spendable because + they are being held for pending outbound flows. + type: integer + required: + - cash + - inbound_pending + - outbound_pending + title: TreasuryTransactionsResourceBalanceImpact + type: object + x-expandableFields: [] + treasury_transactions_resource_flow_details: + description: '' + properties: + credit_reversal: + $ref: '#/components/schemas/treasury.credit_reversal' + debit_reversal: + $ref: '#/components/schemas/treasury.debit_reversal' + inbound_transfer: + $ref: '#/components/schemas/treasury.inbound_transfer' + issuing_authorization: + $ref: '#/components/schemas/issuing.authorization' + outbound_payment: + $ref: '#/components/schemas/treasury.outbound_payment' + outbound_transfer: + $ref: '#/components/schemas/treasury.outbound_transfer' + received_credit: + $ref: '#/components/schemas/treasury.received_credit' + received_debit: + $ref: '#/components/schemas/treasury.received_debit' + type: + description: >- + Type of the flow that created the Transaction. Set to the same value + as `flow_type`. + enum: + - credit_reversal + - debit_reversal + - inbound_transfer + - issuing_authorization + - other + - outbound_payment + - outbound_transfer + - received_credit + - received_debit + type: string + required: + - type + title: TreasuryTransactionsResourceFlowDetails + type: object + x-expandableFields: + - credit_reversal + - debit_reversal + - inbound_transfer + - issuing_authorization + - outbound_payment + - outbound_transfer + - received_credit + - received_debit + us_bank_account_networks: + description: '' + properties: + preferred: + description: The preferred network. + maxLength: 5000 + nullable: true + type: string + supported: + description: All supported networks. + items: + enum: + - ach + - us_domestic_wire + type: string + type: array + required: + - supported + title: us_bank_account_networks + type: object + x-expandableFields: [] + verification_session_redaction: + description: '' + properties: + status: + description: >- + Indicates whether this object and its related objects have been + redacted or not. + enum: + - processing + - redacted + type: string + required: + - status + title: verification_session_redaction + type: object + x-expandableFields: [] + webhook_endpoint: + description: >- + You can configure [webhook endpoints](https://docs.stripe.com/webhooks/) + via the API to be + + notified about events that happen in your Stripe account or connected + + accounts. + + + Most users configure webhooks from [the + dashboard](https://dashboard.stripe.com/webhooks), which provides a user + interface for registering and testing your webhook endpoints. + + + Related guide: [Setting up + webhooks](https://docs.stripe.com/webhooks/configure) + properties: + api_version: + description: The API version events are rendered as for this webhook endpoint. + maxLength: 5000 + nullable: true + type: string + application: + description: The ID of the associated Connect application. + maxLength: 5000 + nullable: true + type: string + created: + description: >- + Time at which the object was created. Measured in seconds since the + Unix epoch. + format: unix-time + type: integer + description: + description: An optional description of what the webhook is used for. + maxLength: 5000 + nullable: true + type: string + enabled_events: + description: >- + The list of events to enable for this endpoint. `['*']` indicates + that all events are enabled, except those that require explicit + selection. + items: + maxLength: 5000 + type: string + type: array + id: + description: Unique identifier for the object. + maxLength: 5000 + type: string + livemode: + description: >- + Has the value `true` if the object exists in live mode or the value + `false` if the object exists in test mode. + type: boolean + metadata: + additionalProperties: + maxLength: 500 + type: string + description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + type: object + object: + description: >- + String representing the object's type. Objects of the same type + share the same value. + enum: + - webhook_endpoint + type: string + secret: + description: >- + The endpoint's secret, used to generate [webhook + signatures](https://docs.stripe.com/webhooks/signatures). Only + returned at creation. + maxLength: 5000 + type: string + status: + description: The status of the webhook. It can be `enabled` or `disabled`. + maxLength: 5000 + type: string + url: + description: The URL of the webhook endpoint. + maxLength: 5000 + type: string + required: + - created + - enabled_events + - id + - livemode + - metadata + - object + - status + - url + title: NotificationWebhookEndpoint + type: object + x-expandableFields: [] + x-resourceId: webhook_endpoint + securitySchemes: + basicAuth: + description: >- + Basic HTTP authentication. Allowed headers-- Authorization: Basic + | Authorization: Basic + scheme: basic + type: http + bearerAuth: + bearerFormat: auth-scheme + description: >- + Bearer HTTP authentication. Allowed headers-- Authorization: Bearer + + scheme: bearer + type: http +info: + contact: + email: dev-platform@stripe.com + name: Stripe Dev Platform Team + url: 'https://stripe.com' + description: >- + The Stripe REST API. Please see https://stripe.com/docs/api for more + details. + termsOfService: 'https://stripe.com/us/terms/' + title: Stripe API + version: 2025-05-28.basil + x-stripeSpecFilename: spec3 +paths: + '/v1/coupons/{coupon}': + delete: + description: >- +

You can delete coupons via the coupon management page + of the Stripe dashboard. However, deleting a coupon does not affect any + customers who have already applied the coupon; it means that new + customers can’t redeem the coupon. You can also delete coupons via the + API.

+ operationId: DeleteCouponsCoupon + parameters: + - in: path + name: coupon + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/deleted_coupon' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete a coupon + get: + description:

Retrieves the coupon with the given ID.

+ operationId: GetCouponsCoupon + parameters: + - in: path + name: coupon + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/coupon' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a coupon + post: + description: >- +

Updates the metadata of a coupon. Other coupon details (currency, + duration, amount_off) are, by design, not editable.

+ operationId: PostCouponsCoupon + parameters: + - in: path + name: coupon + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + currency_options: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + currency_options: + additionalProperties: + properties: + amount_off: + type: integer + required: + - amount_off + title: currency_option + type: object + description: >- + Coupons defined in each available currency option (only + supported if the coupon is amount-based). Each key must be a + three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html) and + a [supported currency](https://stripe.com/docs/currencies). + type: object + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + name: + description: >- + Name of the coupon displayed to customers on, for instance + invoices, or receipts. By default the `id` is shown if + `name` is not set. + maxLength: 40 + type: string + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/coupon' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Update a coupon + /v1/credit_notes: + get: + description:

Returns a list of credit notes.

+ operationId: GetCreditNotes + parameters: + - description: >- + Only return credit notes that were created during the given date + interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + Only return credit notes for the customer specified by this customer + ID. + in: query + name: customer + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + Only return credit notes for the invoice specified by this invoice + ID. + in: query + name: invoice + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/credit_note' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: CreditNotesList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all credit notes + post: + description: >- +

Issue a credit note to adjust the amount of a finalized invoice. A + credit note will first reduce the invoice’s + amount_remaining (and amount_due), but not + below zero. + + This amount is indicated by the credit note’s + pre_payment_amount. The excess amount is indicated by + post_payment_amount, and it can result in any combination + of the following:

+ + +
    + +
  • Refunds: create a new refund (using refund_amount) or + link existing refunds (using refunds).
  • + +
  • Customer balance credit: credit the customer’s balance (using + credit_amount) which will be automatically applied to their + next invoice when it’s finalized.
  • + +
  • Outside of Stripe credit: record the amount that is or will be + credited outside of Stripe (using out_of_band_amount).
  • + +
+ + +

The sum of refunds, customer balance credits, and outside of Stripe + credits must equal the post_payment_amount.

+ + +

You may issue multiple credit notes for an invoice. Each credit note + may increment the invoice’s + pre_payment_credit_notes_amount, + + post_payment_credit_notes_amount, or both, depending on the + invoice’s amount_remaining at the time of credit note + creation.

+ operationId: PostCreditNotes + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + lines: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + refunds: + explode: true + style: deepObject + shipping_cost: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + amount: + description: >- + The integer amount in cents (or local equivalent) + representing the total amount of the credit note. + type: integer + credit_amount: + description: >- + The integer amount in cents (or local equivalent) + representing the amount to credit the customer's balance, + which will be automatically applied to their next invoice. + type: integer + effective_at: + description: >- + The date when this credit note is in effect. Same as + `created` unless overwritten. When defined, this value + replaces the system-generated 'Date of issue' printed on the + credit note PDF. + format: unix-time + type: integer + email_type: + description: >- + Type of email to send to the customer, one of `credit_note` + or `none` and the default is `credit_note`. + enum: + - credit_note + - none + type: string + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + invoice: + description: ID of the invoice. + maxLength: 5000 + type: string + lines: + description: Line items that make up the credit note. + items: + properties: + amount: + type: integer + description: + maxLength: 5000 + type: string + invoice_line_item: + maxLength: 5000 + type: string + quantity: + type: integer + tax_amounts: + anyOf: + - items: + properties: + amount: + type: integer + tax_rate: + maxLength: 5000 + type: string + taxable_amount: + type: integer + required: + - amount + - tax_rate + - taxable_amount + title: tax_amount_with_tax_rate_param + type: object + type: array + - enum: + - '' + type: string + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + type: + enum: + - custom_line_item + - invoice_line_item + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - type + title: credit_note_line_item_params + type: object + type: array + memo: + description: The credit note's memo appears on the credit note PDF. + maxLength: 5000 + type: string + metadata: + additionalProperties: + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + type: object + out_of_band_amount: + description: >- + The integer amount in cents (or local equivalent) + representing the amount that is credited outside of Stripe. + type: integer + reason: + description: >- + Reason for issuing this credit note, one of `duplicate`, + `fraudulent`, `order_change`, or `product_unsatisfactory` + enum: + - duplicate + - fraudulent + - order_change + - product_unsatisfactory + type: string + refund_amount: + description: >- + The integer amount in cents (or local equivalent) + representing the amount to refund. If set, a refund will be + created for the charge associated with the invoice. + type: integer + refunds: + description: Refunds to link to this credit note. + items: + properties: + amount_refunded: + type: integer + refund: + type: string + title: credit_note_refund_params + type: object + type: array + shipping_cost: + description: >- + When shipping_cost contains the shipping_rate from the + invoice, the shipping_cost is included in the credit note. + properties: + shipping_rate: + maxLength: 5000 + type: string + title: credit_note_shipping_cost + type: object + required: + - invoice + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/credit_note' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a credit note + /v1/credit_notes/preview: + get: + description:

Get a preview of a credit note without creating it.

+ operationId: GetCreditNotesPreview + parameters: + - description: >- + The integer amount in cents (or local equivalent) representing the + total amount of the credit note. + in: query + name: amount + required: false + schema: + type: integer + style: form + - description: >- + The integer amount in cents (or local equivalent) representing the + amount to credit the customer's balance, which will be automatically + applied to their next invoice. + in: query + name: credit_amount + required: false + schema: + type: integer + style: form + - description: >- + The date when this credit note is in effect. Same as `created` + unless overwritten. When defined, this value replaces the + system-generated 'Date of issue' printed on the credit note PDF. + in: query + name: effective_at + required: false + schema: + format: unix-time + type: integer + style: form + - description: >- + Type of email to send to the customer, one of `credit_note` or + `none` and the default is `credit_note`. + in: query + name: email_type + required: false + schema: + enum: + - credit_note + - none + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: ID of the invoice. + in: query + name: invoice + required: true + schema: + maxLength: 5000 + type: string + style: form + - description: Line items that make up the credit note. + explode: true + in: query + name: lines + required: false + schema: + items: + properties: + amount: + type: integer + description: + maxLength: 5000 + type: string + invoice_line_item: + maxLength: 5000 + type: string + quantity: + type: integer + tax_amounts: + anyOf: + - items: + properties: + amount: + type: integer + tax_rate: + maxLength: 5000 + type: string + taxable_amount: + type: integer + required: + - amount + - tax_rate + - taxable_amount + title: tax_amount_with_tax_rate_param + type: object + type: array + - enum: + - '' + type: string + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + type: + enum: + - custom_line_item + - invoice_line_item + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - type + title: credit_note_line_item_params + type: object + type: array + style: deepObject + - description: The credit note's memo appears on the credit note PDF. + in: query + name: memo + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + Individual keys can be unset by posting an empty value to them. All + keys can be unset by posting an empty value to `metadata`. + explode: true + in: query + name: metadata + required: false + schema: + additionalProperties: + type: string + type: object + style: deepObject + - description: >- + The integer amount in cents (or local equivalent) representing the + amount that is credited outside of Stripe. + in: query + name: out_of_band_amount + required: false + schema: + type: integer + style: form + - description: >- + Reason for issuing this credit note, one of `duplicate`, + `fraudulent`, `order_change`, or `product_unsatisfactory` + in: query + name: reason + required: false + schema: + enum: + - duplicate + - fraudulent + - order_change + - product_unsatisfactory + type: string + style: form + - description: >- + The integer amount in cents (or local equivalent) representing the + amount to refund. If set, a refund will be created for the charge + associated with the invoice. + in: query + name: refund_amount + required: false + schema: + type: integer + style: form + - description: Refunds to link to this credit note. + explode: true + in: query + name: refunds + required: false + schema: + items: + properties: + amount_refunded: + type: integer + refund: + type: string + title: credit_note_refund_params + type: object + type: array + style: deepObject + - description: >- + When shipping_cost contains the shipping_rate from the invoice, the + shipping_cost is included in the credit note. + explode: true + in: query + name: shipping_cost + required: false + schema: + properties: + shipping_rate: + maxLength: 5000 + type: string + title: credit_note_shipping_cost + type: object + style: deepObject + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/credit_note' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Preview a credit note + /v1/credit_notes/preview/lines: + get: + description: >- +

When retrieving a credit note preview, you’ll get a + lines property containing the first handful of those + items. This URL you can retrieve the full (paginated) list of line + items.

+ operationId: GetCreditNotesPreviewLines + parameters: + - description: >- + The integer amount in cents (or local equivalent) representing the + total amount of the credit note. + in: query + name: amount + required: false + schema: + type: integer + style: form + - description: >- + The integer amount in cents (or local equivalent) representing the + amount to credit the customer's balance, which will be automatically + applied to their next invoice. + in: query + name: credit_amount + required: false + schema: + type: integer + style: form + - description: >- + The date when this credit note is in effect. Same as `created` + unless overwritten. When defined, this value replaces the + system-generated 'Date of issue' printed on the credit note PDF. + in: query + name: effective_at + required: false + schema: + format: unix-time + type: integer + style: form + - description: >- + Type of email to send to the customer, one of `credit_note` or + `none` and the default is `credit_note`. + in: query + name: email_type + required: false + schema: + enum: + - credit_note + - none + type: string + style: form + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: ID of the invoice. + in: query + name: invoice + required: true + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: Line items that make up the credit note. + explode: true + in: query + name: lines + required: false + schema: + items: + properties: + amount: + type: integer + description: + maxLength: 5000 + type: string + invoice_line_item: + maxLength: 5000 + type: string + quantity: + type: integer + tax_amounts: + anyOf: + - items: + properties: + amount: + type: integer + tax_rate: + maxLength: 5000 + type: string + taxable_amount: + type: integer + required: + - amount + - tax_rate + - taxable_amount + title: tax_amount_with_tax_rate_param + type: object + type: array + - enum: + - '' + type: string + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + type: + enum: + - custom_line_item + - invoice_line_item + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - type + title: credit_note_line_item_params + type: object + type: array + style: deepObject + - description: The credit note's memo appears on the credit note PDF. + in: query + name: memo + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + Set of [key-value pairs](https://stripe.com/docs/api/metadata) that + you can attach to an object. This can be useful for storing + additional information about the object in a structured format. + Individual keys can be unset by posting an empty value to them. All + keys can be unset by posting an empty value to `metadata`. + explode: true + in: query + name: metadata + required: false + schema: + additionalProperties: + type: string + type: object + style: deepObject + - description: >- + The integer amount in cents (or local equivalent) representing the + amount that is credited outside of Stripe. + in: query + name: out_of_band_amount + required: false + schema: + type: integer + style: form + - description: >- + Reason for issuing this credit note, one of `duplicate`, + `fraudulent`, `order_change`, or `product_unsatisfactory` + in: query + name: reason + required: false + schema: + enum: + - duplicate + - fraudulent + - order_change + - product_unsatisfactory + type: string + style: form + - description: >- + The integer amount in cents (or local equivalent) representing the + amount to refund. If set, a refund will be created for the charge + associated with the invoice. + in: query + name: refund_amount + required: false + schema: + type: integer + style: form + - description: Refunds to link to this credit note. + explode: true + in: query + name: refunds + required: false + schema: + items: + properties: + amount_refunded: + type: integer + refund: + type: string + title: credit_note_refund_params + type: object + type: array + style: deepObject + - description: >- + When shipping_cost contains the shipping_rate from the invoice, the + shipping_cost is included in the credit note. + explode: true + in: query + name: shipping_cost + required: false + schema: + properties: + shipping_rate: + maxLength: 5000 + type: string + title: credit_note_shipping_cost + type: object + style: deepObject + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/credit_note_line_item' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: CreditNoteLinesList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a credit note preview's line items + '/v1/customers/{customer}/bank_accounts/{id}': + delete: + description:

Delete a specified source for a given customer.

+ operationId: DeleteCustomersCustomerBankAccountsId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/payment_source' + - $ref: '#/components/schemas/deleted_payment_source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete a customer source + get: + deprecated: true + description: >- +

By default, you can see the 10 most recent sources stored on a + Customer directly on the object, but you can also retrieve details about + a specific bank account stored on the Stripe account.

+ operationId: GetCustomersCustomerBankAccountsId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/bank_account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a bank account + post: + description:

Update a specified source for a given customer.

+ operationId: PostCustomersCustomerBankAccountsId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + owner: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + account_holder_name: + description: >- + The name of the person or business that owns the bank + account. + maxLength: 5000 + type: string + account_holder_type: + description: >- + The type of entity that holds the account. This can be + either `individual` or `company`. + enum: + - company + - individual + maxLength: 5000 + type: string + address_city: + description: City/District/Suburb/Town/Village. + maxLength: 5000 + type: string + address_country: + description: 'Billing address country, if provided when creating card.' + maxLength: 5000 + type: string + address_line1: + description: Address line 1 (Street address/PO Box/Company name). + maxLength: 5000 + type: string + address_line2: + description: Address line 2 (Apartment/Suite/Unit/Building). + maxLength: 5000 + type: string + address_state: + description: State/County/Province/Region. + maxLength: 5000 + type: string + address_zip: + description: ZIP or postal code. + maxLength: 5000 + type: string + exp_month: + description: Two digit number representing the card’s expiration month. + maxLength: 5000 + type: string + exp_year: + description: Four digit number representing the card’s expiration year. + maxLength: 5000 + type: string + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + name: + description: Cardholder name. + maxLength: 5000 + type: string + owner: + properties: + address: + properties: + city: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + line1: + maxLength: 5000 + type: string + line2: + maxLength: 5000 + type: string + postal_code: + maxLength: 5000 + type: string + state: + maxLength: 5000 + type: string + title: source_address + type: object + email: + type: string + name: + maxLength: 5000 + type: string + phone: + maxLength: 5000 + type: string + title: owner + type: object + type: object + required: false + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + '/v1/customers/{customer}/bank_accounts/{id}/verify': + post: + description:

Verify a specified bank account for a given customer.

+ operationId: PostCustomersCustomerBankAccountsIdVerify + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + amounts: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + amounts: + description: >- + Two positive integers, in *cents*, equal to the values of + the microdeposits sent to the bank account. + items: + type: integer + type: array + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/bank_account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Verify a bank account + '/v1/customers/{customer}/cards': + get: + deprecated: true + description: >- +

You can see a list of the cards belonging to a customer. + + Note that the 10 most recent sources are always available on the + Customer object. + + If you need more than those 10, you can use this API method and the + limit and starting_after parameters to page + through additional cards.

+ operationId: GetCustomersCustomerCards + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/card' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: CardList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all cards + post: + description: >- +

When you create a new credit card, you must specify a customer or + recipient on which to create it.

+ + +

If the card’s owner has no default card, then the new card will + become the default. + + However, if the owner already has a default, then it will not change. + + To change the default, you should update the customer to have a new + default_source.

+ operationId: PostCustomersCustomerCards + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + bank_account: + explode: true + style: deepObject + card: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + alipay_account: + description: >- + A token returned by [Stripe.js](https://stripe.com/docs/js) + representing the user’s Alipay account details. + maxLength: 5000 + type: string + bank_account: + anyOf: + - properties: + account_holder_name: + maxLength: 5000 + type: string + account_holder_type: + enum: + - company + - individual + maxLength: 5000 + type: string + account_number: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + currency: + format: currency + type: string + object: + enum: + - bank_account + maxLength: 5000 + type: string + routing_number: + maxLength: 5000 + type: string + required: + - account_number + - country + title: customer_payment_source_bank_account + type: object + - maxLength: 5000 + type: string + description: >- + Either a token, like the ones returned by + [Stripe.js](https://stripe.com/docs/js), or a dictionary + containing a user's bank account details. + card: + anyOf: + - properties: + address_city: + maxLength: 5000 + type: string + address_country: + maxLength: 5000 + type: string + address_line1: + maxLength: 5000 + type: string + address_line2: + maxLength: 5000 + type: string + address_state: + maxLength: 5000 + type: string + address_zip: + maxLength: 5000 + type: string + cvc: + maxLength: 5000 + type: string + exp_month: + type: integer + exp_year: + type: integer + metadata: + additionalProperties: + type: string + type: object + name: + maxLength: 5000 + type: string + number: + maxLength: 5000 + type: string + object: + enum: + - card + maxLength: 5000 + type: string + required: + - exp_month + - exp_year + - number + title: customer_payment_source_card + type: object + - maxLength: 5000 + type: string + description: >- + A token, like the ones returned by + [Stripe.js](https://stripe.com/docs/js). + x-stripeBypassValidation: true + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + additionalProperties: + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + type: object + source: + description: >- + Please refer to full + [documentation](https://stripe.com/docs/api) instead. + maxLength: 5000 + type: string + x-stripeBypassValidation: true + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/payment_source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a card + '/v1/customers/{customer}/cards/{id}': + delete: + description:

Delete a specified source for a given customer.

+ operationId: DeleteCustomersCustomerCardsId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/payment_source' + - $ref: '#/components/schemas/deleted_payment_source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete a customer source + get: + deprecated: true + description: >- +

You can always see the 10 most recent cards directly on a customer; + this method lets you retrieve details about a specific card stored on + the customer.

+ operationId: GetCustomersCustomerCardsId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/card' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a card + post: + description:

Update a specified source for a given customer.

+ operationId: PostCustomersCustomerCardsId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + owner: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + account_holder_name: + description: >- + The name of the person or business that owns the bank + account. + maxLength: 5000 + type: string + account_holder_type: + description: >- + The type of entity that holds the account. This can be + either `individual` or `company`. + enum: + - company + - individual + maxLength: 5000 + type: string + address_city: + description: City/District/Suburb/Town/Village. + maxLength: 5000 + type: string + address_country: + description: 'Billing address country, if provided when creating card.' + maxLength: 5000 + type: string + address_line1: + description: Address line 1 (Street address/PO Box/Company name). + maxLength: 5000 + type: string + address_line2: + description: Address line 2 (Apartment/Suite/Unit/Building). + maxLength: 5000 + type: string + address_state: + description: State/County/Province/Region. + maxLength: 5000 + type: string + address_zip: + description: ZIP or postal code. + maxLength: 5000 + type: string + exp_month: + description: Two digit number representing the card’s expiration month. + maxLength: 5000 + type: string + exp_year: + description: Four digit number representing the card’s expiration year. + maxLength: 5000 + type: string + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + name: + description: Cardholder name. + maxLength: 5000 + type: string + owner: + properties: + address: + properties: + city: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + line1: + maxLength: 5000 + type: string + line2: + maxLength: 5000 + type: string + postal_code: + maxLength: 5000 + type: string + state: + maxLength: 5000 + type: string + title: source_address + type: object + email: + type: string + name: + maxLength: 5000 + type: string + phone: + maxLength: 5000 + type: string + title: owner + type: object + type: object + required: false + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + '/v1/customers/{customer}/cash_balance': + get: + description:

Retrieves a customer’s cash balance.

+ operationId: GetCustomersCustomerCashBalance + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/cash_balance' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a cash balance + post: + description:

Changes the settings on a customer’s cash balance.

+ operationId: PostCustomersCustomerCashBalance + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + settings: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + settings: + description: A hash of settings for this cash balance. + properties: + reconciliation_mode: + enum: + - automatic + - manual + - merchant_default + type: string + title: balance_settings_param + type: object + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/cash_balance' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Update a cash balance's settings + '/v1/customers/{customer}/cash_balance_transactions': + get: + description: >- +

Returns a list of transactions that modified the customer’s cash balance.

+ operationId: GetCustomersCustomerCashBalanceTransactions + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: >- + Customers with certain payments enabled have a cash balance, + representing funds that were paid + + by the customer to a merchant, but have not yet been allocated + to a payment. Cash Balance Transactions + + represent when funds are moved into or out of this balance. + This includes funding by the customer, allocation + + to payments, and refunds to the customer. + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/customer_cash_balance_transaction' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: CustomerCashBalanceTransactionList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List cash balance transactions + '/v1/customers/{customer}/cash_balance_transactions/{transaction}': + get: + description: >- +

Retrieves a specific cash balance transaction, which updated the + customer’s cash + balance.

+ operationId: GetCustomersCustomerCashBalanceTransactionsTransaction + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: transaction + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/customer_cash_balance_transaction' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a cash balance transaction + '/v1/customers/{customer}/discount': + delete: + description:

Removes the currently applied discount on a customer.

+ operationId: DeleteCustomersCustomerDiscount + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/deleted_discount' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete a customer discount + get: + description: '' + operationId: GetCustomersCustomerDiscount + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/discount' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + '/v1/customers/{customer}/funding_instructions': + post: + description: >- +

Retrieve funding instructions for a customer cash balance. If funding + instructions do not yet exist for the customer, new + + funding instructions will be created. If funding instructions have + already been created for a given customer, the same + + funding instructions will be retrieved. In other words, we will return + the same funding instructions each time.

+ operationId: PostCustomersCustomerFundingInstructions + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + bank_transfer: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + bank_transfer: + description: Additional parameters for `bank_transfer` funding types + properties: + eu_bank_transfer: + properties: + country: + maxLength: 5000 + type: string + required: + - country + title: eu_bank_account_params + type: object + requested_address_types: + items: + enum: + - iban + - sort_code + - spei + - zengin + type: string + x-stripeBypassValidation: true + type: array + type: + enum: + - eu_bank_transfer + - gb_bank_transfer + - jp_bank_transfer + - mx_bank_transfer + - us_bank_transfer + type: string + x-stripeBypassValidation: true + required: + - type + title: bank_transfer_params + type: object + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + funding_type: + description: The `funding_type` to get the instructions for. + enum: + - bank_transfer + type: string + required: + - bank_transfer + - currency + - funding_type + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/funding_instructions' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create or retrieve funding instructions for a customer cash balance + '/v1/customers/{customer}/payment_methods': + get: + description:

Returns a list of PaymentMethods for a given Customer

+ operationId: GetCustomersCustomerPaymentMethods + parameters: + - description: >- + This field indicates whether this payment method can be shown again + to its customer in a checkout flow. Stripe products such as Checkout + and Elements use this field to determine whether a payment method + can be shown as a saved payment method in a checkout flow. + in: query + name: allow_redisplay + required: false + schema: + enum: + - always + - limited + - unspecified + type: string + style: form + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + An optional filter on the list, based on the object `type` field. + Without the filter, the list includes all current and future payment + method types. If your integration expects only one type of payment + method in the response, make sure to provide a type value in the + request. + in: query + name: type + required: false + schema: + enum: + - acss_debit + - affirm + - afterpay_clearpay + - alipay + - alma + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - billie + - blik + - boleto + - card + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - kakao_pay + - klarna + - konbini + - kr_card + - link + - mobilepay + - multibanco + - naver_pay + - nz_bank_account + - oxxo + - p24 + - pay_by_bank + - payco + - paynow + - paypal + - pix + - promptpay + - revolut_pay + - samsung_pay + - satispay + - sepa_debit + - sofort + - swish + - twint + - us_bank_account + - wechat_pay + - zip + type: string + x-stripeBypassValidation: true + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/payment_method' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: CustomerPaymentMethodResourceList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List a Customer's PaymentMethods + '/v1/customers/{customer}/payment_methods/{payment_method}': + get: + description:

Retrieves a PaymentMethod object for a given Customer.

+ operationId: GetCustomersCustomerPaymentMethodsPaymentMethod + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: payment_method + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/payment_method' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a Customer's PaymentMethod + '/v1/customers/{customer}/sources': + get: + description:

List sources for a specified customer.

+ operationId: GetCustomersCustomerSources + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: Filter sources according to a particular object type. + in: query + name: object + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + anyOf: + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/source' + title: Polymorphic + x-stripeBypassValidation: true + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: ApmsSourcesSourceList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + post: + description: >- +

When you create a new credit card, you must specify a customer or + recipient on which to create it.

+ + +

If the card’s owner has no default card, then the new card will + become the default. + + However, if the owner already has a default, then it will not change. + + To change the default, you should update the customer to have a new + default_source.

+ operationId: PostCustomersCustomerSources + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + bank_account: + explode: true + style: deepObject + card: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + alipay_account: + description: >- + A token returned by [Stripe.js](https://stripe.com/docs/js) + representing the user’s Alipay account details. + maxLength: 5000 + type: string + bank_account: + anyOf: + - properties: + account_holder_name: + maxLength: 5000 + type: string + account_holder_type: + enum: + - company + - individual + maxLength: 5000 + type: string + account_number: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + currency: + format: currency + type: string + object: + enum: + - bank_account + maxLength: 5000 + type: string + routing_number: + maxLength: 5000 + type: string + required: + - account_number + - country + title: customer_payment_source_bank_account + type: object + - maxLength: 5000 + type: string + description: >- + Either a token, like the ones returned by + [Stripe.js](https://stripe.com/docs/js), or a dictionary + containing a user's bank account details. + card: + anyOf: + - properties: + address_city: + maxLength: 5000 + type: string + address_country: + maxLength: 5000 + type: string + address_line1: + maxLength: 5000 + type: string + address_line2: + maxLength: 5000 + type: string + address_state: + maxLength: 5000 + type: string + address_zip: + maxLength: 5000 + type: string + cvc: + maxLength: 5000 + type: string + exp_month: + type: integer + exp_year: + type: integer + metadata: + additionalProperties: + type: string + type: object + name: + maxLength: 5000 + type: string + number: + maxLength: 5000 + type: string + object: + enum: + - card + maxLength: 5000 + type: string + required: + - exp_month + - exp_year + - number + title: customer_payment_source_card + type: object + - maxLength: 5000 + type: string + description: >- + A token, like the ones returned by + [Stripe.js](https://stripe.com/docs/js). + x-stripeBypassValidation: true + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + additionalProperties: + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + type: object + source: + description: >- + Please refer to full + [documentation](https://stripe.com/docs/api) instead. + maxLength: 5000 + type: string + x-stripeBypassValidation: true + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/payment_source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a card + '/v1/customers/{customer}/sources/{id}': + delete: + description:

Delete a specified source for a given customer.

+ operationId: DeleteCustomersCustomerSourcesId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/payment_source' + - $ref: '#/components/schemas/deleted_payment_source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete a customer source + get: + description:

Retrieve a specified source for a given customer.

+ operationId: GetCustomersCustomerSourcesId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: id + required: true + schema: + maxLength: 500 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/payment_source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + post: + description:

Update a specified source for a given customer.

+ operationId: PostCustomersCustomerSourcesId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + owner: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + account_holder_name: + description: >- + The name of the person or business that owns the bank + account. + maxLength: 5000 + type: string + account_holder_type: + description: >- + The type of entity that holds the account. This can be + either `individual` or `company`. + enum: + - company + - individual + maxLength: 5000 + type: string + address_city: + description: City/District/Suburb/Town/Village. + maxLength: 5000 + type: string + address_country: + description: 'Billing address country, if provided when creating card.' + maxLength: 5000 + type: string + address_line1: + description: Address line 1 (Street address/PO Box/Company name). + maxLength: 5000 + type: string + address_line2: + description: Address line 2 (Apartment/Suite/Unit/Building). + maxLength: 5000 + type: string + address_state: + description: State/County/Province/Region. + maxLength: 5000 + type: string + address_zip: + description: ZIP or postal code. + maxLength: 5000 + type: string + exp_month: + description: Two digit number representing the card’s expiration month. + maxLength: 5000 + type: string + exp_year: + description: Four digit number representing the card’s expiration year. + maxLength: 5000 + type: string + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + name: + description: Cardholder name. + maxLength: 5000 + type: string + owner: + properties: + address: + properties: + city: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + line1: + maxLength: 5000 + type: string + line2: + maxLength: 5000 + type: string + postal_code: + maxLength: 5000 + type: string + state: + maxLength: 5000 + type: string + title: source_address + type: object + email: + type: string + name: + maxLength: 5000 + type: string + phone: + maxLength: 5000 + type: string + title: owner + type: object + type: object + required: false + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/card' + - $ref: '#/components/schemas/bank_account' + - $ref: '#/components/schemas/source' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + '/v1/customers/{customer}/sources/{id}/verify': + post: + description:

Verify a specified bank account for a given customer.

+ operationId: PostCustomersCustomerSourcesIdVerify + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + amounts: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + amounts: + description: >- + Two positive integers, in *cents*, equal to the values of + the microdeposits sent to the bank account. + items: + type: integer + type: array + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/bank_account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Verify a bank account + '/v1/customers/{customer}/subscriptions': + get: + description: >- +

You can see a list of the customer’s active subscriptions. Note that + the 10 most recent active subscriptions are always available by default + on the customer object. If you need more than those 10, you can use the + limit and starting_after parameters to page through additional + subscriptions.

+ operationId: GetCustomersCustomerSubscriptions + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/subscription' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: SubscriptionList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List active subscriptions + post: + description:

Creates a new subscription on an existing customer.

+ operationId: PostCustomersCustomerSubscriptions + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + add_invoice_items: + explode: true + style: deepObject + application_fee_percent: + explode: true + style: deepObject + automatic_tax: + explode: true + style: deepObject + billing_thresholds: + explode: true + style: deepObject + default_tax_rates: + explode: true + style: deepObject + discounts: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + invoice_settings: + explode: true + style: deepObject + items: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + payment_settings: + explode: true + style: deepObject + pending_invoice_item_interval: + explode: true + style: deepObject + transfer_data: + explode: true + style: deepObject + trial_end: + explode: true + style: deepObject + trial_settings: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + add_invoice_items: + description: >- + A list of prices and quantities that will generate invoice + items appended to the next invoice for this subscription. + You may pass up to 20 items. + items: + properties: + discounts: + items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + price: + maxLength: 5000 + type: string + price_data: + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + title: one_time_price_data_with_negative_amounts + type: object + quantity: + type: integer + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + title: add_invoice_item_entry + type: object + type: array + application_fee_percent: + anyOf: + - type: number + - enum: + - '' + type: string + description: >- + A non-negative decimal between 0 and 100, with at most two + decimal places. This represents the percentage of the + subscription invoice total that will be transferred to the + application owner's Stripe account. The request must be made + by a platform account on a connected account in order to set + an application fee percentage. For more information, see the + application fees + [documentation](https://stripe.com/docs/connect/subscriptions#collecting-fees-on-subscriptions). + automatic_tax: + description: >- + Automatic tax settings for this subscription. We recommend + you only include this parameter when the existing value is + being changed. + properties: + enabled: + type: boolean + liability: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + required: + - enabled + title: automatic_tax_config + type: object + backdate_start_date: + description: >- + For new subscriptions, a past timestamp to backdate the + subscription's start date to. If set, the first invoice will + contain a proration for the timespan between the start date + and the current time. Can be combined with trials and the + billing cycle anchor. + format: unix-time + type: integer + billing_cycle_anchor: + description: >- + A future timestamp in UTC format to anchor the + subscription's [billing + cycle](https://stripe.com/docs/subscriptions/billing-cycle). + The anchor is the reference point that aligns future billing + cycle dates. It sets the day of week for `week` intervals, + the day of month for `month` and `year` intervals, and the + month of year for `year` intervals. + format: unix-time + type: integer + x-stripeBypassValidation: true + billing_thresholds: + anyOf: + - properties: + amount_gte: + type: integer + reset_billing_cycle_anchor: + type: boolean + title: billing_thresholds_param + type: object + - enum: + - '' + type: string + description: >- + Define thresholds at which an invoice will be sent, and the + subscription advanced to a new billing period. When + updating, pass an empty string to remove previously-defined + thresholds. + cancel_at: + description: >- + A timestamp at which the subscription should cancel. If set + to a date before the current period ends, this will cause a + proration if prorations have been enabled using + `proration_behavior`. If set during a future period, this + will always cause a proration for that period. + format: unix-time + type: integer + cancel_at_period_end: + description: >- + Indicate whether this subscription should cancel at the end + of the current period (`current_period_end`). Defaults to + `false`. This param will be removed in a future API version. + Please use `cancel_at` instead. + type: boolean + collection_method: + description: >- + Either `charge_automatically`, or `send_invoice`. When + charging automatically, Stripe will attempt to pay this + subscription at the end of the cycle using the default + source attached to the customer. When sending an invoice, + Stripe will email your customer an invoice with payment + instructions and mark the subscription as `active`. Defaults + to `charge_automatically`. + enum: + - charge_automatically + - send_invoice + type: string + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + days_until_due: + description: >- + Number of days a customer has to pay invoices generated by + this subscription. Valid only for subscriptions where + `collection_method` is set to `send_invoice`. + type: integer + default_payment_method: + description: >- + ID of the default payment method for the subscription. It + must belong to the customer associated with the + subscription. This takes precedence over `default_source`. + If neither are set, invoices will use the customer's + [invoice_settings.default_payment_method](https://stripe.com/docs/api/customers/object#customer_object-invoice_settings-default_payment_method) + or + [default_source](https://stripe.com/docs/api/customers/object#customer_object-default_source). + maxLength: 5000 + type: string + default_source: + description: >- + ID of the default payment source for the subscription. It + must belong to the customer associated with the subscription + and be in a chargeable state. If `default_payment_method` is + also set, `default_payment_method` will take precedence. If + neither are set, invoices will use the customer's + [invoice_settings.default_payment_method](https://stripe.com/docs/api/customers/object#customer_object-invoice_settings-default_payment_method) + or + [default_source](https://stripe.com/docs/api/customers/object#customer_object-default_source). + maxLength: 5000 + type: string + default_tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + description: >- + The tax rates that will apply to any subscription item that + does not have `tax_rates` set. Invoices created will have + their `default_tax_rates` populated from the subscription. + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + description: >- + The coupons to redeem into discounts for the subscription. + If not specified or empty, inherits the discount from the + subscription's customer. + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + invoice_settings: + description: All invoices will be billed using the specified settings. + properties: + account_tax_ids: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + issuer: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + title: invoice_settings_param + type: object + items: + description: >- + A list of up to 20 subscription items, each with an attached + price. + items: + properties: + billing_thresholds: + anyOf: + - properties: + usage_gte: + type: integer + required: + - usage_gte + title: item_billing_thresholds_param + type: object + - enum: + - '' + type: string + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + metadata: + additionalProperties: + type: string + type: object + price: + maxLength: 5000 + type: string + price_data: + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + recurring: + properties: + interval: + enum: + - day + - month + - week + - year + type: string + interval_count: + type: integer + required: + - interval + title: recurring_adhoc + type: object + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + - recurring + title: recurring_price_data + type: object + quantity: + type: integer + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + title: subscription_item_create_params + type: object + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + off_session: + description: >- + Indicates if a customer is on or off-session while an + invoice payment is attempted. Defaults to `false` + (on-session). + type: boolean + payment_behavior: + description: >- + Only applies to subscriptions with + `collection_method=charge_automatically`. + + + Use `allow_incomplete` to create Subscriptions with + `status=incomplete` if the first invoice can't be paid. + Creating Subscriptions with this status allows you to manage + scenarios where additional customer actions are needed to + pay a subscription's invoice. For example, SCA regulation + may require 3DS authentication to complete payment. See the + [SCA Migration + Guide](https://stripe.com/docs/billing/migration/strong-customer-authentication) + for Billing to learn more. This is the default behavior. + + + Use `default_incomplete` to create Subscriptions with + `status=incomplete` when the first invoice requires payment, + otherwise start as active. Subscriptions transition to + `status=active` when successfully confirming the + PaymentIntent on the first invoice. This allows simpler + management of scenarios where additional customer actions + are needed to pay a subscription’s invoice, such as failed + payments, [SCA + regulation](https://stripe.com/docs/billing/migration/strong-customer-authentication), + or collecting a mandate for a bank debit payment method. If + the PaymentIntent is not confirmed within 23 hours + Subscriptions transition to `status=incomplete_expired`, + which is a terminal state. + + + Use `error_if_incomplete` if you want Stripe to return an + HTTP 402 status code if a subscription's first invoice can't + be paid. For example, if a payment method requires 3DS + authentication due to SCA regulation and further customer + action is needed, this parameter doesn't create a + Subscription and returns an error instead. This was the + default behavior for API versions prior to 2019-03-14. See + the [changelog](https://stripe.com/docs/upgrades#2019-03-14) + to learn more. + + + `pending_if_incomplete` is only used with updates and cannot + be passed when creating a Subscription. + + + Subscriptions with `collection_method=send_invoice` are + automatically activated regardless of the first Invoice + status. + enum: + - allow_incomplete + - default_incomplete + - error_if_incomplete + - pending_if_incomplete + type: string + payment_settings: + description: >- + Payment settings to pass to invoices created by the + subscription. + properties: + payment_method_options: + properties: + acss_debit: + anyOf: + - properties: + mandate_options: + properties: + transaction_type: + enum: + - business + - personal + type: string + title: mandate_options_param + type: object + verification_method: + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + bancontact: + anyOf: + - properties: + preferred_language: + enum: + - de + - en + - fr + - nl + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + card: + anyOf: + - properties: + mandate_options: + properties: + amount: + type: integer + amount_type: + enum: + - fixed + - maximum + type: string + description: + maxLength: 200 + type: string + title: mandate_options_param + type: object + network: + enum: + - amex + - cartes_bancaires + - diners + - discover + - eftpos_au + - girocard + - interac + - jcb + - link + - mastercard + - unionpay + - unknown + - visa + maxLength: 5000 + type: string + x-stripeBypassValidation: true + request_three_d_secure: + enum: + - any + - automatic + - challenge + type: string + title: subscription_payment_method_options_param + type: object + - enum: + - '' + type: string + customer_balance: + anyOf: + - properties: + bank_transfer: + properties: + eu_bank_transfer: + properties: + country: + maxLength: 5000 + type: string + required: + - country + title: eu_bank_transfer_param + type: object + type: + type: string + title: bank_transfer_param + type: object + funding_type: + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + konbini: + anyOf: + - properties: {} + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + sepa_debit: + anyOf: + - properties: {} + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + us_bank_account: + anyOf: + - properties: + financial_connections: + properties: + filters: + properties: + account_subcategories: + items: + enum: + - checking + - savings + type: string + type: array + title: >- + invoice_linked_account_options_filters_param + type: object + permissions: + items: + enum: + - balances + - ownership + - payment_method + - transactions + maxLength: 5000 + type: string + x-stripeBypassValidation: true + type: array + prefetch: + items: + enum: + - balances + - ownership + - transactions + type: string + x-stripeBypassValidation: true + type: array + title: invoice_linked_account_options_param + type: object + verification_method: + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + title: payment_method_options + type: object + payment_method_types: + anyOf: + - items: + enum: + - ach_credit_transfer + - ach_debit + - acss_debit + - affirm + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - boleto + - card + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - jp_credit_transfer + - kakao_pay + - klarna + - konbini + - kr_card + - link + - multibanco + - naver_pay + - nz_bank_account + - p24 + - payco + - paynow + - paypal + - promptpay + - revolut_pay + - sepa_credit_transfer + - sepa_debit + - sofort + - swish + - us_bank_account + - wechat_pay + type: string + x-stripeBypassValidation: true + type: array + - enum: + - '' + type: string + save_default_payment_method: + enum: + - 'off' + - on_subscription + type: string + title: payment_settings + type: object + pending_invoice_item_interval: + anyOf: + - properties: + interval: + enum: + - day + - month + - week + - year + type: string + interval_count: + type: integer + required: + - interval + title: pending_invoice_item_interval_params + type: object + - enum: + - '' + type: string + description: >- + Specifies an interval for how often to bill for any pending + invoice items. It is analogous to calling [Create an + invoice](https://stripe.com/docs/api#create_invoice) for the + given subscription at the specified interval. + proration_behavior: + description: >- + Determines how to handle + [prorations](https://stripe.com/docs/billing/subscriptions/prorations) + resulting from the `billing_cycle_anchor`. If no value is + passed, the default is `create_prorations`. + enum: + - always_invoice + - create_prorations + - none + type: string + transfer_data: + description: >- + If specified, the funds from the subscription's invoices + will be transferred to the destination and the ID of the + resulting transfers will be found on the resulting charges. + properties: + amount_percent: + type: number + destination: + type: string + required: + - destination + title: transfer_data_specs + type: object + trial_end: + anyOf: + - enum: + - now + maxLength: 5000 + type: string + - format: unix-time + type: integer + description: >- + Unix timestamp representing the end of the trial period the + customer will get before being charged for the first time. + If set, trial_end will override the default trial period of + the plan the customer is being subscribed to. The special + value `now` can be provided to end the customer's trial + immediately. Can be at most two years from + `billing_cycle_anchor`. See [Using trial periods on + subscriptions](https://stripe.com/docs/billing/subscriptions/trials) + to learn more. + trial_from_plan: + description: >- + Indicates if a plan's `trial_period_days` should be applied + to the subscription. Setting `trial_end` per subscription is + preferred, and this defaults to `false`. Setting this flag + to `true` together with `trial_end` is not allowed. See + [Using trial periods on + subscriptions](https://stripe.com/docs/billing/subscriptions/trials) + to learn more. + type: boolean + trial_period_days: + description: >- + Integer representing the number of trial period days before + the customer is charged for the first time. This will always + overwrite any trials that might apply via a subscribed plan. + See [Using trial periods on + subscriptions](https://stripe.com/docs/billing/subscriptions/trials) + to learn more. + type: integer + trial_settings: + description: Settings related to subscription trials. + properties: + end_behavior: + properties: + missing_payment_method: + enum: + - cancel + - create_invoice + - pause + type: string + required: + - missing_payment_method + title: end_behavior + type: object + required: + - end_behavior + title: trial_settings_config + type: object + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/subscription' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a subscription + '/v1/customers/{customer}/subscriptions/{subscription_exposed_id}': + delete: + description: >- +

Cancels a customer’s subscription. If you set the + at_period_end parameter to true, the + subscription will remain active until the end of the period, at which + point it will be canceled and not renewed. Otherwise, with the default + false value, the subscription is terminated immediately. In + either case, the customer will not be charged again for the + subscription.

+ + +

Note, however, that any pending invoice items that you’ve created + will still be charged for at the end of the period, unless manually deleted. If you’ve set the subscription + to cancel at the end of the period, any pending prorations will also be + left in place and collected at the end of the period. But if the + subscription is set to cancel immediately, pending prorations will be + removed.

+ + +

By default, upon subscription cancellation, Stripe will stop + automatic collection of all finalized invoices for the customer. This is + intended to prevent unexpected payment attempts after the customer has + canceled a subscription. However, you can resume automatic collection of + the invoices manually after subscription cancellation to have us + proceed. Or, you could check for unpaid invoices before allowing the + customer to cancel the subscription at all.

+ operationId: DeleteCustomersCustomerSubscriptionsSubscriptionExposedId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: subscription_exposed_id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + invoice_now: + description: >- + Can be set to `true` if `at_period_end` is not set to + `true`. Will generate a final invoice that invoices for any + un-invoiced metered usage and new/pending proration invoice + items. + type: boolean + prorate: + description: >- + Can be set to `true` if `at_period_end` is not set to + `true`. Will generate a proration invoice item that credits + remaining unused time until the subscription period end. + type: boolean + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/subscription' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Cancel a subscription + get: + description:

Retrieves the subscription with the given ID.

+ operationId: GetCustomersCustomerSubscriptionsSubscriptionExposedId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: subscription_exposed_id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/subscription' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a subscription + post: + description: >- +

Updates an existing subscription on a customer to match the specified + parameters. When changing plans or quantities, we will optionally + prorate the price we charge next month to make up for any price changes. + To preview how the proration will be calculated, use the upcoming invoice endpoint.

+ operationId: PostCustomersCustomerSubscriptionsSubscriptionExposedId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: subscription_exposed_id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + add_invoice_items: + explode: true + style: deepObject + application_fee_percent: + explode: true + style: deepObject + automatic_tax: + explode: true + style: deepObject + billing_thresholds: + explode: true + style: deepObject + cancel_at: + explode: true + style: deepObject + cancellation_details: + explode: true + style: deepObject + default_source: + explode: true + style: deepObject + default_tax_rates: + explode: true + style: deepObject + discounts: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + invoice_settings: + explode: true + style: deepObject + items: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + pause_collection: + explode: true + style: deepObject + payment_settings: + explode: true + style: deepObject + pending_invoice_item_interval: + explode: true + style: deepObject + transfer_data: + explode: true + style: deepObject + trial_end: + explode: true + style: deepObject + trial_settings: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + add_invoice_items: + description: >- + A list of prices and quantities that will generate invoice + items appended to the next invoice for this subscription. + You may pass up to 20 items. + items: + properties: + discounts: + items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + price: + maxLength: 5000 + type: string + price_data: + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + title: one_time_price_data_with_negative_amounts + type: object + quantity: + type: integer + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + title: add_invoice_item_entry + type: object + type: array + application_fee_percent: + anyOf: + - type: number + - enum: + - '' + type: string + description: >- + A non-negative decimal between 0 and 100, with at most two + decimal places. This represents the percentage of the + subscription invoice total that will be transferred to the + application owner's Stripe account. The request must be made + by a platform account on a connected account in order to set + an application fee percentage. For more information, see the + application fees + [documentation](https://stripe.com/docs/connect/subscriptions#collecting-fees-on-subscriptions). + automatic_tax: + description: >- + Automatic tax settings for this subscription. We recommend + you only include this parameter when the existing value is + being changed. + properties: + enabled: + type: boolean + liability: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + required: + - enabled + title: automatic_tax_config + type: object + billing_cycle_anchor: + description: >- + Either `now` or `unchanged`. Setting the value to `now` + resets the subscription's billing cycle anchor to the + current time. For more information, see the billing cycle + [documentation](https://stripe.com/docs/billing/subscriptions/billing-cycle). + enum: + - now + - unchanged + maxLength: 5000 + type: string + x-stripeBypassValidation: true + billing_thresholds: + anyOf: + - properties: + amount_gte: + type: integer + reset_billing_cycle_anchor: + type: boolean + title: billing_thresholds_param + type: object + - enum: + - '' + type: string + description: >- + Define thresholds at which an invoice will be sent, and the + subscription advanced to a new billing period. When + updating, pass an empty string to remove previously-defined + thresholds. + cancel_at: + anyOf: + - format: unix-time + type: integer + - enum: + - '' + type: string + description: >- + A timestamp at which the subscription should cancel. If set + to a date before the current period ends, this will cause a + proration if prorations have been enabled using + `proration_behavior`. If set during a future period, this + will always cause a proration for that period. + cancel_at_period_end: + description: >- + Indicate whether this subscription should cancel at the end + of the current period (`current_period_end`). Defaults to + `false`. This param will be removed in a future API version. + Please use `cancel_at` instead. + type: boolean + cancellation_details: + description: Details about why this subscription was cancelled + properties: + comment: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + feedback: + enum: + - '' + - customer_service + - low_quality + - missing_features + - other + - switched_service + - too_complex + - too_expensive + - unused + type: string + title: cancellation_details_param + type: object + collection_method: + description: >- + Either `charge_automatically`, or `send_invoice`. When + charging automatically, Stripe will attempt to pay this + subscription at the end of the cycle using the default + source attached to the customer. When sending an invoice, + Stripe will email your customer an invoice with payment + instructions and mark the subscription as `active`. Defaults + to `charge_automatically`. + enum: + - charge_automatically + - send_invoice + type: string + days_until_due: + description: >- + Number of days a customer has to pay invoices generated by + this subscription. Valid only for subscriptions where + `collection_method` is set to `send_invoice`. + type: integer + default_payment_method: + description: >- + ID of the default payment method for the subscription. It + must belong to the customer associated with the + subscription. This takes precedence over `default_source`. + If neither are set, invoices will use the customer's + [invoice_settings.default_payment_method](https://stripe.com/docs/api/customers/object#customer_object-invoice_settings-default_payment_method) + or + [default_source](https://stripe.com/docs/api/customers/object#customer_object-default_source). + maxLength: 5000 + type: string + default_source: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + description: >- + ID of the default payment source for the subscription. It + must belong to the customer associated with the subscription + and be in a chargeable state. If `default_payment_method` is + also set, `default_payment_method` will take precedence. If + neither are set, invoices will use the customer's + [invoice_settings.default_payment_method](https://stripe.com/docs/api/customers/object#customer_object-invoice_settings-default_payment_method) + or + [default_source](https://stripe.com/docs/api/customers/object#customer_object-default_source). + default_tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + description: >- + The tax rates that will apply to any subscription item that + does not have `tax_rates` set. Invoices created will have + their `default_tax_rates` populated from the subscription. + Pass an empty string to remove previously-defined tax rates. + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + description: >- + The coupons to redeem into discounts for the subscription. + If not specified or empty, inherits the discount from the + subscription's customer. + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + invoice_settings: + description: All invoices will be billed using the specified settings. + properties: + account_tax_ids: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + issuer: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + title: invoice_settings_param + type: object + items: + description: >- + A list of up to 20 subscription items, each with an attached + price. + items: + properties: + billing_thresholds: + anyOf: + - properties: + usage_gte: + type: integer + required: + - usage_gte + title: item_billing_thresholds_param + type: object + - enum: + - '' + type: string + clear_usage: + type: boolean + deleted: + type: boolean + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + id: + maxLength: 5000 + type: string + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + price: + maxLength: 5000 + type: string + price_data: + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + recurring: + properties: + interval: + enum: + - day + - month + - week + - year + type: string + interval_count: + type: integer + required: + - interval + title: recurring_adhoc + type: object + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + - recurring + title: recurring_price_data + type: object + quantity: + type: integer + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + title: subscription_item_update_params + type: object + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + off_session: + description: >- + Indicates if a customer is on or off-session while an + invoice payment is attempted. Defaults to `false` + (on-session). + type: boolean + pause_collection: + anyOf: + - properties: + behavior: + enum: + - keep_as_draft + - mark_uncollectible + - void + type: string + resumes_at: + format: unix-time + type: integer + required: + - behavior + title: pause_collection_param + type: object + - enum: + - '' + type: string + description: >- + If specified, payment collection for this subscription will + be paused. Note that the subscription status will be + unchanged and will not be updated to `paused`. Learn more + about [pausing + collection](https://stripe.com/docs/billing/subscriptions/pause-payment). + payment_behavior: + description: >- + Use `allow_incomplete` to transition the subscription to + `status=past_due` if a payment is required but cannot be + paid. This allows you to manage scenarios where additional + user actions are needed to pay a subscription's invoice. For + example, SCA regulation may require 3DS authentication to + complete payment. See the [SCA Migration + Guide](https://stripe.com/docs/billing/migration/strong-customer-authentication) + for Billing to learn more. This is the default behavior. + + + Use `default_incomplete` to transition the subscription to + `status=past_due` when payment is required and await + explicit confirmation of the invoice's payment intent. This + allows simpler management of scenarios where additional user + actions are needed to pay a subscription’s invoice. Such as + failed payments, [SCA + regulation](https://stripe.com/docs/billing/migration/strong-customer-authentication), + or collecting a mandate for a bank debit payment method. + + + Use `pending_if_incomplete` to update the subscription using + [pending + updates](https://stripe.com/docs/billing/subscriptions/pending-updates). + When you use `pending_if_incomplete` you can only pass the + parameters [supported by pending + updates](https://stripe.com/docs/billing/pending-updates-reference#supported-attributes). + + + Use `error_if_incomplete` if you want Stripe to return an + HTTP 402 status code if a subscription's invoice cannot be + paid. For example, if a payment method requires 3DS + authentication due to SCA regulation and further user action + is needed, this parameter does not update the subscription + and returns an error instead. This was the default behavior + for API versions prior to 2019-03-14. See the + [changelog](https://stripe.com/docs/upgrades#2019-03-14) to + learn more. + enum: + - allow_incomplete + - default_incomplete + - error_if_incomplete + - pending_if_incomplete + type: string + payment_settings: + description: >- + Payment settings to pass to invoices created by the + subscription. + properties: + payment_method_options: + properties: + acss_debit: + anyOf: + - properties: + mandate_options: + properties: + transaction_type: + enum: + - business + - personal + type: string + title: mandate_options_param + type: object + verification_method: + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + bancontact: + anyOf: + - properties: + preferred_language: + enum: + - de + - en + - fr + - nl + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + card: + anyOf: + - properties: + mandate_options: + properties: + amount: + type: integer + amount_type: + enum: + - fixed + - maximum + type: string + description: + maxLength: 200 + type: string + title: mandate_options_param + type: object + network: + enum: + - amex + - cartes_bancaires + - diners + - discover + - eftpos_au + - girocard + - interac + - jcb + - link + - mastercard + - unionpay + - unknown + - visa + maxLength: 5000 + type: string + x-stripeBypassValidation: true + request_three_d_secure: + enum: + - any + - automatic + - challenge + type: string + title: subscription_payment_method_options_param + type: object + - enum: + - '' + type: string + customer_balance: + anyOf: + - properties: + bank_transfer: + properties: + eu_bank_transfer: + properties: + country: + maxLength: 5000 + type: string + required: + - country + title: eu_bank_transfer_param + type: object + type: + type: string + title: bank_transfer_param + type: object + funding_type: + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + konbini: + anyOf: + - properties: {} + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + sepa_debit: + anyOf: + - properties: {} + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + us_bank_account: + anyOf: + - properties: + financial_connections: + properties: + filters: + properties: + account_subcategories: + items: + enum: + - checking + - savings + type: string + type: array + title: >- + invoice_linked_account_options_filters_param + type: object + permissions: + items: + enum: + - balances + - ownership + - payment_method + - transactions + maxLength: 5000 + type: string + x-stripeBypassValidation: true + type: array + prefetch: + items: + enum: + - balances + - ownership + - transactions + type: string + x-stripeBypassValidation: true + type: array + title: invoice_linked_account_options_param + type: object + verification_method: + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + title: payment_method_options + type: object + payment_method_types: + anyOf: + - items: + enum: + - ach_credit_transfer + - ach_debit + - acss_debit + - affirm + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - boleto + - card + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - jp_credit_transfer + - kakao_pay + - klarna + - konbini + - kr_card + - link + - multibanco + - naver_pay + - nz_bank_account + - p24 + - payco + - paynow + - paypal + - promptpay + - revolut_pay + - sepa_credit_transfer + - sepa_debit + - sofort + - swish + - us_bank_account + - wechat_pay + type: string + x-stripeBypassValidation: true + type: array + - enum: + - '' + type: string + save_default_payment_method: + enum: + - 'off' + - on_subscription + type: string + title: payment_settings + type: object + pending_invoice_item_interval: + anyOf: + - properties: + interval: + enum: + - day + - month + - week + - year + type: string + interval_count: + type: integer + required: + - interval + title: pending_invoice_item_interval_params + type: object + - enum: + - '' + type: string + description: >- + Specifies an interval for how often to bill for any pending + invoice items. It is analogous to calling [Create an + invoice](https://stripe.com/docs/api#create_invoice) for the + given subscription at the specified interval. + proration_behavior: + description: >- + Determines how to handle + [prorations](https://stripe.com/docs/billing/subscriptions/prorations) + when the billing cycle changes (e.g., when switching plans, + resetting `billing_cycle_anchor=now`, or starting a trial), + or if an item's `quantity` changes. The default value is + `create_prorations`. + enum: + - always_invoice + - create_prorations + - none + type: string + proration_date: + description: >- + If set, prorations will be calculated as though the + subscription was updated at the given time. This can be used + to apply exactly the same prorations that were previewed + with the [create + preview](https://stripe.com/docs/api/invoices/create_preview) + endpoint. `proration_date` can also be used to implement + custom proration logic, such as prorating by day instead of + by second, by providing the time that you wish to use for + proration calculations. + format: unix-time + type: integer + transfer_data: + anyOf: + - properties: + amount_percent: + type: number + destination: + type: string + required: + - destination + title: transfer_data_specs + type: object + - enum: + - '' + type: string + description: >- + If specified, the funds from the subscription's invoices + will be transferred to the destination and the ID of the + resulting transfers will be found on the resulting charges. + This will be unset if you POST an empty value. + trial_end: + anyOf: + - enum: + - now + maxLength: 5000 + type: string + - format: unix-time + type: integer + description: >- + Unix timestamp representing the end of the trial period the + customer will get before being charged for the first time. + This will always overwrite any trials that might apply via a + subscribed plan. If set, trial_end will override the default + trial period of the plan the customer is being subscribed + to. The special value `now` can be provided to end the + customer's trial immediately. Can be at most two years from + `billing_cycle_anchor`. + trial_from_plan: + description: >- + Indicates if a plan's `trial_period_days` should be applied + to the subscription. Setting `trial_end` per subscription is + preferred, and this defaults to `false`. Setting this flag + to `true` together with `trial_end` is not allowed. See + [Using trial periods on + subscriptions](https://stripe.com/docs/billing/subscriptions/trials) + to learn more. + type: boolean + trial_settings: + description: Settings related to subscription trials. + properties: + end_behavior: + properties: + missing_payment_method: + enum: + - cancel + - create_invoice + - pause + type: string + required: + - missing_payment_method + title: end_behavior + type: object + required: + - end_behavior + title: trial_settings_config + type: object + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/subscription' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Update a subscription on a customer + '/v1/customers/{customer}/subscriptions/{subscription_exposed_id}/discount': + delete: + description:

Removes the currently applied discount on a customer.

+ operationId: DeleteCustomersCustomerSubscriptionsSubscriptionExposedIdDiscount + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: subscription_exposed_id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/deleted_discount' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete a customer discount + get: + description: '' + operationId: GetCustomersCustomerSubscriptionsSubscriptionExposedIdDiscount + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: subscription_exposed_id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/discount' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + '/v1/customers/{customer}/tax_ids': + get: + description:

Returns a list of tax IDs for a customer.

+ operationId: GetCustomersCustomerTaxIds + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/tax_id' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: TaxIDsList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all Customer tax IDs + post: + description:

Creates a new tax_id object for a customer.

+ operationId: PostCustomersCustomerTaxIds + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: + description: >- + Type of the tax ID, one of `ad_nrt`, `ae_trn`, `al_tin`, + `am_tin`, `ao_tin`, `ar_cuit`, `au_abn`, `au_arn`, `aw_tin`, + `az_tin`, `ba_tin`, `bb_tin`, `bd_bin`, `bf_ifu`, `bg_uic`, + `bh_vat`, `bj_ifu`, `bo_tin`, `br_cnpj`, `br_cpf`, `bs_tin`, + `by_tin`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, + `ca_pst_sk`, `ca_qst`, `cd_nif`, `ch_uid`, `ch_vat`, + `cl_tin`, `cm_niu`, `cn_tin`, `co_nit`, `cr_tin`, `cv_nif`, + `de_stn`, `do_rcn`, `ec_ruc`, `eg_tin`, `es_cif`, `et_tin`, + `eu_oss_vat`, `eu_vat`, `gb_vat`, `ge_vat`, `gn_nif`, + `hk_br`, `hr_oib`, `hu_tin`, `id_npwp`, `il_vat`, `in_gst`, + `is_vat`, `jp_cn`, `jp_rn`, `jp_trn`, `ke_pin`, `kg_tin`, + `kh_tin`, `kr_brn`, `kz_bin`, `la_tin`, `li_uid`, `li_vat`, + `ma_vat`, `md_vat`, `me_pib`, `mk_vat`, `mr_nif`, `mx_rfc`, + `my_frp`, `my_itn`, `my_sst`, `ng_tin`, `no_vat`, `no_voec`, + `np_pan`, `nz_gst`, `om_vat`, `pe_ruc`, `ph_tin`, `ro_tin`, + `rs_pib`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, + `si_tin`, `sn_ninea`, `sr_fin`, `sv_nit`, `th_vat`, + `tj_tin`, `tr_tin`, `tw_vat`, `tz_vat`, `ua_vat`, `ug_tin`, + `us_ein`, `uy_ruc`, `uz_tin`, `uz_vat`, `ve_rif`, `vn_tin`, + `za_vat`, `zm_tin`, or `zw_tin` + enum: + - ad_nrt + - ae_trn + - al_tin + - am_tin + - ao_tin + - ar_cuit + - au_abn + - au_arn + - aw_tin + - az_tin + - ba_tin + - bb_tin + - bd_bin + - bf_ifu + - bg_uic + - bh_vat + - bj_ifu + - bo_tin + - br_cnpj + - br_cpf + - bs_tin + - by_tin + - ca_bn + - ca_gst_hst + - ca_pst_bc + - ca_pst_mb + - ca_pst_sk + - ca_qst + - cd_nif + - ch_uid + - ch_vat + - cl_tin + - cm_niu + - cn_tin + - co_nit + - cr_tin + - cv_nif + - de_stn + - do_rcn + - ec_ruc + - eg_tin + - es_cif + - et_tin + - eu_oss_vat + - eu_vat + - gb_vat + - ge_vat + - gn_nif + - hk_br + - hr_oib + - hu_tin + - id_npwp + - il_vat + - in_gst + - is_vat + - jp_cn + - jp_rn + - jp_trn + - ke_pin + - kg_tin + - kh_tin + - kr_brn + - kz_bin + - la_tin + - li_uid + - li_vat + - ma_vat + - md_vat + - me_pib + - mk_vat + - mr_nif + - mx_rfc + - my_frp + - my_itn + - my_sst + - ng_tin + - no_vat + - no_voec + - np_pan + - nz_gst + - om_vat + - pe_ruc + - ph_tin + - ro_tin + - rs_pib + - ru_inn + - ru_kpp + - sa_vat + - sg_gst + - sg_uen + - si_tin + - sn_ninea + - sr_fin + - sv_nit + - th_vat + - tj_tin + - tr_tin + - tw_vat + - tz_vat + - ua_vat + - ug_tin + - us_ein + - uy_ruc + - uz_tin + - uz_vat + - ve_rif + - vn_tin + - za_vat + - zm_tin + - zw_tin + maxLength: 5000 + type: string + x-stripeBypassValidation: true + value: + description: Value of the tax ID. + type: string + required: + - type + - value + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/tax_id' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a Customer tax ID + '/v1/customers/{customer}/tax_ids/{id}': + delete: + description:

Deletes an existing tax_id object.

+ operationId: DeleteCustomersCustomerTaxIdsId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/deleted_tax_id' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete a Customer tax ID + get: + description: >- +

Retrieves the tax_id object with the given + identifier.

+ operationId: GetCustomersCustomerTaxIdsId + parameters: + - in: path + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/tax_id' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a Customer tax ID + /v1/disputes: + get: + description:

Returns a list of your disputes.

+ operationId: GetDisputes + parameters: + - description: >- + Only return disputes associated to the charge specified by this + charge ID. + in: query + name: charge + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + Only return disputes that were created during the given date + interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + Only return disputes associated to the PaymentIntent specified by + this PaymentIntent ID. + in: query + name: payment_intent + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/dispute' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/disputes + type: string + required: + - data + - has_more + - object + - url + title: DisputeList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all disputes + '/v1/disputes/{dispute}': + get: + description:

Retrieves the dispute with the given ID.

+ operationId: GetDisputesDispute + parameters: + - in: path + name: dispute + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/dispute' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a dispute + post: + description: >- +

When you get a dispute, contacting your customer is always the best + first step. If that doesn’t work, you can submit evidence to help us + resolve the dispute in your favor. You can do this in your dashboard, but if you + prefer, you can use the API to submit evidence programmatically.

+ + +

Depending on your dispute type, different evidence fields will give + you a better chance of winning your dispute. To figure out which + evidence fields to provide, see our guide to dispute types.

+ operationId: PostDisputesDispute + parameters: + - in: path + name: dispute + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + evidence: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + evidence: + description: >- + Evidence to upload, to respond to a dispute. Updating any + field in the hash will submit all fields in the hash for + review. The combined character count of all fields is + limited to 150,000. + properties: + access_activity_log: + maxLength: 20000 + type: string + billing_address: + maxLength: 5000 + type: string + cancellation_policy: + type: string + cancellation_policy_disclosure: + maxLength: 20000 + type: string + cancellation_rebuttal: + maxLength: 20000 + type: string + customer_communication: + type: string + customer_email_address: + maxLength: 5000 + type: string + customer_name: + maxLength: 5000 + type: string + customer_purchase_ip: + maxLength: 5000 + type: string + customer_signature: + type: string + duplicate_charge_documentation: + type: string + duplicate_charge_explanation: + maxLength: 20000 + type: string + duplicate_charge_id: + maxLength: 5000 + type: string + enhanced_evidence: + anyOf: + - properties: + visa_compelling_evidence_3: + properties: + disputed_transaction: + properties: + customer_account_id: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + customer_device_fingerprint: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + customer_device_id: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + customer_email_address: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + customer_purchase_ip: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + merchandise_or_services: + enum: + - merchandise + - services + type: string + product_description: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + shipping_address: + properties: + city: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + country: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + line1: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + line2: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + postal_code: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + state: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + title: shipping_address + type: object + title: >- + visa_compelling_evidence3_disputed_transaction + type: object + prior_undisputed_transactions: + items: + properties: + charge: + maxLength: 5000 + type: string + customer_account_id: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + customer_device_fingerprint: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + customer_device_id: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + customer_email_address: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + customer_purchase_ip: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + product_description: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + shipping_address: + properties: + city: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + country: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + line1: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + line2: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + postal_code: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + state: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + title: shipping_address + type: object + required: + - charge + title: >- + visa_compelling_evidence3_prior_undisputed_transaction + type: object + type: array + title: visa_compelling_evidence3 + type: object + visa_compliance: + properties: + fee_acknowledged: + type: boolean + title: visa_compliance + type: object + title: enhanced_evidence + type: object + - enum: + - '' + type: string + product_description: + maxLength: 20000 + type: string + receipt: + type: string + refund_policy: + type: string + refund_policy_disclosure: + maxLength: 20000 + type: string + refund_refusal_explanation: + maxLength: 20000 + type: string + service_date: + maxLength: 5000 + type: string + service_documentation: + type: string + shipping_address: + maxLength: 5000 + type: string + shipping_carrier: + maxLength: 5000 + type: string + shipping_date: + maxLength: 5000 + type: string + shipping_documentation: + type: string + shipping_tracking_number: + maxLength: 5000 + type: string + uncategorized_file: + type: string + uncategorized_text: + maxLength: 20000 + type: string + title: dispute_evidence_params + type: object + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + submit: + description: >- + Whether to immediately submit evidence to the bank. If + `false`, evidence is staged on the dispute. Staged evidence + is visible in the API and Dashboard, and can be submitted to + the bank by making another request with this attribute set + to `true` (the default). + type: boolean + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/dispute' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Update a dispute + '/v1/disputes/{dispute}/close': + post: + description: >- +

Closing the dispute for a charge indicates that you do not have any + evidence to submit and are essentially dismissing the dispute, + acknowledging it as lost.

+ + +

The status of the dispute will change from + needs_response to lost. Closing a dispute + is irreversible.

+ operationId: PostDisputesDisputeClose + parameters: + - in: path + name: dispute + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/dispute' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Close a dispute + /v1/entitlements/active_entitlements: + get: + description:

Retrieve a list of active entitlements for a customer

+ operationId: GetEntitlementsActiveEntitlements + parameters: + - description: The ID of the customer. + in: query + name: customer + required: true + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/entitlements.active_entitlement' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: EntitlementsResourceCustomerEntitlementList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all active entitlements + '/v1/entitlements/active_entitlements/{id}': + get: + description:

Retrieve an active entitlement

+ operationId: GetEntitlementsActiveEntitlementsId + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: The ID of the entitlement. + in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/entitlements.active_entitlement' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve an active entitlement + /v1/entitlements/features: + get: + description:

Retrieve a list of features

+ operationId: GetEntitlementsFeatures + parameters: + - description: >- + If set, filter results to only include features with the given + archive status. + in: query + name: archived + required: false + schema: + type: boolean + style: form + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + If set, filter results to only include features with the given + lookup_key. + in: query + name: lookup_key + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/entitlements.feature' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/entitlements/features + type: string + required: + - data + - has_more + - object + - url + title: EntitlementsResourceFeatureList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all features + post: + description:

Creates a feature

+ operationId: PostEntitlementsFeatures + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + lookup_key: + description: >- + A unique key you provide as your own system identifier. This + may be up to 80 characters. + maxLength: 80 + type: string + metadata: + additionalProperties: + type: string + description: >- + Set of key-value pairs that you can attach to an object. + This can be useful for storing additional information about + the object in a structured format. + type: object + name: + description: >- + The feature's name, for your own purpose, not meant to be + displayable to the customer. + maxLength: 80 + type: string + required: + - lookup_key + - name + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/entitlements.feature' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a feature + '/v1/entitlements/features/{id}': + get: + description:

Retrieves a feature

+ operationId: GetEntitlementsFeaturesId + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: The ID of the feature. + in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/entitlements.feature' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a feature + post: + description:

Update a feature’s metadata or permanently deactivate it.

+ operationId: PostEntitlementsFeaturesId + parameters: + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + active: + description: >- + Inactive features cannot be attached to new products and + will not be returned from the features list endpoint. + type: boolean + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of key-value pairs that you can attach to an object. + This can be useful for storing additional information about + the object in a structured format. + name: + description: >- + The feature's name, for your own purpose, not meant to be + displayable to the customer. + maxLength: 80 + type: string + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/entitlements.feature' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Updates a feature + /v1/ephemeral_keys: + post: + description:

Creates a short-lived API key for a given resource.

+ operationId: PostEphemeralKeys + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + customer: + description: >- + The ID of the Customer you'd like to modify using the + resulting ephemeral key. + maxLength: 5000 + type: string + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + issuing_card: + description: >- + The ID of the Issuing Card you'd like to access using the + resulting ephemeral key. + maxLength: 5000 + type: string + nonce: + description: >- + A single-use token, created by Stripe.js, used for creating + ephemeral keys for Issuing Cards without exchanging + sensitive information. + maxLength: 5000 + type: string + verification_session: + description: >- + The ID of the Identity VerificationSession you'd like to + access using the resulting ephemeral key + maxLength: 5000 + type: string + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ephemeral_key' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create an ephemeral key + '/v1/ephemeral_keys/{key}': + delete: + description:

Invalidates a short-lived API key for a given resource.

+ operationId: DeleteEphemeralKeysKey + parameters: + - in: path + name: key + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ephemeral_key' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Immediately invalidate an ephemeral key + /v1/events: + get: + description: >- +

List events, going back up to 30 days. Each event data is rendered + according to Stripe API version at its creation time, specified in event object + api_version attribute (not according to your current Stripe + API version or Stripe-Version header).

+ operationId: GetEvents + parameters: + - description: Only return events that were created during the given date interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + Filter events by whether all webhooks were successfully delivered. + If false, events which are still pending or have failed all delivery + attempts to a webhook endpoint will be returned. + in: query + name: delivery_success + required: false + schema: + type: boolean + style: form + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A string containing a specific event name, or group of events using + * as a wildcard. The list will be filtered to include only events + with a matching event property. + in: query + name: type + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + An array of up to 20 strings containing specific event names. The + list will be filtered to include only events with a matching event + property. You may pass either `type` or `types`, but not both. + explode: true + in: query + name: types + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/event' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/events + type: string + required: + - data + - has_more + - object + - url + title: NotificationEventList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all events + '/v1/events/{id}': + get: + description: >- +

Retrieves the details of an event if it was created in the last 30 + days. Supply the unique identifier of the event, which you might have + received in a webhook.

+ operationId: GetEventsId + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/event' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve an event + /v1/exchange_rates: + get: + description: >- +

Returns a list of objects that contain the rates at which foreign + currencies are converted to one another. Only shows the currencies for + which Stripe supports.

+ operationId: GetExchangeRates + parameters: + - description: >- + A cursor for use in pagination. `ending_before` is the currency that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with the exchange rate for + currency X your subsequent call can include `ending_before=obj_bar` + in order to fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and total number of supported payout currencies, and the + default is the max. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is the currency + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with the exchange rate + for currency X, your subsequent call can include `starting_after=X` + in order to fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/exchange_rate' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/exchange_rates + type: string + required: + - data + - has_more + - object + - url + title: ExchangeRateList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all exchange rates + '/v1/exchange_rates/{rate_id}': + get: + description: >- +

Retrieves the exchange rates from the given currency to every + supported currency.

+ operationId: GetExchangeRatesRateId + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: rate_id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/exchange_rate' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve an exchange rate + '/v1/external_accounts/{id}': + post: + description: >- +

Updates the metadata, account holder name, account holder type of a + bank account belonging to + + a connected account and optionally sets it as the default for its + currency. Other bank account + + details are not editable by design.

+ + +

You can only update bank accounts when account.controller.requirement_collection + is application, which includes Custom accounts.

+ + +

You can re-enable a disabled bank account by performing an update + call without providing any + + arguments or changes.

+ operationId: PostExternalAccountsId + parameters: + - in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + documents: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + account_holder_name: + description: >- + The name of the person or business that owns the bank + account. + maxLength: 5000 + type: string + account_holder_type: + description: >- + The type of entity that holds the account. This can be + either `individual` or `company`. + enum: + - '' + - company + - individual + maxLength: 5000 + type: string + account_type: + description: >- + The bank account type. This can only be `checking` or + `savings` in most countries. In Japan, this can only be + `futsu` or `toza`. + enum: + - checking + - futsu + - savings + - toza + maxLength: 5000 + type: string + address_city: + description: City/District/Suburb/Town/Village. + maxLength: 5000 + type: string + address_country: + description: 'Billing address country, if provided when creating card.' + maxLength: 5000 + type: string + address_line1: + description: Address line 1 (Street address/PO Box/Company name). + maxLength: 5000 + type: string + address_line2: + description: Address line 2 (Apartment/Suite/Unit/Building). + maxLength: 5000 + type: string + address_state: + description: State/County/Province/Region. + maxLength: 5000 + type: string + address_zip: + description: ZIP or postal code. + maxLength: 5000 + type: string + default_for_currency: + description: >- + When set to true, this becomes the default external account + for its currency. + type: boolean + documents: + description: >- + Documents that may be submitted to satisfy various + informational requests. + properties: + bank_account_ownership_verification: + properties: + files: + items: + maxLength: 500 + type: string + type: array + title: documents_param + type: object + title: external_account_documents_param + type: object + exp_month: + description: Two digit number representing the card’s expiration month. + maxLength: 5000 + type: string + exp_year: + description: Four digit number representing the card’s expiration year. + maxLength: 5000 + type: string + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + name: + description: Cardholder name. + maxLength: 5000 + type: string + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/external_account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + /v1/file_links: + get: + description:

Returns a list of file links.

+ operationId: GetFileLinks + parameters: + - description: Only return links that were created during the given date interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + Filter links by their expiration status. By default, Stripe returns + all links. + in: query + name: expired + required: false + schema: + type: boolean + style: form + - description: Only return links for the given file. + in: query + name: file + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/file_link' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/file_links + type: string + required: + - data + - has_more + - object + - url + title: FileResourceFileLinkList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all file links + post: + description:

Creates a new file link object.

+ operationId: PostFileLinks + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + expires_at: + description: The link isn't usable after this future timestamp. + format: unix-time + type: integer + file: + description: >- + The ID of the file. The file's `purpose` must be one of the + following: `business_icon`, `business_logo`, + `customer_signature`, `dispute_evidence`, + `finance_report_run`, `financial_account_statement`, + `identity_document_downloadable`, + `issuing_regulatory_reporting`, `pci_document`, `selfie`, + `sigma_scheduled_query`, `tax_document_user_upload`, or + `terminal_reader_splashscreen`. + maxLength: 5000 + type: string + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + required: + - file + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/file_link' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a file link + '/v1/file_links/{link}': + get: + description:

Retrieves the file link with the given ID.

+ operationId: GetFileLinksLink + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: link + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/file_link' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a file link + post: + description: >- +

Updates an existing file link object. Expired links can no longer be + updated.

+ operationId: PostFileLinksLink + parameters: + - in: path + name: link + required: true + schema: + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + expires_at: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + expires_at: + anyOf: + - enum: + - now + maxLength: 5000 + type: string + - format: unix-time + type: integer + - enum: + - '' + type: string + description: >- + A future timestamp after which the link will no longer be + usable, or `now` to expire the link immediately. + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/file_link' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Update a file link + /v1/files: + get: + description: >- +

Returns a list of the files that your account has access to. Stripe + sorts and returns the files by their creation dates, placing the most + recently created files at the top.

+ operationId: GetFiles + parameters: + - description: Only return files that were created during the given date interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + Filter queries by the file purpose. If you don't provide a purpose, + the queries return unfiltered files. + in: query + name: purpose + required: false + schema: + enum: + - account_requirement + - additional_verification + - business_icon + - business_logo + - customer_signature + - dispute_evidence + - document_provider_identity_document + - finance_report_run + - financial_account_statement + - identity_document + - identity_document_downloadable + - issuing_regulatory_reporting + - pci_document + - selfie + - sigma_scheduled_query + - tax_document_user_upload + - terminal_reader_splashscreen + maxLength: 5000 + type: string + x-stripeBypassValidation: true + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/file' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/files + type: string + required: + - data + - has_more + - object + - url + title: FileResourceFileList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all files + post: + description: >- +

To upload a file to Stripe, you need to send a request of type + multipart/form-data. Include the file you want to upload in + the request, and the parameters for creating a file.

+ + +

All of Stripe’s officially supported Client libraries support sending + multipart/form-data.

+ operationId: PostFiles + requestBody: + content: + multipart/form-data: + encoding: + expand: + explode: true + style: deepObject + file_link_data: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + file: + description: >- + A file to upload. Make sure that the specifications follow + RFC 2388, which defines file transfers for the + `multipart/form-data` protocol. + format: binary + type: string + file_link_data: + description: >- + Optional parameters that automatically create a [file + link](https://stripe.com/docs/api#file_links) for the newly + created file. + properties: + create: + type: boolean + expires_at: + format: unix-time + type: integer + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + required: + - create + title: file_link_creation_params + type: object + purpose: + description: >- + The + [purpose](https://stripe.com/docs/file-upload#uploading-a-file) + of the uploaded file. + enum: + - account_requirement + - additional_verification + - business_icon + - business_logo + - customer_signature + - dispute_evidence + - identity_document + - issuing_regulatory_reporting + - pci_document + - tax_document_user_upload + - terminal_reader_splashscreen + type: string + x-stripeBypassValidation: true + required: + - file + - purpose + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/file' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + servers: + - url: 'https://files.stripe.com/' + summary: Create a file + '/v1/files/{file}': + get: + description: >- +

Retrieves the details of an existing file object. After you supply a + unique file ID, Stripe returns the corresponding file object. Learn how + to access file + contents.

+ operationId: GetFilesFile + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: file + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/file' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a file + /v1/financial_connections/accounts: + get: + description: >- +

Returns a list of Financial Connections Account + objects.

+ operationId: GetFinancialConnectionsAccounts + parameters: + - description: >- + If present, only return accounts that belong to the specified + account holder. `account_holder[customer]` and + `account_holder[account]` are mutually exclusive. + explode: true + in: query + name: account_holder + required: false + schema: + properties: + account: + maxLength: 5000 + type: string + customer: + maxLength: 5000 + type: string + title: accountholder_params + type: object + style: deepObject + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + If present, only return accounts that were collected as part of the + given session. + in: query + name: session + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/financial_connections.account' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/financial_connections/accounts + type: string + required: + - data + - has_more + - object + - url + title: BankConnectionsResourceLinkedAccountList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List Accounts + '/v1/financial_connections/accounts/{account}': + get: + description: >- +

Retrieves the details of an Financial Connections + Account.

+ operationId: GetFinancialConnectionsAccountsAccount + parameters: + - in: path + name: account + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/financial_connections.account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve an Account + '/v1/financial_connections/accounts/{account}/disconnect': + post: + description: >- +

Disables your access to a Financial Connections Account. + You will no longer be able to access data associated with the account + (e.g. balances, transactions).

+ operationId: PostFinancialConnectionsAccountsAccountDisconnect + parameters: + - in: path + name: account + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/financial_connections.account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Disconnect an Account + '/v1/financial_connections/accounts/{account}/owners': + get: + description:

Lists all owners for a given Account

+ operationId: GetFinancialConnectionsAccountsAccountOwners + parameters: + - in: path + name: account + required: true + schema: + maxLength: 5000 + type: string + style: simple + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: The ID of the ownership object to fetch owners from. + in: query + name: ownership + required: true + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/financial_connections.account_owner' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: BankConnectionsResourceOwnerList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List Account Owners + '/v1/financial_connections/accounts/{account}/refresh': + post: + description: >- +

Refreshes the data associated with a Financial Connections + Account.

+ operationId: PostFinancialConnectionsAccountsAccountRefresh + parameters: + - in: path + name: account + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + features: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + features: + description: The list of account features that you would like to refresh. + items: + enum: + - balance + - ownership + - transactions + type: string + x-stripeBypassValidation: true + type: array + required: + - features + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/financial_connections.account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Refresh Account data + '/v1/financial_connections/accounts/{account}/subscribe': + post: + description: >- +

Subscribes to periodic refreshes of data associated with a Financial + Connections Account.

+ operationId: PostFinancialConnectionsAccountsAccountSubscribe + parameters: + - in: path + name: account + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + features: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + features: + description: >- + The list of account features to which you would like to + subscribe. + items: + enum: + - transactions + type: string + x-stripeBypassValidation: true + type: array + required: + - features + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/financial_connections.account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Subscribe to data refreshes for an Account + '/v1/financial_connections/accounts/{account}/unsubscribe': + post: + description: >- +

Unsubscribes from periodic refreshes of data associated with a + Financial Connections Account.

+ operationId: PostFinancialConnectionsAccountsAccountUnsubscribe + parameters: + - in: path + name: account + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + features: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + features: + description: >- + The list of account features from which you would like to + unsubscribe. + items: + enum: + - transactions + type: string + x-stripeBypassValidation: true + type: array + required: + - features + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/financial_connections.account' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Unsubscribe from data refreshes for an Account + /v1/financial_connections/sessions: + post: + description: >- +

To launch the Financial Connections authorization flow, create a + Session. The session’s client_secret can be + used to launch the flow using Stripe.js.

+ operationId: PostFinancialConnectionsSessions + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + account_holder: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + filters: + explode: true + style: deepObject + permissions: + explode: true + style: deepObject + prefetch: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + account_holder: + description: The account holder to link accounts for. + properties: + account: + maxLength: 5000 + type: string + customer: + maxLength: 5000 + type: string + type: + enum: + - account + - customer + type: string + required: + - type + title: accountholder_params + type: object + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + filters: + description: Filters to restrict the kinds of accounts to collect. + properties: + account_subcategories: + items: + enum: + - checking + - credit_card + - line_of_credit + - mortgage + - savings + type: string + type: array + countries: + items: + maxLength: 5000 + type: string + type: array + title: filters_params + type: object + permissions: + description: >- + List of data features that you would like to request access + to. + + + Possible values are `balances`, `transactions`, `ownership`, + and `payment_method`. + items: + enum: + - balances + - ownership + - payment_method + - transactions + maxLength: 5000 + type: string + x-stripeBypassValidation: true + type: array + prefetch: + description: >- + List of data features that you would like to retrieve upon + account creation. + items: + enum: + - balances + - ownership + - transactions + type: string + x-stripeBypassValidation: true + type: array + return_url: + description: >- + For webview integrations only. Upon completing OAuth login + in the native browser, the user will be redirected to this + URL to return to your app. + maxLength: 5000 + type: string + required: + - account_holder + - permissions + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/financial_connections.session' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a Session + '/v1/financial_connections/sessions/{session}': + get: + description: >- +

Retrieves the details of a Financial Connections + Session

+ operationId: GetFinancialConnectionsSessionsSession + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: session + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/financial_connections.session' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a Session + /v1/financial_connections/transactions: + get: + description: >- +

Returns a list of Financial Connections Transaction + objects.

+ operationId: GetFinancialConnectionsTransactions + parameters: + - description: >- + The ID of the Financial Connections Account whose transactions will + be retrieved. + in: query + name: account + required: true + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A filter on the list based on the object `transacted_at` field. The + value can be a string with an integer Unix timestamp, or it can be a + dictionary with the following options: + explode: true + in: query + name: transacted_at + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + A filter on the list based on the object `transaction_refresh` + field. The value can be a dictionary with the following options: + explode: true + in: query + name: transaction_refresh + required: false + schema: + properties: + after: + maxLength: 5000 + type: string + required: + - after + title: transaction_refresh_params + type: object + style: deepObject + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/financial_connections.transaction' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/financial_connections/transactions + type: string + required: + - data + - has_more + - object + - url + title: BankConnectionsResourceTransactionList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List Transactions + '/v1/financial_connections/transactions/{transaction}': + get: + description: >- +

Retrieves the details of a Financial Connections + Transaction

+ operationId: GetFinancialConnectionsTransactionsTransaction + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: transaction + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/financial_connections.transaction' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a Transaction + /v1/forwarding/requests: + get: + description:

Lists all ForwardingRequest objects.

+ operationId: GetForwardingRequests + parameters: + - description: >- + Similar to other List endpoints, filters results based on created + timestamp. You can pass gt, gte, lt, and lte timestamp values. + explode: true + in: query + name: created + required: false + schema: + properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: created_param + type: object + style: deepObject + - description: >- + A pagination cursor to fetch the previous page of the list. The + value must be a ForwardingRequest ID. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A pagination cursor to fetch the next page of the list. The value + must be a ForwardingRequest ID. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: List of ForwardingRequest data. + properties: + data: + items: + $ref: '#/components/schemas/forwarding.request' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: ForwardingRequestList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all ForwardingRequests + post: + description:

Creates a ForwardingRequest object.

+ operationId: PostForwardingRequests + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + replacements: + explode: true + style: deepObject + request: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + additionalProperties: + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + type: object + payment_method: + description: >- + The PaymentMethod to insert into the forwarded request. + Forwarding previously consumed PaymentMethods is allowed. + maxLength: 5000 + type: string + replacements: + description: The field kinds to be replaced in the forwarded request. + items: + enum: + - card_cvc + - card_expiry + - card_number + - cardholder_name + - request_signature + type: string + x-stripeBypassValidation: true + type: array + request: + description: >- + The request body and headers to be sent to the destination + endpoint. + properties: + body: + maxLength: 5000 + type: string + headers: + items: + properties: + name: + maxLength: 5000 + type: string + value: + maxLength: 5000 + type: string + required: + - name + - value + title: header_param + type: object + type: array + title: request_param + type: object + url: + description: >- + The destination URL for the forwarded request. Must be + supported by the config. + maxLength: 5000 + type: string + required: + - payment_method + - replacements + - url + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/forwarding.request' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a ForwardingRequest + '/v1/forwarding/requests/{id}': + get: + description:

Retrieves a ForwardingRequest object.

+ operationId: GetForwardingRequestsId + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: id + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/forwarding.request' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a ForwardingRequest + /v1/identity/verification_reports: + get: + description:

List all verification reports.

+ operationId: GetIdentityVerificationReports + parameters: + - description: >- + A string to reference this user. This can be a customer ID, a + session ID, or similar, and can be used to reconcile this + verification with your internal systems. + in: query + name: client_reference_id + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + Only return VerificationReports that were created during the given + date interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Only return VerificationReports of this type + in: query + name: type + required: false + schema: + enum: + - document + - id_number + type: string + x-stripeBypassValidation: true + style: form + - description: >- + Only return VerificationReports created by this VerificationSession + ID. It is allowed to provide a VerificationIntent ID. + in: query + name: verification_session + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/identity.verification_report' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/identity/verification_reports + type: string + required: + - data + - has_more + - object + - url + title: GelatoVerificationReportList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List VerificationReports + '/v1/identity/verification_reports/{report}': + get: + description:

Retrieves an existing VerificationReport

+ operationId: GetIdentityVerificationReportsReport + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: report + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/identity.verification_report' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a VerificationReport + /v1/identity/verification_sessions: + get: + description:

Returns a list of VerificationSessions

+ operationId: GetIdentityVerificationSessions + parameters: + - description: >- + A string to reference this user. This can be a customer ID, a + session ID, or similar, and can be used to reconcile this + verification with your internal systems. + in: query + name: client_reference_id + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + Only return VerificationSessions that were created during the given + date interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - in: query + name: related_customer + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + Only return VerificationSessions with this status. [Learn more about + the lifecycle of + sessions](https://stripe.com/docs/identity/how-sessions-work). + in: query + name: status + required: false + schema: + enum: + - canceled + - processing + - requires_input + - verified + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/identity.verification_session' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/identity/verification_sessions + type: string + required: + - data + - has_more + - object + - url + title: GelatoVerificationSessionList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List VerificationSessions + post: + description: >- +

Creates a VerificationSession object.

+ + +

After the VerificationSession is created, display a verification + modal using the session client_secret or send your users to + the session’s url.

+ + +

If your API key is in test mode, verification checks won’t actually + process, though everything else will occur as if in live mode.

+ + +

Related guide: Verify your users’ + identity documents

+ operationId: PostIdentityVerificationSessions + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + options: + explode: true + style: deepObject + provided_details: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + client_reference_id: + description: >- + A string to reference this user. This can be a customer ID, + a session ID, or similar, and can be used to reconcile this + verification with your internal systems. + maxLength: 5000 + type: string + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + additionalProperties: + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + type: object + options: + description: A set of options for the session’s verification checks. + properties: + document: + anyOf: + - properties: + allowed_types: + items: + enum: + - driving_license + - id_card + - passport + type: string + type: array + require_id_number: + type: boolean + require_live_capture: + type: boolean + require_matching_selfie: + type: boolean + title: document_options + type: object + - enum: + - '' + type: string + title: session_options_param + type: object + provided_details: + description: >- + Details provided about the user being verified. These + details may be shown to the user. + properties: + email: + type: string + phone: + type: string + title: provided_details_param + type: object + related_customer: + description: Customer ID + maxLength: 5000 + type: string + return_url: + description: >- + The URL that the user will be redirected to upon completing + the verification flow. + type: string + type: + description: >- + The type of [verification + check](https://stripe.com/docs/identity/verification-checks) + to be performed. You must provide a `type` if not passing + `verification_flow`. + enum: + - document + - id_number + type: string + x-stripeBypassValidation: true + verification_flow: + description: >- + The ID of a verification flow from the Dashboard. See + https://docs.stripe.com/identity/verification-flows. + maxLength: 5000 + type: string + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/identity.verification_session' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a VerificationSession + '/v1/identity/verification_sessions/{session}': + get: + description: >- +

Retrieves the details of a VerificationSession that was previously + created.

+ + +

When the session status is requires_input, you can use + this method to retrieve a valid + + client_secret or url to allow + re-submission.

+ operationId: GetIdentityVerificationSessionsSession + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: session + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/identity.verification_session' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve a VerificationSession + post: + description: >- +

Updates a VerificationSession object.

+ + +

When the session status is requires_input, you can use + this method to update the + + verification check and options.

+ operationId: PostIdentityVerificationSessionsSession + parameters: + - in: path + name: session + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + options: + explode: true + style: deepObject + provided_details: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + additionalProperties: + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + type: object + options: + description: A set of options for the session’s verification checks. + properties: + document: + anyOf: + - properties: + allowed_types: + items: + enum: + - driving_license + - id_card + - passport + type: string + type: array + require_id_number: + type: boolean + require_live_capture: + type: boolean + require_matching_selfie: + type: boolean + title: document_options + type: object + - enum: + - '' + type: string + title: session_options_param + type: object + provided_details: + description: >- + Details provided about the user being verified. These + details may be shown to the user. + properties: + email: + type: string + phone: + type: string + title: provided_details_param + type: object + type: + description: >- + The type of [verification + check](https://stripe.com/docs/identity/verification-checks) + to be performed. + enum: + - document + - id_number + type: string + x-stripeBypassValidation: true + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/identity.verification_session' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Update a VerificationSession + '/v1/identity/verification_sessions/{session}/cancel': + post: + description: >- +

A VerificationSession object can be canceled when it is in + requires_input status.

+ + +

Once canceled, future submission attempts are disabled. This cannot + be undone. Learn + more.

+ operationId: PostIdentityVerificationSessionsSessionCancel + parameters: + - in: path + name: session + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/identity.verification_session' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Cancel a VerificationSession + '/v1/identity/verification_sessions/{session}/redact': + post: + description: >- +

Redact a VerificationSession to remove all collected information from + Stripe. This will redact + + the VerificationSession and all objects related to it, including + VerificationReports, Events, + + request logs, etc.

+ + +

A VerificationSession object can be redacted when it is in + requires_input or verified + + status. Redacting a + VerificationSession in requires_action + + state will automatically cancel it.

+ + +

The redaction process may take up to four days. When the redaction + process is in progress, the + + VerificationSession’s redaction.status field will be set to + processing; when the process is + + finished, it will change to redacted and an + identity.verification_session.redacted event + + will be emitted.

+ + +

Redaction is irreversible. Redacted objects are still accessible in + the Stripe API, but all the + + fields that contain personal data will be replaced by the string + [redacted] or a similar + + placeholder. The metadata field will also be erased. + Redacted objects cannot be updated or + + used for any purpose.

+ + +

Learn + more.

+ operationId: PostIdentityVerificationSessionsSessionRedact + parameters: + - in: path + name: session + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/identity.verification_session' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Redact a VerificationSession + /v1/invoice_payments: + get: + description: >- +

When retrieving an invoice, there is an includable payments property + containing the first handful of those items. There is also a URL where + you can retrieve the full (paginated) list of payments.

+ operationId: GetInvoicePayments + parameters: + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: The identifier of the invoice whose payments to return. + in: query + name: invoice + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: The payment details of the invoice payments to return. + explode: true + in: query + name: payment + required: false + schema: + properties: + payment_intent: + maxLength: 5000 + type: string + type: + enum: + - payment_intent + type: string + x-stripeBypassValidation: true + required: + - type + title: payment_param + type: object + style: deepObject + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: The status of the invoice payments to return. + in: query + name: status + required: false + schema: + enum: + - canceled + - open + - paid + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/invoice_payment' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: InvoicesPaymentsListInvoicePayments + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all payments for an invoice + '/v1/invoice_payments/{invoice_payment}': + get: + description:

Retrieves the invoice payment with the given ID.

+ operationId: GetInvoicePaymentsInvoicePayment + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: invoice_payment + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoice_payment' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve an InvoicePayment + /v1/invoice_rendering_templates: + get: + description: >- +

List all templates, ordered by creation date, with the most recently + created template appearing first.

+ operationId: GetInvoiceRenderingTemplates + parameters: + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + - in: query + name: status + required: false + schema: + enum: + - active + - archived + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/invoice_rendering_template' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: InvoiceRenderingTemplatesList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all invoice rendering templates + '/v1/invoice_rendering_templates/{template}': + get: + description: >- +

Retrieves an invoice rendering template with the given ID. It by + default returns the latest version of the template. Optionally, specify + a version to see previous versions.

+ operationId: GetInvoiceRenderingTemplatesTemplate + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: template + required: true + schema: + maxLength: 5000 + type: string + style: simple + - in: query + name: version + required: false + schema: + type: integer + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoice_rendering_template' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve an invoice rendering template + '/v1/invoice_rendering_templates/{template}/archive': + post: + description: >- +

Updates the status of an invoice rendering template to ‘archived’ so + no new Stripe objects (customers, invoices, etc.) can reference it. The + template can also no longer be updated. However, if the template is + already set on a Stripe object, it will continue to be applied on + invoices generated by it.

+ operationId: PostInvoiceRenderingTemplatesTemplateArchive + parameters: + - in: path + name: template + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoice_rendering_template' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Archive an invoice rendering template + '/v1/invoice_rendering_templates/{template}/unarchive': + post: + description: >- +

Unarchive an invoice rendering template so it can be used on new + Stripe objects again.

+ operationId: PostInvoiceRenderingTemplatesTemplateUnarchive + parameters: + - in: path + name: template + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + expand: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoice_rendering_template' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Unarchive an invoice rendering template + /v1/invoiceitems: + get: + description: >- +

Returns a list of your invoice items. Invoice items are returned + sorted by creation date, with the most recently created invoice items + appearing first.

+ operationId: GetInvoiceitems + parameters: + - description: >- + Only return invoice items that were created during the given date + interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + The identifier of the customer whose invoice items to return. If + none is provided, all invoice items will be returned. + in: query + name: customer + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + Only return invoice items belonging to this invoice. If none is + provided, all invoice items will be returned. If specifying an + invoice, no customer identifier is needed. + in: query + name: invoice + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + Set to `true` to only show pending invoice items, which are not yet + attached to any invoices. Set to `false` to only show invoice items + already attached to invoices. If unspecified, no filter is applied. + in: query + name: pending + required: false + schema: + type: boolean + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/invoiceitem' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/invoiceitems + type: string + required: + - data + - has_more + - object + - url + title: InvoicesItemsList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all invoice items + post: + description: >- +

Creates an item to be added to a draft invoice (up to 250 items per + invoice). If no invoice is specified, the item will be on the next + invoice created for the customer specified.

+ operationId: PostInvoiceitems + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + discounts: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + period: + explode: true + style: deepObject + price_data: + explode: true + style: deepObject + pricing: + explode: true + style: deepObject + tax_code: + explode: true + style: deepObject + tax_rates: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + amount: + description: >- + The integer amount in cents (or local equivalent) of the + charge to be applied to the upcoming invoice. Passing in a + negative `amount` will reduce the `amount_due` on the + invoice. + type: integer + currency: + description: >- + Three-letter [ISO currency + code](https://www.iso.org/iso-4217-currency-codes.html), in + lowercase. Must be a [supported + currency](https://stripe.com/docs/currencies). + format: currency + type: string + customer: + description: >- + The ID of the customer who will be billed when this invoice + item is billed. + maxLength: 5000 + type: string + description: + description: >- + An arbitrary string which you can attach to the invoice + item. The description is displayed in the invoice for easy + tracking. + maxLength: 5000 + type: string + discountable: + description: >- + Controls whether discounts apply to this invoice item. + Defaults to false for prorations or negative invoice items, + and true for all other invoice items. + type: boolean + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + description: >- + The coupons and promotion codes to redeem into discounts for + the invoice item or invoice line item. + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + invoice: + description: >- + The ID of an existing invoice to add this invoice item to. + When left blank, the invoice item will be added to the next + upcoming scheduled invoice. This is useful when adding + invoice items in response to an invoice.created webhook. You + can only add invoice items to draft invoices and there is a + maximum of 250 items per invoice. + maxLength: 5000 + type: string + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + period: + description: >- + The period associated with this invoice item. When set to + different values, the period will be rendered on the + invoice. If you have [Stripe Revenue + Recognition](https://stripe.com/docs/revenue-recognition) + enabled, the period will be used to recognize and defer + revenue. See the [Revenue Recognition + documentation](https://stripe.com/docs/revenue-recognition/methodology/subscriptions-and-invoicing) + for details. + properties: + end: + format: unix-time + type: integer + start: + format: unix-time + type: integer + required: + - end + - start + title: period + type: object + price_data: + description: >- + Data used to generate a new + [Price](https://stripe.com/docs/api/prices) object inline. + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + title: one_time_price_data + type: object + pricing: + description: The pricing information for the invoice item. + properties: + price: + maxLength: 5000 + type: string + title: pricing_param + type: object + quantity: + description: >- + Non-negative integer. The quantity of units for the invoice + item. + type: integer + subscription: + description: >- + The ID of a subscription to add this invoice item to. When + left blank, the invoice item is added to the next upcoming + scheduled invoice. When set, scheduled invoices for + subscriptions other than the specified subscription will + ignore the invoice item. Use this when you want to express + that an invoice item has been accrued within the context of + a particular subscription. + maxLength: 5000 + type: string + tax_behavior: + description: >- + Only required if a [default tax + behavior](https://stripe.com/docs/tax/products-prices-tax-categories-tax-behavior#setting-a-default-tax-behavior-(recommended)) + was not provided in the Stripe Tax settings. Specifies + whether the price is considered inclusive of taxes or + exclusive of taxes. One of `inclusive`, `exclusive`, or + `unspecified`. Once specified as either `inclusive` or + `exclusive`, it cannot be changed. + enum: + - exclusive + - inclusive + - unspecified + type: string + tax_code: + anyOf: + - type: string + - enum: + - '' + type: string + description: 'A [tax code](https://stripe.com/docs/tax/tax-categories) ID.' + tax_rates: + description: >- + The tax rates which apply to the invoice item. When set, the + `default_tax_rates` on the invoice do not apply to this + invoice item. + items: + maxLength: 5000 + type: string + type: array + unit_amount_decimal: + description: >- + The decimal unit amount in cents (or local equivalent) of + the charge to be applied to the upcoming invoice. This + `unit_amount_decimal` will be multiplied by the quantity to + get the full amount. Passing in a negative + `unit_amount_decimal` will reduce the `amount_due` on the + invoice. Accepts at most 12 decimal places. + format: decimal + type: string + required: + - customer + type: object + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoiceitem' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create an invoice item + '/v1/invoiceitems/{invoiceitem}': + delete: + description: >- +

Deletes an invoice item, removing it from an invoice. Deleting + invoice items is only possible when they’re not attached to invoices, or + if it’s attached to a draft invoice.

+ operationId: DeleteInvoiceitemsInvoiceitem + parameters: + - in: path + name: invoiceitem + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/deleted_invoiceitem' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete an invoice item + get: + description:

Retrieves the invoice item with the given ID.

+ operationId: GetInvoiceitemsInvoiceitem + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: invoiceitem + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoiceitem' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve an invoice item + post: + description: >- +

Updates the amount or description of an invoice item on an upcoming + invoice. Updating an invoice item is only possible before the invoice + it’s attached to is closed.

+ operationId: PostInvoiceitemsInvoiceitem + parameters: + - in: path + name: invoiceitem + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + discounts: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + period: + explode: true + style: deepObject + price_data: + explode: true + style: deepObject + pricing: + explode: true + style: deepObject + tax_code: + explode: true + style: deepObject + tax_rates: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + amount: + description: >- + The integer amount in cents (or local equivalent) of the + charge to be applied to the upcoming invoice. If you want to + apply a credit to the customer's account, pass a negative + amount. + type: integer + description: + description: >- + An arbitrary string which you can attach to the invoice + item. The description is displayed in the invoice for easy + tracking. + maxLength: 5000 + type: string + discountable: + description: >- + Controls whether discounts apply to this invoice item. + Defaults to false for prorations or negative invoice items, + and true for all other invoice items. Cannot be set to true + for prorations. + type: boolean + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + description: >- + The coupons, promotion codes & existing discounts which + apply to the invoice item or invoice line item. Item + discounts are applied before invoice discounts. Pass an + empty string to remove previously-defined discounts. + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + period: + description: >- + The period associated with this invoice item. When set to + different values, the period will be rendered on the + invoice. If you have [Stripe Revenue + Recognition](https://stripe.com/docs/revenue-recognition) + enabled, the period will be used to recognize and defer + revenue. See the [Revenue Recognition + documentation](https://stripe.com/docs/revenue-recognition/methodology/subscriptions-and-invoicing) + for details. + properties: + end: + format: unix-time + type: integer + start: + format: unix-time + type: integer + required: + - end + - start + title: period + type: object + price_data: + description: >- + Data used to generate a new + [Price](https://stripe.com/docs/api/prices) object inline. + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + title: one_time_price_data + type: object + pricing: + description: The pricing information for the invoice item. + properties: + price: + maxLength: 5000 + type: string + title: pricing_param + type: object + quantity: + description: >- + Non-negative integer. The quantity of units for the invoice + item. + type: integer + tax_behavior: + description: >- + Only required if a [default tax + behavior](https://stripe.com/docs/tax/products-prices-tax-categories-tax-behavior#setting-a-default-tax-behavior-(recommended)) + was not provided in the Stripe Tax settings. Specifies + whether the price is considered inclusive of taxes or + exclusive of taxes. One of `inclusive`, `exclusive`, or + `unspecified`. Once specified as either `inclusive` or + `exclusive`, it cannot be changed. + enum: + - exclusive + - inclusive + - unspecified + type: string + tax_code: + anyOf: + - type: string + - enum: + - '' + type: string + description: 'A [tax code](https://stripe.com/docs/tax/tax-categories) ID.' + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + description: >- + The tax rates which apply to the invoice item. When set, the + `default_tax_rates` on the invoice do not apply to this + invoice item. Pass an empty string to remove + previously-defined tax rates. + unit_amount_decimal: + description: >- + The decimal unit amount in cents (or local equivalent) of + the charge to be applied to the upcoming invoice. This + `unit_amount_decimal` will be multiplied by the quantity to + get the full amount. Passing in a negative + `unit_amount_decimal` will reduce the `amount_due` on the + invoice. Accepts at most 12 decimal places. + format: decimal + type: string + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoiceitem' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Update an invoice item + /v1/invoices: + get: + description: >- +

You can list all invoices, or list the invoices for a specific + customer. The invoices are returned sorted by creation date, with the + most recently created invoices appearing first.

+ operationId: GetInvoices + parameters: + - description: >- + The collection method of the invoice to retrieve. Either + `charge_automatically` or `send_invoice`. + in: query + name: collection_method + required: false + schema: + enum: + - charge_automatically + - send_invoice + type: string + style: form + - description: >- + Only return invoices that were created during the given date + interval. + explode: true + in: query + name: created + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: Only return invoices for the customer specified by this customer ID. + in: query + name: customer + required: false + schema: + maxLength: 5000 + type: string + style: form + - explode: true + in: query + name: due_date + required: false + schema: + anyOf: + - properties: + gt: + type: integer + gte: + type: integer + lt: + type: integer + lte: + type: integer + title: range_query_specs + type: object + - type: integer + style: deepObject + - description: >- + A cursor for use in pagination. `ending_before` is an object ID that + defines your place in the list. For instance, if you make a list + request and receive 100 objects, starting with `obj_bar`, your + subsequent call can include `ending_before=obj_bar` in order to + fetch the previous page of the list. + in: query + name: ending_before + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for use in pagination. `starting_after` is an object ID + that defines your place in the list. For instance, if you make a + list request and receive 100 objects, ending with `obj_foo`, your + subsequent call can include `starting_after=obj_foo` in order to + fetch the next page of the list. + in: query + name: starting_after + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + The status of the invoice, one of `draft`, `open`, `paid`, + `uncollectible`, or `void`. [Learn + more](https://stripe.com/docs/billing/invoices/workflow#workflow-overview) + in: query + name: status + required: false + schema: + enum: + - draft + - open + - paid + - uncollectible + - void + type: string + style: form + - description: >- + Only return invoices for the subscription specified by this + subscription ID. + in: query + name: subscription + required: false + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/invoice' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/invoices + type: string + required: + - data + - has_more + - object + - url + title: InvoicesResourceList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all invoices + post: + description: >- +

This endpoint creates a draft invoice for a given customer. The + invoice remains a draft until you finalize the invoice, which allows you to + pay or send the + invoice to your customers.

+ operationId: PostInvoices + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + account_tax_ids: + explode: true + style: deepObject + automatic_tax: + explode: true + style: deepObject + custom_fields: + explode: true + style: deepObject + default_tax_rates: + explode: true + style: deepObject + discounts: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + from_invoice: + explode: true + style: deepObject + issuer: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + payment_settings: + explode: true + style: deepObject + rendering: + explode: true + style: deepObject + shipping_cost: + explode: true + style: deepObject + shipping_details: + explode: true + style: deepObject + transfer_data: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + account_tax_ids: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + description: >- + The account tax IDs associated with the invoice. Only + editable when the invoice is a draft. + application_fee_amount: + description: >- + A fee in cents (or local equivalent) that will be applied to + the invoice and transferred to the application owner's + Stripe account. The request must be made with an OAuth key + or the Stripe-Account header in order to take an application + fee. For more information, see the application fees + [documentation](https://stripe.com/docs/billing/invoices/connect#collecting-fees). + type: integer + auto_advance: + description: >- + Controls whether Stripe performs [automatic + collection](https://stripe.com/docs/invoicing/integration/automatic-advancement-collection) + of the invoice. If `false`, the invoice's state doesn't + automatically advance without an explicit action. + type: boolean + automatic_tax: + description: Settings for automatic tax lookup for this invoice. + properties: + enabled: + type: boolean + liability: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + required: + - enabled + title: automatic_tax_param + type: object + automatically_finalizes_at: + description: >- + The time when this invoice should be scheduled to finalize. + The invoice will be finalized at this time if it is still in + draft state. + format: unix-time + type: integer + collection_method: + description: >- + Either `charge_automatically`, or `send_invoice`. When + charging automatically, Stripe will attempt to pay this + invoice using the default source attached to the customer. + When sending an invoice, Stripe will email this invoice to + the customer with payment instructions. Defaults to + `charge_automatically`. + enum: + - charge_automatically + - send_invoice + type: string + currency: + description: >- + The currency to create this invoice in. Defaults to that of + `customer` if not specified. + format: currency + type: string + custom_fields: + anyOf: + - items: + properties: + name: + maxLength: 40 + type: string + value: + maxLength: 140 + type: string + required: + - name + - value + title: custom_field_params + type: object + type: array + - enum: + - '' + type: string + description: >- + A list of up to 4 custom fields to be displayed on the + invoice. + customer: + description: The ID of the customer who will be billed. + maxLength: 5000 + type: string + days_until_due: + description: >- + The number of days from when the invoice is created until it + is due. Valid only for invoices where + `collection_method=send_invoice`. + type: integer + default_payment_method: + description: >- + ID of the default payment method for the invoice. It must + belong to the customer associated with the invoice. If not + set, defaults to the subscription's default payment method, + if any, or to the default payment method in the customer's + invoice settings. + maxLength: 5000 + type: string + default_source: + description: >- + ID of the default payment source for the invoice. It must + belong to the customer associated with the invoice and be in + a chargeable state. If not set, defaults to the + subscription's default source, if any, or to the customer's + default source. + maxLength: 5000 + type: string + default_tax_rates: + description: >- + The tax rates that will apply to any line item that does not + have `tax_rates` set. + items: + maxLength: 5000 + type: string + type: array + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. Referenced as 'memo' in the Dashboard. + maxLength: 1500 + type: string + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + description: >- + The coupons and promotion codes to redeem into discounts for + the invoice. If not specified, inherits the discount from + the invoice's customer. Pass an empty string to avoid + inheriting any discounts. + due_date: + description: >- + The date on which payment for this invoice is due. Valid + only for invoices where `collection_method=send_invoice`. + format: unix-time + type: integer + effective_at: + description: >- + The date when this invoice is in effect. Same as + `finalized_at` unless overwritten. When defined, this value + replaces the system-generated 'Date of issue' printed on the + invoice PDF and receipt. + format: unix-time + type: integer + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + footer: + description: Footer to be displayed on the invoice. + maxLength: 5000 + type: string + from_invoice: + description: >- + Revise an existing invoice. The new invoice will be created + in `status=draft`. See the [revision + documentation](https://stripe.com/docs/invoicing/invoice-revisions) + for more details. + properties: + action: + enum: + - revision + maxLength: 5000 + type: string + invoice: + maxLength: 5000 + type: string + required: + - action + - invoice + title: from_invoice + type: object + issuer: + description: >- + The connected account that issues the invoice. The invoice + is presented with the branding and support information of + the specified account. + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + number: + description: >- + Set the number for this invoice. If no number is present + then a number will be assigned automatically when the + invoice is finalized. In many markets, regulations require + invoices to be unique, sequential and / or gapless. You are + responsible for ensuring this is true across all your + different invoicing systems in the event that you edit the + invoice number using our API. If you use only Stripe for + your invoices and do not change invoice numbers, Stripe + handles this aspect of compliance for you automatically. + maxLength: 26 + type: string + on_behalf_of: + description: >- + The account (if any) for which the funds of the invoice + payment are intended. If set, the invoice will be presented + with the branding and support information of the specified + account. See the [Invoices with + Connect](https://stripe.com/docs/billing/invoices/connect) + documentation for details. + type: string + payment_settings: + description: >- + Configuration settings for the PaymentIntent that is + generated when the invoice is finalized. + properties: + default_mandate: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + payment_method_options: + properties: + acss_debit: + anyOf: + - properties: + mandate_options: + properties: + transaction_type: + enum: + - business + - personal + type: string + title: mandate_options_param + type: object + verification_method: + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + bancontact: + anyOf: + - properties: + preferred_language: + enum: + - de + - en + - fr + - nl + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + card: + anyOf: + - properties: + installments: + properties: + enabled: + type: boolean + plan: + anyOf: + - properties: + count: + type: integer + interval: + enum: + - month + type: string + type: + enum: + - fixed_count + type: string + x-stripeBypassValidation: true + required: + - type + title: installment_plan + type: object + - enum: + - '' + type: string + title: installments_param + type: object + request_three_d_secure: + enum: + - any + - automatic + - challenge + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + customer_balance: + anyOf: + - properties: + bank_transfer: + properties: + eu_bank_transfer: + properties: + country: + maxLength: 5000 + type: string + required: + - country + title: eu_bank_transfer_param + type: object + type: + type: string + title: bank_transfer_param + type: object + funding_type: + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + konbini: + anyOf: + - properties: {} + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + sepa_debit: + anyOf: + - properties: {} + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + us_bank_account: + anyOf: + - properties: + financial_connections: + properties: + filters: + properties: + account_subcategories: + items: + enum: + - checking + - savings + type: string + type: array + title: >- + invoice_linked_account_options_filters_param + type: object + permissions: + items: + enum: + - balances + - ownership + - payment_method + - transactions + maxLength: 5000 + type: string + x-stripeBypassValidation: true + type: array + prefetch: + items: + enum: + - balances + - ownership + - transactions + type: string + x-stripeBypassValidation: true + type: array + title: invoice_linked_account_options_param + type: object + verification_method: + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + title: payment_method_options + type: object + payment_method_types: + anyOf: + - items: + enum: + - ach_credit_transfer + - ach_debit + - acss_debit + - affirm + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - boleto + - card + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - jp_credit_transfer + - kakao_pay + - klarna + - konbini + - kr_card + - link + - multibanco + - naver_pay + - nz_bank_account + - p24 + - payco + - paynow + - paypal + - promptpay + - revolut_pay + - sepa_credit_transfer + - sepa_debit + - sofort + - swish + - us_bank_account + - wechat_pay + type: string + x-stripeBypassValidation: true + type: array + - enum: + - '' + type: string + title: payment_settings + type: object + pending_invoice_items_behavior: + description: >- + How to handle pending invoice items on invoice creation. + Defaults to `exclude` if the parameter is omitted. + enum: + - exclude + - include + type: string + x-stripeBypassValidation: true + rendering: + description: >- + The rendering-related settings that control how the invoice + is displayed on customer-facing surfaces such as PDF and + Hosted Invoice Page. + properties: + amount_tax_display: + enum: + - '' + - exclude_tax + - include_inclusive_tax + type: string + pdf: + properties: + page_size: + enum: + - a4 + - auto + - letter + type: string + title: rendering_pdf_param + type: object + template: + maxLength: 5000 + type: string + template_version: + anyOf: + - type: integer + - enum: + - '' + type: string + title: rendering_param + type: object + shipping_cost: + description: Settings for the cost of shipping for this invoice. + properties: + shipping_rate: + maxLength: 5000 + type: string + shipping_rate_data: + properties: + delivery_estimate: + properties: + maximum: + properties: + unit: + enum: + - business_day + - day + - hour + - month + - week + type: string + value: + type: integer + required: + - unit + - value + title: delivery_estimate_bound + type: object + minimum: + properties: + unit: + enum: + - business_day + - day + - hour + - month + - week + type: string + value: + type: integer + required: + - unit + - value + title: delivery_estimate_bound + type: object + title: delivery_estimate + type: object + display_name: + maxLength: 100 + type: string + fixed_amount: + properties: + amount: + type: integer + currency: + format: currency + type: string + currency_options: + additionalProperties: + properties: + amount: + type: integer + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + required: + - amount + title: currency_option + type: object + type: object + required: + - amount + - currency + title: fixed_amount + type: object + metadata: + additionalProperties: + type: string + type: object + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + tax_code: + type: string + type: + enum: + - fixed_amount + type: string + required: + - display_name + title: method_params + type: object + title: shipping_cost + type: object + shipping_details: + description: >- + Shipping details for the invoice. The Invoice PDF will use + the `shipping_details` value if it is set, otherwise the PDF + will render the shipping address from the customer. + properties: + address: + properties: + city: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + line1: + maxLength: 5000 + type: string + line2: + maxLength: 5000 + type: string + postal_code: + maxLength: 5000 + type: string + state: + maxLength: 5000 + type: string + title: optional_fields_address + type: object + name: + maxLength: 5000 + type: string + phone: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + required: + - address + - name + title: recipient_shipping_with_optional_fields_address + type: object + statement_descriptor: + description: >- + Extra information about a charge for the customer's credit + card statement. It must contain at least one letter. If not + specified and this invoice is part of a subscription, the + default `statement_descriptor` will be set to the first + subscription item's product's `statement_descriptor`. + maxLength: 22 + type: string + subscription: + description: >- + The ID of the subscription to invoice, if any. If set, the + created invoice will only include pending invoice items for + that subscription. The subscription's billing cycle and + regular subscription events won't be affected. + maxLength: 5000 + type: string + transfer_data: + description: >- + If specified, the funds from the invoice will be transferred + to the destination and the ID of the resulting transfer will + be found on the invoice's charge. + properties: + amount: + type: integer + destination: + type: string + required: + - destination + title: transfer_data_specs + type: object + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoice' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create an invoice + /v1/invoices/create_preview: + post: + description: >- +

At any time, you can preview the upcoming invoice for a subscription + or subscription schedule. This will show you all the charges that are + pending, including subscription renewal charges, invoice item charges, + etc. It will also show you any discounts that are applicable to the + invoice.

+ + +

You can also preview the effects of creating or updating a + subscription or subscription schedule, including a preview of any + prorations that will take place. To ensure that the actual proration is + calculated exactly the same as the previewed proration, you should pass + the subscription_details.proration_date parameter when + doing the actual subscription update.

+ + +

The recommended way to get only the prorations being previewed on the + invoice is to consider line items where + parent.subscription_item_details.proration is + true.

+ + +

Note that when you are viewing an upcoming invoice, you are simply + viewing a preview – the invoice has not yet been created. As such, the + upcoming invoice will not show up in invoice listing calls, and you + cannot use the API to pay or edit the invoice. If you want to change the + amount that your customer will be billed, you can add, remove, or update + pending invoice items, or update the customer’s discount.

+ + +

Note: Currency conversion calculations use the latest exchange rates. + Exchange rates may vary between the time of the preview and the time of + the actual invoice creation. Learn more

+ operationId: PostInvoicesCreatePreview + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + automatic_tax: + explode: true + style: deepObject + customer_details: + explode: true + style: deepObject + discounts: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + invoice_items: + explode: true + style: deepObject + issuer: + explode: true + style: deepObject + on_behalf_of: + explode: true + style: deepObject + schedule_details: + explode: true + style: deepObject + subscription_details: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + automatic_tax: + description: Settings for automatic tax lookup for this invoice preview. + properties: + enabled: + type: boolean + liability: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + required: + - enabled + title: automatic_tax_param + type: object + currency: + description: >- + The currency to preview this invoice in. Defaults to that of + `customer` if not specified. + format: currency + type: string + customer: + description: >- + The identifier of the customer whose upcoming invoice you'd + like to retrieve. If `automatic_tax` is enabled then one of + `customer`, `customer_details`, `subscription`, or + `schedule` must be set. + maxLength: 5000 + type: string + customer_details: + description: >- + Details about the customer you want to invoice or overrides + for an existing customer. If `automatic_tax` is enabled then + one of `customer`, `customer_details`, `subscription`, or + `schedule` must be set. + properties: + address: + anyOf: + - properties: + city: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + line1: + maxLength: 5000 + type: string + line2: + maxLength: 5000 + type: string + postal_code: + maxLength: 5000 + type: string + state: + maxLength: 5000 + type: string + title: optional_fields_address + type: object + - enum: + - '' + type: string + shipping: + anyOf: + - properties: + address: + properties: + city: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + line1: + maxLength: 5000 + type: string + line2: + maxLength: 5000 + type: string + postal_code: + maxLength: 5000 + type: string + state: + maxLength: 5000 + type: string + title: optional_fields_customer_address + type: object + name: + maxLength: 5000 + type: string + phone: + maxLength: 5000 + type: string + required: + - address + - name + title: customer_shipping + type: object + - enum: + - '' + type: string + tax: + properties: + ip_address: + anyOf: + - type: string + - enum: + - '' + type: string + title: tax_param + type: object + tax_exempt: + enum: + - '' + - exempt + - none + - reverse + type: string + tax_ids: + items: + properties: + type: + enum: + - ad_nrt + - ae_trn + - al_tin + - am_tin + - ao_tin + - ar_cuit + - au_abn + - au_arn + - aw_tin + - az_tin + - ba_tin + - bb_tin + - bd_bin + - bf_ifu + - bg_uic + - bh_vat + - bj_ifu + - bo_tin + - br_cnpj + - br_cpf + - bs_tin + - by_tin + - ca_bn + - ca_gst_hst + - ca_pst_bc + - ca_pst_mb + - ca_pst_sk + - ca_qst + - cd_nif + - ch_uid + - ch_vat + - cl_tin + - cm_niu + - cn_tin + - co_nit + - cr_tin + - cv_nif + - de_stn + - do_rcn + - ec_ruc + - eg_tin + - es_cif + - et_tin + - eu_oss_vat + - eu_vat + - gb_vat + - ge_vat + - gn_nif + - hk_br + - hr_oib + - hu_tin + - id_npwp + - il_vat + - in_gst + - is_vat + - jp_cn + - jp_rn + - jp_trn + - ke_pin + - kg_tin + - kh_tin + - kr_brn + - kz_bin + - la_tin + - li_uid + - li_vat + - ma_vat + - md_vat + - me_pib + - mk_vat + - mr_nif + - mx_rfc + - my_frp + - my_itn + - my_sst + - ng_tin + - no_vat + - no_voec + - np_pan + - nz_gst + - om_vat + - pe_ruc + - ph_tin + - ro_tin + - rs_pib + - ru_inn + - ru_kpp + - sa_vat + - sg_gst + - sg_uen + - si_tin + - sn_ninea + - sr_fin + - sv_nit + - th_vat + - tj_tin + - tr_tin + - tw_vat + - tz_vat + - ua_vat + - ug_tin + - us_ein + - uy_ruc + - uz_tin + - uz_vat + - ve_rif + - vn_tin + - za_vat + - zm_tin + - zw_tin + maxLength: 5000 + type: string + x-stripeBypassValidation: true + value: + type: string + required: + - type + - value + title: data_params + type: object + type: array + title: customer_details_param + type: object + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + description: >- + The coupons to redeem into discounts for the invoice + preview. If not specified, inherits the discount from the + subscription or customer. This works for both coupons + directly applied to an invoice and coupons applied to a + subscription. Pass an empty string to avoid inheriting any + discounts. + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + invoice_items: + description: >- + List of invoice items to add or update in the upcoming + invoice preview (up to 250). + items: + properties: + amount: + type: integer + currency: + format: currency + type: string + description: + maxLength: 5000 + type: string + discountable: + type: boolean + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + invoiceitem: + maxLength: 5000 + type: string + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + period: + properties: + end: + format: unix-time + type: integer + start: + format: unix-time + type: integer + required: + - end + - start + title: period + type: object + price: + maxLength: 5000 + type: string + price_data: + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + title: one_time_price_data + type: object + quantity: + type: integer + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + tax_code: + anyOf: + - type: string + - enum: + - '' + type: string + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + title: invoice_item_preview_params + type: object + type: array + issuer: + description: >- + The connected account that issues the invoice. The invoice + is presented with the branding and support information of + the specified account. + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + on_behalf_of: + anyOf: + - type: string + - enum: + - '' + type: string + description: >- + The account (if any) for which the funds of the invoice + payment are intended. If set, the invoice will be presented + with the branding and support information of the specified + account. See the [Invoices with + Connect](https://stripe.com/docs/billing/invoices/connect) + documentation for details. + preview_mode: + description: >- + Customizes the types of values to include when calculating + the invoice. Defaults to `next` if unspecified. + enum: + - next + - recurring + type: string + schedule: + description: >- + The identifier of the schedule whose upcoming invoice you'd + like to retrieve. Cannot be used with subscription or + subscription fields. + maxLength: 5000 + type: string + schedule_details: + description: >- + The schedule creation or modification params to apply as a + preview. Cannot be used with `subscription` or + `subscription_` prefixed fields. + properties: + end_behavior: + enum: + - cancel + - release + type: string + phases: + items: + properties: + add_invoice_items: + items: + properties: + discounts: + items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + price: + maxLength: 5000 + type: string + price_data: + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + title: one_time_price_data_with_negative_amounts + type: object + quantity: + type: integer + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + title: add_invoice_item_entry + type: object + type: array + application_fee_percent: + type: number + automatic_tax: + properties: + enabled: + type: boolean + liability: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + required: + - enabled + title: automatic_tax_config + type: object + billing_cycle_anchor: + enum: + - automatic + - phase_start + type: string + billing_thresholds: + anyOf: + - properties: + amount_gte: + type: integer + reset_billing_cycle_anchor: + type: boolean + title: billing_thresholds_param + type: object + - enum: + - '' + type: string + collection_method: + enum: + - charge_automatically + - send_invoice + type: string + default_payment_method: + maxLength: 5000 + type: string + default_tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + description: + anyOf: + - maxLength: 500 + type: string + - enum: + - '' + type: string + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + end_date: + anyOf: + - format: unix-time + type: integer + - enum: + - now + maxLength: 5000 + type: string + invoice_settings: + properties: + account_tax_ids: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + days_until_due: + type: integer + issuer: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + title: invoice_settings + type: object + items: + items: + properties: + billing_thresholds: + anyOf: + - properties: + usage_gte: + type: integer + required: + - usage_gte + title: item_billing_thresholds_param + type: object + - enum: + - '' + type: string + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + metadata: + additionalProperties: + type: string + type: object + price: + maxLength: 5000 + type: string + price_data: + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + recurring: + properties: + interval: + enum: + - day + - month + - week + - year + type: string + interval_count: + type: integer + required: + - interval + title: recurring_adhoc + type: object + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + - recurring + title: recurring_price_data + type: object + quantity: + type: integer + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + title: configuration_item_params + type: object + type: array + iterations: + type: integer + metadata: + additionalProperties: + type: string + type: object + on_behalf_of: + type: string + proration_behavior: + enum: + - always_invoice + - create_prorations + - none + type: string + start_date: + anyOf: + - format: unix-time + type: integer + - enum: + - now + maxLength: 5000 + type: string + transfer_data: + properties: + amount_percent: + type: number + destination: + type: string + required: + - destination + title: transfer_data_specs + type: object + trial: + type: boolean + trial_end: + anyOf: + - format: unix-time + type: integer + - enum: + - now + maxLength: 5000 + type: string + required: + - items + title: phase_configuration_params + type: object + type: array + proration_behavior: + enum: + - always_invoice + - create_prorations + - none + type: string + title: schedule_details_params + type: object + subscription: + description: >- + The identifier of the subscription for which you'd like to + retrieve the upcoming invoice. If not provided, but a + `subscription_details.items` is provided, you will preview + creating a subscription with those items. If neither + `subscription` nor `subscription_details.items` is provided, + you will retrieve the next upcoming invoice from among the + customer's subscriptions. + maxLength: 5000 + type: string + subscription_details: + description: >- + The subscription creation or modification params to apply as + a preview. Cannot be used with `schedule` or + `schedule_details` fields. + properties: + billing_cycle_anchor: + anyOf: + - enum: + - now + - unchanged + maxLength: 5000 + type: string + - format: unix-time + type: integer + cancel_at: + anyOf: + - format: unix-time + type: integer + - enum: + - '' + type: string + cancel_at_period_end: + type: boolean + cancel_now: + type: boolean + default_tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + items: + items: + properties: + billing_thresholds: + anyOf: + - properties: + usage_gte: + type: integer + required: + - usage_gte + title: item_billing_thresholds_param + type: object + - enum: + - '' + type: string + clear_usage: + type: boolean + deleted: + type: boolean + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + id: + maxLength: 5000 + type: string + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + price: + maxLength: 5000 + type: string + price_data: + properties: + currency: + format: currency + type: string + product: + maxLength: 5000 + type: string + recurring: + properties: + interval: + enum: + - day + - month + - week + - year + type: string + interval_count: + type: integer + required: + - interval + title: recurring_adhoc + type: object + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + unit_amount: + type: integer + unit_amount_decimal: + format: decimal + type: string + required: + - currency + - product + - recurring + title: recurring_price_data + type: object + quantity: + type: integer + tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + title: subscription_item_update_params + type: object + type: array + proration_behavior: + enum: + - always_invoice + - create_prorations + - none + type: string + proration_date: + format: unix-time + type: integer + resume_at: + enum: + - now + maxLength: 5000 + type: string + start_date: + format: unix-time + type: integer + trial_end: + anyOf: + - enum: + - now + maxLength: 5000 + type: string + - format: unix-time + type: integer + title: subscription_details_params + type: object + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoice' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Create a preview invoice + /v1/invoices/search: + get: + description: >- +

Search for invoices you’ve previously created using Stripe’s Search Query Language. + + Don’t use search in read-after-write flows where strict consistency is + necessary. Under normal operating + + conditions, data is searchable in less than a minute. Occasionally, + propagation of new or updated data can be up + + to an hour behind during outages. Search functionality is not available + to merchants in India.

+ operationId: GetInvoicesSearch + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - description: >- + A limit on the number of objects to be returned. Limit can range + between 1 and 100, and the default is 10. + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: >- + A cursor for pagination across multiple pages of results. Don't + include this parameter on the first call. Use the next_page value + returned in a previous response to request subsequent results. + in: query + name: page + required: false + schema: + maxLength: 5000 + type: string + style: form + - description: >- + The search query string. See [search query + language](https://stripe.com/docs/search#search-query-language) and + the list of supported [query fields for + invoices](https://stripe.com/docs/search#query-fields-for-invoices). + in: query + name: query + required: true + schema: + maxLength: 5000 + type: string + style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + items: + $ref: '#/components/schemas/invoice' + type: array + has_more: + type: boolean + next_page: + maxLength: 5000 + nullable: true + type: string + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. + enum: + - search_result + type: string + total_count: + description: >- + The total number of objects that match the query, only + accurate up to 10,000. + type: integer + url: + maxLength: 5000 + type: string + required: + - data + - has_more + - object + - url + title: SearchResult + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Search invoices + '/v1/invoices/{invoice}': + delete: + description: >- +

Permanently deletes a one-off invoice draft. This cannot be undone. + Attempts to delete invoices that are no longer in a draft state will + fail; once an invoice has been finalized or if an invoice is for a + subscription, it must be voided.

+ operationId: DeleteInvoicesInvoice + parameters: + - in: path + name: invoice + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/deleted_invoice' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Delete a draft invoice + get: + description:

Retrieves the invoice with the given ID.

+ operationId: GetInvoicesInvoice + parameters: + - description: Specifies which fields in the response should be expanded. + explode: true + in: query + name: expand + required: false + schema: + items: + maxLength: 5000 + type: string + type: array + style: deepObject + - in: path + name: invoice + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoice' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Retrieve an invoice + post: + description: >- +

Draft invoices are fully editable. Once an invoice is finalized, + + monetary values, as well as collection_method, become + uneditable.

+ + +

If you would like to stop the Stripe Billing engine from + automatically finalizing, reattempting payments on, + + sending reminders for, or automatically + reconciling invoices, pass + + auto_advance=false.

+ operationId: PostInvoicesInvoice + parameters: + - in: path + name: invoice + required: true + schema: + maxLength: 5000 + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + encoding: + account_tax_ids: + explode: true + style: deepObject + automatic_tax: + explode: true + style: deepObject + custom_fields: + explode: true + style: deepObject + default_source: + explode: true + style: deepObject + default_tax_rates: + explode: true + style: deepObject + discounts: + explode: true + style: deepObject + effective_at: + explode: true + style: deepObject + expand: + explode: true + style: deepObject + issuer: + explode: true + style: deepObject + metadata: + explode: true + style: deepObject + number: + explode: true + style: deepObject + on_behalf_of: + explode: true + style: deepObject + payment_settings: + explode: true + style: deepObject + rendering: + explode: true + style: deepObject + shipping_cost: + explode: true + style: deepObject + shipping_details: + explode: true + style: deepObject + transfer_data: + explode: true + style: deepObject + schema: + additionalProperties: false + properties: + account_tax_ids: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + description: >- + The account tax IDs associated with the invoice. Only + editable when the invoice is a draft. + application_fee_amount: + description: >- + A fee in cents (or local equivalent) that will be applied to + the invoice and transferred to the application owner's + Stripe account. The request must be made with an OAuth key + or the Stripe-Account header in order to take an application + fee. For more information, see the application fees + [documentation](https://stripe.com/docs/billing/invoices/connect#collecting-fees). + type: integer + auto_advance: + description: >- + Controls whether Stripe performs [automatic + collection](https://stripe.com/docs/invoicing/integration/automatic-advancement-collection) + of the invoice. + type: boolean + automatic_tax: + description: Settings for automatic tax lookup for this invoice. + properties: + enabled: + type: boolean + liability: + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + required: + - enabled + title: automatic_tax_param + type: object + automatically_finalizes_at: + description: >- + The time when this invoice should be scheduled to finalize. + The invoice will be finalized at this time if it is still in + draft state. To turn off automatic finalization, set + `auto_advance` to false. + format: unix-time + type: integer + collection_method: + description: >- + Either `charge_automatically` or `send_invoice`. This field + can be updated only on `draft` invoices. + enum: + - charge_automatically + - send_invoice + type: string + custom_fields: + anyOf: + - items: + properties: + name: + maxLength: 40 + type: string + value: + maxLength: 140 + type: string + required: + - name + - value + title: custom_field_params + type: object + type: array + - enum: + - '' + type: string + description: >- + A list of up to 4 custom fields to be displayed on the + invoice. If a value for `custom_fields` is specified, the + list specified will replace the existing custom field list + on this invoice. Pass an empty string to remove + previously-defined fields. + days_until_due: + description: >- + The number of days from which the invoice is created until + it is due. Only valid for invoices where + `collection_method=send_invoice`. This field can only be + updated on `draft` invoices. + type: integer + default_payment_method: + description: >- + ID of the default payment method for the invoice. It must + belong to the customer associated with the invoice. If not + set, defaults to the subscription's default payment method, + if any, or to the default payment method in the customer's + invoice settings. + maxLength: 5000 + type: string + default_source: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + description: >- + ID of the default payment source for the invoice. It must + belong to the customer associated with the invoice and be in + a chargeable state. If not set, defaults to the + subscription's default source, if any, or to the customer's + default source. + default_tax_rates: + anyOf: + - items: + maxLength: 5000 + type: string + type: array + - enum: + - '' + type: string + description: >- + The tax rates that will apply to any line item that does not + have `tax_rates` set. Pass an empty string to remove + previously-defined tax rates. + description: + description: >- + An arbitrary string attached to the object. Often useful for + displaying to users. Referenced as 'memo' in the Dashboard. + maxLength: 1500 + type: string + discounts: + anyOf: + - items: + properties: + coupon: + maxLength: 5000 + type: string + discount: + maxLength: 5000 + type: string + promotion_code: + maxLength: 5000 + type: string + title: discounts_data_param + type: object + type: array + - enum: + - '' + type: string + description: >- + The discounts that will apply to the invoice. Pass an empty + string to remove previously-defined discounts. + due_date: + description: >- + The date on which payment for this invoice is due. Only + valid for invoices where `collection_method=send_invoice`. + This field can only be updated on `draft` invoices. + format: unix-time + type: integer + effective_at: + anyOf: + - format: unix-time + type: integer + - enum: + - '' + type: string + description: >- + The date when this invoice is in effect. Same as + `finalized_at` unless overwritten. When defined, this value + replaces the system-generated 'Date of issue' printed on the + invoice PDF and receipt. + expand: + description: Specifies which fields in the response should be expanded. + items: + maxLength: 5000 + type: string + type: array + footer: + description: Footer to be displayed on the invoice. + maxLength: 5000 + type: string + issuer: + description: >- + The connected account that issues the invoice. The invoice + is presented with the branding and support information of + the specified account. + properties: + account: + type: string + type: + enum: + - account + - self + type: string + x-stripeBypassValidation: true + required: + - type + title: param + type: object + metadata: + anyOf: + - additionalProperties: + type: string + type: object + - enum: + - '' + type: string + description: >- + Set of [key-value + pairs](https://stripe.com/docs/api/metadata) that you can + attach to an object. This can be useful for storing + additional information about the object in a structured + format. Individual keys can be unset by posting an empty + value to them. All keys can be unset by posting an empty + value to `metadata`. + number: + anyOf: + - maxLength: 26 + type: string + - enum: + - '' + type: string + description: >- + Set the number for this invoice. If no number is present + then a number will be assigned automatically when the + invoice is finalized. In many markets, regulations require + invoices to be unique, sequential and / or gapless. You are + responsible for ensuring this is true across all your + different invoicing systems in the event that you edit the + invoice number using our API. If you use only Stripe for + your invoices and do not change invoice numbers, Stripe + handles this aspect of compliance for you automatically. + on_behalf_of: + anyOf: + - type: string + - enum: + - '' + type: string + description: >- + The account (if any) for which the funds of the invoice + payment are intended. If set, the invoice will be presented + with the branding and support information of the specified + account. See the [Invoices with + Connect](https://stripe.com/docs/billing/invoices/connect) + documentation for details. + payment_settings: + description: >- + Configuration settings for the PaymentIntent that is + generated when the invoice is finalized. + properties: + default_mandate: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + payment_method_options: + properties: + acss_debit: + anyOf: + - properties: + mandate_options: + properties: + transaction_type: + enum: + - business + - personal + type: string + title: mandate_options_param + type: object + verification_method: + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + bancontact: + anyOf: + - properties: + preferred_language: + enum: + - de + - en + - fr + - nl + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + card: + anyOf: + - properties: + installments: + properties: + enabled: + type: boolean + plan: + anyOf: + - properties: + count: + type: integer + interval: + enum: + - month + type: string + type: + enum: + - fixed_count + type: string + x-stripeBypassValidation: true + required: + - type + title: installment_plan + type: object + - enum: + - '' + type: string + title: installments_param + type: object + request_three_d_secure: + enum: + - any + - automatic + - challenge + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + customer_balance: + anyOf: + - properties: + bank_transfer: + properties: + eu_bank_transfer: + properties: + country: + maxLength: 5000 + type: string + required: + - country + title: eu_bank_transfer_param + type: object + type: + type: string + title: bank_transfer_param + type: object + funding_type: + type: string + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + konbini: + anyOf: + - properties: {} + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + sepa_debit: + anyOf: + - properties: {} + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + us_bank_account: + anyOf: + - properties: + financial_connections: + properties: + filters: + properties: + account_subcategories: + items: + enum: + - checking + - savings + type: string + type: array + title: >- + invoice_linked_account_options_filters_param + type: object + permissions: + items: + enum: + - balances + - ownership + - payment_method + - transactions + maxLength: 5000 + type: string + x-stripeBypassValidation: true + type: array + prefetch: + items: + enum: + - balances + - ownership + - transactions + type: string + x-stripeBypassValidation: true + type: array + title: invoice_linked_account_options_param + type: object + verification_method: + enum: + - automatic + - instant + - microdeposits + type: string + x-stripeBypassValidation: true + title: invoice_payment_method_options_param + type: object + - enum: + - '' + type: string + title: payment_method_options + type: object + payment_method_types: + anyOf: + - items: + enum: + - ach_credit_transfer + - ach_debit + - acss_debit + - affirm + - amazon_pay + - au_becs_debit + - bacs_debit + - bancontact + - boleto + - card + - cashapp + - customer_balance + - eps + - fpx + - giropay + - grabpay + - ideal + - jp_credit_transfer + - kakao_pay + - klarna + - konbini + - kr_card + - link + - multibanco + - naver_pay + - nz_bank_account + - p24 + - payco + - paynow + - paypal + - promptpay + - revolut_pay + - sepa_credit_transfer + - sepa_debit + - sofort + - swish + - us_bank_account + - wechat_pay + type: string + x-stripeBypassValidation: true + type: array + - enum: + - '' + type: string + title: payment_settings + type: object + rendering: + description: >- + The rendering-related settings that control how the invoice + is displayed on customer-facing surfaces such as PDF and + Hosted Invoice Page. + properties: + amount_tax_display: + enum: + - '' + - exclude_tax + - include_inclusive_tax + type: string + pdf: + properties: + page_size: + enum: + - a4 + - auto + - letter + type: string + title: rendering_pdf_param + type: object + template: + maxLength: 5000 + type: string + template_version: + anyOf: + - type: integer + - enum: + - '' + type: string + title: rendering_param + type: object + shipping_cost: + anyOf: + - properties: + shipping_rate: + maxLength: 5000 + type: string + shipping_rate_data: + properties: + delivery_estimate: + properties: + maximum: + properties: + unit: + enum: + - business_day + - day + - hour + - month + - week + type: string + value: + type: integer + required: + - unit + - value + title: delivery_estimate_bound + type: object + minimum: + properties: + unit: + enum: + - business_day + - day + - hour + - month + - week + type: string + value: + type: integer + required: + - unit + - value + title: delivery_estimate_bound + type: object + title: delivery_estimate + type: object + display_name: + maxLength: 100 + type: string + fixed_amount: + properties: + amount: + type: integer + currency: + format: currency + type: string + currency_options: + additionalProperties: + properties: + amount: + type: integer + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + required: + - amount + title: currency_option + type: object + type: object + required: + - amount + - currency + title: fixed_amount + type: object + metadata: + additionalProperties: + type: string + type: object + tax_behavior: + enum: + - exclusive + - inclusive + - unspecified + type: string + tax_code: + type: string + type: + enum: + - fixed_amount + type: string + required: + - display_name + title: method_params + type: object + title: shipping_cost + type: object + - enum: + - '' + type: string + description: Settings for the cost of shipping for this invoice. + shipping_details: + anyOf: + - properties: + address: + properties: + city: + maxLength: 5000 + type: string + country: + maxLength: 5000 + type: string + line1: + maxLength: 5000 + type: string + line2: + maxLength: 5000 + type: string + postal_code: + maxLength: 5000 + type: string + state: + maxLength: 5000 + type: string + title: optional_fields_address + type: object + name: + maxLength: 5000 + type: string + phone: + anyOf: + - maxLength: 5000 + type: string + - enum: + - '' + type: string + required: + - address + - name + title: recipient_shipping_with_optional_fields_address + type: object + - enum: + - '' + type: string + description: >- + Shipping details for the invoice. The Invoice PDF will use + the `shipping_details` value if it is set, otherwise the PDF + will render the shipping address from the customer. + statement_descriptor: + description: >- + Extra information about a charge for the customer's credit + card statement. It must contain at least one letter. If not + specified and this invoice is part of a subscription, the + default `statement_descriptor` will be set to the first + subscription item's product's `statement_descriptor`. + maxLength: 22 + type: string + transfer_data: + anyOf: + - properties: + amount: + type: integer + destination: + type: string + required: + - destination + title: transfer_data_specs + type: object + - enum: + - '' + type: string + description: >- + If specified, the funds from the invoice will be transferred + to the destination and the ID of the resulting transfer will + be found on the invoice's charge. This will be unset if you + POST an empty value. + type: object + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/invoice' + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: Update an invoice + /v1/products: + get: + description: >- +

Returns a list of your products. The products are returned sorted by + creation date, with the most recently created products appearing + first.

+ operationId: GetProducts + #parameters: + # - description: >- + # Only return products that are active or inactive (e.g., pass `false` + # to list all inactive products). + # in: query + # name: active + # required: false + # schema: + # type: boolean + # style: form + # - description: >- + # Only return products that were created during the given date + # interval. + # explode: true + # in: query + # name: created + # required: false + # schema: + # anyOf: + # - properties: + # gt: + # type: integer + # gte: + # type: integer + # lt: + # type: integer + # lte: + # type: integer + # title: range_query_specs + # type: object + # - type: integer + # style: deepObject + # - description: >- + # A cursor for use in pagination. `ending_before` is an object ID that + # defines your place in the list. For instance, if you make a list + # request and receive 100 objects, starting with `obj_bar`, your + # subsequent call can include `ending_before=obj_bar` in order to + # fetch the previous page of the list. + # in: query + # name: ending_before + # required: false + # schema: + # maxLength: 5000 + # type: string + # style: form + # - description: Specifies which fields in the response should be expanded. + # explode: true + # in: query + # name: expand + # required: false + # schema: + # items: + # maxLength: 5000 + # type: string + # type: array + # style: deepObject + # - description: >- + # Only return products with the given IDs. Cannot be used with + # [starting_after](https://stripe.com/docs/api#list_products-starting_after) + # or + # [ending_before](https://stripe.com/docs/api#list_products-ending_before). + # explode: true + # in: query + # name: ids + # required: false + # schema: + # items: + # maxLength: 5000 + # type: string + # type: array + # style: deepObject + # - description: >- + # A limit on the number of objects to be returned. Limit can range + # between 1 and 100, and the default is 10. + # in: query + # name: limit + # required: false + # schema: + # type: integer + # style: form + # - description: >- + # Only return products that can be shipped (i.e., physical, not + # digital products). + # in: query + # name: shippable + # required: false + # schema: + # type: boolean + # style: form + # - description: >- + # A cursor for use in pagination. `starting_after` is an object ID + # that defines your place in the list. For instance, if you make a + # list request and receive 100 objects, ending with `obj_foo`, your + # subsequent call can include `starting_after=obj_foo` in order to + # fetch the next page of the list. + # in: query + # name: starting_after + # required: false + # schema: + # maxLength: 5000 + # type: string + # style: form + # - description: Only return products with the given url. + # in: query + # name: url + # required: false + # schema: + # maxLength: 5000 + # type: string + # style: form + requestBody: + content: + application/x-www-form-urlencoded: + encoding: {} + schema: + additionalProperties: false + properties: {} + type: object + required: false + responses: + '200': + content: + application/json: + schema: + description: '' + properties: + data: + description: Details about each object. + items: + $ref: '#/components/schemas/product' + type: array + has_more: + description: >- + True if this list has another page of items after this one + that can be fetched. + type: boolean + object: + description: >- + String representing the object's type. Objects of the same + type share the same value. Always has the value `list`. + enum: + - list + type: string + url: + description: The URL where this list can be accessed. + maxLength: 5000 + pattern: ^/v1/products + type: string + required: + - data + - has_more + - object + - url + title: ProductList + type: object + x-expandableFields: + - data + description: Successful response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/error' + description: Error response. + summary: List all products +security: + - basicAuth: [] + - bearerAuth: [] +servers: + - url: 'https://api.stripe.com/' diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml new file mode 100644 index 000000000..62890c55a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/account-service-asyncapi.yaml @@ -0,0 +1,42 @@ +asyncapi: '2.1.0' +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups + x-microcks: + labels: + domain: authentication + status: GA + team: Team B +channels: + user/signedup: + bindings: + ws: + method: POST + subscribe: + x-microcks-operation: + delay: 30 + message: + $ref: '#/components/messages/UserSignedUp' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user + examples: + - name: laurent + payload: + displayName: Laurent Broudoux + email: laurent@microcks.io + - name: random + payload: + displayName: '{{randomFullName()}}' + email: '{{randomEmail()}}' \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-gh-master.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-gh-master.yaml new file mode 100644 index 000000000..d01d48d4e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-gh-master.yaml @@ -0,0 +1,181 @@ +--- +asyncapi: 2.0.0 +info: + x-application-class: unspecified + title: ApiEventService:maintenance + x-view: consumer + version: 0.0.3 +channels: + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}: + publish: + summary: subscribe to maintenance events from elevators + description: | + Maintenance events indicate recommended maintenance actions to be carried out within a certain timeframe. + Each maintenance event must be confirmed with a status_update message using the same tracking_id indicating action is either received or resolved. + operationId: onMaintenance + bindings: + mqtt: + bindingVersion: '0.1.0' + qos: 1 + http: + bindingVersion: '0.1.0' + type: request + method: POST + message: + $ref: "#/components/messages/maintenance" + parameters: + resource_region_id: + $ref: "#/components/parameters/resource_region_id" + equipmentType: + $ref: "#/components/parameters/equipmentType" + eventType: + $ref: "#/components/parameters/eventType" + resourceType: + $ref: "#/components/parameters/resourceType" + resourceId: + $ref: "#/components/parameters/resourceId" +components: + schemas: + maintenance: + type: object + title: Elevator Co maintenance event payload + required: + - header + - body + properties: + header: + additionalProperties: false + type: object + required: + - topic + - timestamp + - tracking_id + properties: + topic: + type: string + timestamp: + type: string + tracking_id: + type: string + pattern: "^[a-zA-Z0-9-]+$" + body: + additionalProperties: false + type: object + required: + - component_id + - description + - timewindow_days + - details + properties: + component_id: + title: component_id that needs maintenance + type: string + timewindow_days: + title: maintenance time window in days + type: integer + description: + title: maintenance description + type: string + details: + title: maintenance details + type: object + probability_of_failure_pct: + title: probability of fault / urgency of maintenace in % + type: integer + messages: + maintenance: + payload: + "$ref": "#/components/schemas/maintenance" + schemaFormat: application/vnd.aai.asyncapi+json;version=2.0.0 + contentType: application/json + examples: + - payload: + header: + topic: apim/elevator-co/api/V1/json/fr/elevator/maintenace/elev-make-1/elevator-id-1 + timestamp: 2021-02-15-13:25:27-UTC + tracking_id: 4fd44ed1-1ba9-4f53-87a2-d4f4385c09d5 + body: + component_id: ABC-1784 + description: misalignment car to floor + timewindow_days: 24 + probability_of_failure_pct: 90 + details: + reasons: + - misalignment car to floor > 8mm + - avg journeys / day > 25 + - payload: + header: + topic: apim/elevator-co/api/V1/json/fr/elevator/maintenace/elev-make-1/elevator-id-1 + timestamp: 2021-03-15-16:25:27-UTC + tracking_id: 1fd11ed1-1ba1-4f53-87a2-d4f4385c11d1 + body: + component_id: XYZ-1958 + description: head stuck in door + timewindow_days: 5 + probability_of_failure_pct: 99 + details: + reasons: + - head stuck in door + - instructions unclear + parameters: + resource_region_id: + schema: + type: string + enum: + - fr + - de + examples: + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-0: + value: fr + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-1: + value: de + equipmentType: + schema: + type: string + enum: + - elevator + examples: + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-0: + value: elevator + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-1: + value: elevator + eventType: + schema: + type: string + enum: + - maintenace + examples: + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-0: + value: maintenance + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-1: + value: maintenance + resourceType: + schema: + type: string + enum: + - elev-make-1 + - elev-make-2 + examples: + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-0: + value: elev-make-1 + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-1: + value: elev-make-2 + resourceId: + schema: + type: string + pattern: "^[a-zA-Z0-9-]+$" + examples: + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-0: + value: abc4711 + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}-1: + value: xyz0815 + securitySchemes: + userPassword: + type: userPassword + description: Username Password + httpBasic: + type: http + description: HTTP Basic + scheme: basic + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-ws-kv.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-ws-kv.yaml new file mode 100644 index 000000000..865998b55 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-ws-kv.yaml @@ -0,0 +1,170 @@ +--- +asyncapi: 2.0.0 +info: + x-application-class: unspecified + title: ApiEventService:maintenance + x-view: consumer + version: 0.0.3 +channels: + elevator-co/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}: + bindings: + ws: + method: POST + publish: + summary: subscribe to maintenance events from elevators + description: | + Maintenance events indicate recommended maintenance actions to be carried out within a certain timeframe. + Each maintenance event must be confirmed with a status_update message using the same tracking_id indicating action is either received or resolved. + operationId: onMaintenance + message: + $ref: "#/components/messages/maintenance" + parameters: + resource_region_id: + $ref: "#/components/parameters/resource_region_id" + equipmentType: + $ref: "#/components/parameters/equipmentType" + eventType: + $ref: "#/components/parameters/eventType" + resourceType: + $ref: "#/components/parameters/resourceType" + resourceId: + $ref: "#/components/parameters/resourceId" +components: + schemas: + maintenance: + type: object + title: Elevator Co maintenance event payload + required: + - header + - body + properties: + header: + additionalProperties: false + type: object + required: + - topic + - timestamp + - tracking_id + properties: + topic: + type: string + timestamp: + type: string + tracking_id: + type: string + pattern: "^[a-zA-Z0-9-]+$" + body: + additionalProperties: false + type: object + required: + - component_id + - description + - timewindow_days + - details + properties: + component_id: + title: component_id that needs maintenance + type: string + timewindow_days: + title: maintenance time window in days + type: integer + description: + title: maintenance description + type: string + details: + title: maintenance details + type: object + probability_of_failure_pct: + title: probability of fault / urgency of maintenace in % + type: integer + messages: + maintenance: + payload: + "$ref": "#/components/schemas/maintenance" + schemaFormat: application/vnd.aai.asyncapi+json;version=2.0.0 + contentType: application/json + examples: + - misalignment: + payload: + header: + topic: apim/elevator-co/api/V1/json/fr/elevator/maintenace/elev-make-1/elevator-id-1 + timestamp: 2021-02-15-13:25:27-UTC + tracking_id: 4fd44ed1-1ba9-4f53-87a2-d4f4385c09d5 + body: + component_id: ABC-1784 + description: misalignment car to floor + timewindow_days: 24 + probability_of_failure_pct: 90 + details: + reasons: + - misalignment car to floor > 8mm + - avg journeys / day > 25 + - doorfailure: + payload: + header: + topic: apim/elevator-co/api/V1/json/fr/elevator/maintenace/elev-make-1/elevator-id-1 + timestamp: 2021-03-15-16:25:27-UTC + tracking_id: 1fd11ed1-1ba1-4f53-87a2-d4f4385c11d1 + body: + component_id: XYZ-1958 + description: head stuck in door + timewindow_days: 5 + probability_of_failure_pct: 99 + details: + reasons: + - head stuck in door + - instructions unclear + parameters: + resource_region_id: + schema: + type: string + enum: + - fr + - de + examples: + - misalignment: + value: fr + - doorfailure: + value: de + equipmentType: + schema: + type: string + enum: + - elevator + examples: + - 'misalignment:elevator' + - 'doorfailure:elevator' + eventType: + schema: + type: string + enum: + - maintenace + examples: + - 'misalignment:maintenance' + - 'doorfailure:maintenance' + resourceType: + schema: + type: string + enum: + - elev-make-1 + - elev-make-2 + examples: + - 'misalignment:elev-make-1' + - 'doorfailure:elev-make-2' + resourceId: + schema: + type: string + pattern: "^[a-zA-Z0-9-]+$" + examples: + - 'misalignment:abc4711' + - 'doorfailure:xyz0815' + securitySchemes: + userPassword: + type: userPassword + description: Username Password + httpBasic: + type: http + description: HTTP Basic + scheme: basic + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-ws.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-ws.yaml new file mode 100644 index 000000000..63e0f7e31 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec-ws.yaml @@ -0,0 +1,178 @@ +--- +asyncapi: 2.0.0 +info: + x-application-class: unspecified + title: ApiEventService:maintenance + x-view: consumer + version: 0.0.3 +channels: + elevator-co/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}: + bindings: + ws: + method: POST + publish: + summary: subscribe to maintenance events from elevators + description: | + Maintenance events indicate recommended maintenance actions to be carried out within a certain timeframe. + Each maintenance event must be confirmed with a status_update message using the same tracking_id indicating action is either received or resolved. + operationId: onMaintenance + message: + $ref: "#/components/messages/maintenance" + parameters: + resource_region_id: + $ref: "#/components/parameters/resource_region_id" + equipmentType: + $ref: "#/components/parameters/equipmentType" + eventType: + $ref: "#/components/parameters/eventType" + resourceType: + $ref: "#/components/parameters/resourceType" + resourceId: + $ref: "#/components/parameters/resourceId" +components: + schemas: + maintenance: + type: object + title: Elevator Co maintenance event payload + required: + - header + - body + properties: + header: + additionalProperties: false + type: object + required: + - topic + - timestamp + - tracking_id + properties: + topic: + type: string + timestamp: + type: string + tracking_id: + type: string + pattern: "^[a-zA-Z0-9-]+$" + body: + additionalProperties: false + type: object + required: + - component_id + - description + - timewindow_days + - details + properties: + component_id: + title: component_id that needs maintenance + type: string + timewindow_days: + title: maintenance time window in days + type: integer + description: + title: maintenance description + type: string + details: + title: maintenance details + type: object + probability_of_failure_pct: + title: probability of fault / urgency of maintenace in % + type: integer + messages: + maintenance: + payload: + "$ref": "#/components/schemas/maintenance" + schemaFormat: application/vnd.aai.asyncapi+json;version=2.0.0 + contentType: application/json + examples: + - misalignment: + payload: + header: + topic: apim/elevator-co/api/V1/json/fr/elevator/maintenace/elev-make-1/elevator-id-1 + timestamp: 2021-02-15-13:25:27-UTC + tracking_id: 4fd44ed1-1ba9-4f53-87a2-d4f4385c09d5 + body: + component_id: ABC-1784 + description: misalignment car to floor + timewindow_days: 24 + probability_of_failure_pct: 90 + details: + reasons: + - misalignment car to floor > 8mm + - avg journeys / day > 25 + - doorfailure: + payload: + header: + topic: apim/elevator-co/api/V1/json/fr/elevator/maintenace/elev-make-1/elevator-id-1 + timestamp: 2021-03-15-16:25:27-UTC + tracking_id: 1fd11ed1-1ba1-4f53-87a2-d4f4385c11d1 + body: + component_id: XYZ-1958 + description: head stuck in door + timewindow_days: 5 + probability_of_failure_pct: 99 + details: + reasons: + - head stuck in door + - instructions unclear + parameters: + resource_region_id: + schema: + type: string + enum: + - fr + - de + examples: + - misalignment: + value: fr + - doorfailure: + value: de + equipmentType: + schema: + type: string + enum: + - elevator + examples: + - misalignment: + value: elevator + - doorfailure: + value: elevator + eventType: + schema: + type: string + enum: + - maintenace + examples: + - misalignment: + value: maintenance + - doorfailure: + value: maintenance + resourceType: + schema: + type: string + enum: + - elev-make-1 + - elev-make-2 + examples: + - misalignment: + value: elev-make-1 + - doorfailure: + value: elev-make-2 + resourceId: + schema: + type: string + pattern: "^[a-zA-Z0-9-]+$" + examples: + - misalignment: + value: abc4711 + - doorfailure: + value: xyz0815 + securitySchemes: + userPassword: + type: userPassword + description: Username Password + httpBasic: + type: http + description: HTTP Basic + scheme: basic + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec.yaml new file mode 100644 index 000000000..40ad19ec9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/api-maintenance.async-api-spec.yaml @@ -0,0 +1,183 @@ +--- +asyncapi: 2.0.0 +info: + x-application-class: unspecified + title: ApiEventService:maintenance + x-view: consumer + version: 0.0.3 +channels: + apim/elevator-co/api/V1/json/{resource_region_id}/{equipmentType}/{eventType}/{resourceType}/{resourceId}: + publish: + summary: subscribe to maintenance events from elevators + description: | + Maintenance events indicate recommended maintenance actions to be carried out within a certain timeframe. + Each maintenance event must be confirmed with a status_update message using the same tracking_id indicating action is either received or resolved. + operationId: onMaintenance + bindings: + mqtt: + bindingVersion: '0.1.0' + qos: 1 + http: + bindingVersion: '0.1.0' + type: request + method: POST + message: + $ref: "#/components/messages/maintenance" + parameters: + resource_region_id: + $ref: "#/components/parameters/resource_region_id" + equipmentType: + $ref: "#/components/parameters/equipmentType" + eventType: + $ref: "#/components/parameters/eventType" + resourceType: + $ref: "#/components/parameters/resourceType" + resourceId: + $ref: "#/components/parameters/resourceId" +components: + schemas: + maintenance: + type: object + title: Elevator Co maintenance event payload + required: + - header + - body + properties: + header: + additionalProperties: false + type: object + required: + - topic + - timestamp + - tracking_id + properties: + topic: + type: string + timestamp: + type: string + tracking_id: + type: string + pattern: "^[a-zA-Z0-9-]+$" + body: + additionalProperties: false + type: object + required: + - component_id + - description + - timewindow_days + - details + properties: + component_id: + title: component_id that needs maintenance + type: string + timewindow_days: + title: maintenance time window in days + type: integer + description: + title: maintenance description + type: string + details: + title: maintenance details + type: object + probability_of_failure_pct: + title: probability of fault / urgency of maintenace in % + type: integer + messages: + maintenance: + payload: + "$ref": "#/components/schemas/maintenance" + schemaFormat: application/vnd.aai.asyncapi+json;version=2.0.0 + contentType: application/json + examples: + - misalignment: + payload: + header: + topic: apim/elevator-co/api/V1/json/fr/elevator/maintenace/elev-make-1/elevator-id-1 + timestamp: 2021-02-15-13:25:27-UTC + tracking_id: 4fd44ed1-1ba9-4f53-87a2-d4f4385c09d5 + body: + component_id: ABC-1784 + description: misalignment car to floor + timewindow_days: 24 + probability_of_failure_pct: 90 + details: + reasons: + - misalignment car to floor > 8mm + - avg journeys / day > 25 + - doorfailure: + payload: + header: + topic: apim/elevator-co/api/V1/json/fr/elevator/maintenace/elev-make-1/elevator-id-1 + timestamp: 2021-03-15-16:25:27-UTC + tracking_id: 1fd11ed1-1ba1-4f53-87a2-d4f4385c11d1 + body: + component_id: XYZ-1958 + description: head stuck in door + timewindow_days: 5 + probability_of_failure_pct: 99 + details: + reasons: + - head stuck in door + - instructions unclear + parameters: + resource_region_id: + schema: + type: string + enum: + - fr + - de + examples: + misalignment: + value: fr + doorfailure: + value: de + equipmentType: + schema: + type: string + enum: + - elevator + examples: + misalignment: + value: elevator + doorfailure: + value: elevator + eventType: + schema: + type: string + enum: + - maintenace + examples: + misalignment: + value: maintenance + doorfailure: + value: maintenance + resourceType: + schema: + type: string + enum: + - elev-make-1 + - elev-make-2 + examples: + misalignment: + value: elev-make-1 + doorfailure: + value: elev-make-2 + resourceId: + schema: + type: string + pattern: "^[a-zA-Z0-9-]+$" + examples: + misalignment: + value: abc4711 + doorfailure: + value: xyz0815 + securitySchemes: + userPassword: + type: userPassword + description: Username Password + httpBasic: + type: http + description: HTTP Basic + scheme: basic + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi-3.0-dynamic.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi-3.0-dynamic.yaml new file mode 100644 index 000000000..e30e96678 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi-3.0-dynamic.yaml @@ -0,0 +1,224 @@ +asyncapi: 3.0.0 +info: + title: Streetlights Kafka API + version: 1.0.0 + description: |- + The Smartylighting Streetlights API allows you to remotely manage the city + lights. + ### Check out its awesome features: + + * Turn a specific streetlight on/off 🌃 + * Dim a specific streetlight 😎 + * Receive real-time information about environmental lighting conditions 📈 + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +defaultContentType: application/json +servers: + scram-connections: + host: test.mykafkacluster.org:18092 + protocol: kafka-secure + description: Test broker secured with scramSha256 + security: + - $ref: '#/components/securitySchemes/saslScram' + tags: + - name: env:test-scram + description: >- + This environment is meant for running internal tests through + scramSha256 + - name: kind:remote + description: This server is a remote server. Not exposed by the application + - name: visibility:private + description: This resource is private and only available to certain users + mtls-connections: + host: test.mykafkacluster.org:28092 + protocol: kafka-secure + description: Test broker secured with X509 + security: + - $ref: '#/components/securitySchemes/certs' + tags: + - name: env:test-mtls + description: This environment is meant for running internal tests through mtls + - name: kind:remote + description: This server is a remote server. Not exposed by the application + - name: visibility:private + description: This resource is private and only available to certain users +channels: + lightingMeasured: + address: smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOn: + address: smartylighting.streetlights.1.0.action.{streetlightId}.turn.on + messages: + turnOn: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOff: + address: smartylighting.streetlights.1.0.action.{streetlightId}.turn.off + messages: + turnOff: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightsDim: + address: smartylighting.streetlights.1.0.action.{streetlightId}.dim + messages: + dimLight: + $ref: '#/components/messages/dimLight' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' + turnOn: + action: send + channel: + $ref: '#/channels/lightTurnOn' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOn/messages/turnOn' + turnOff: + action: send + channel: + $ref: '#/channels/lightTurnOff' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOff/messages/turnOff' + dimLight: + action: send + channel: + $ref: '#/channels/lightsDim' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightsDim/messages/dimLight' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + contentType: application/json + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + examples: + - name: Sample + headers: + my-app-header: 45 + payload: + streetlightId: da059782-3ad0-4e45-88ce-ef3392bc7797 + lumens: 100 + sentAt: "2024-02-06T08:03:38Z" + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/turnOnOffPayload' + dimLight: + name: dimLight + title: Dim light + summary: Command a particular streetlight to dim the lights. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/dimLightPayload' + schemas: + lightMeasuredPayload: + type: object + properties: + streetlightId: + type: string + description: identifier of the light measured. + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + $ref: '#/components/schemas/sentAt' + turnOnOffPayload: + type: object + properties: + streetlightId: + type: string + description: identifier of the light to turn on or off. + command: + type: string + enum: + - 'on' + - 'off' + description: Whether to turn on or off the light. + sentAt: + $ref: '#/components/schemas/sentAt' + dimLightPayload: + type: object + properties: + streetlightId: + type: string + description: identifier of the light to dim. + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + minimum: 0 + maximum: 100 + sentAt: + $ref: '#/components/schemas/sentAt' + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + securitySchemes: + saslScram: + type: scramSha256 + description: Provide your username and password for SASL/SCRAM authentication + certs: + type: X509 + description: Download the certificate files from service provider + parameters: + streetlightId: + description: The ID of the streetlight. + location: $message.payload#/streetlightId + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + operationTraits: + kafka: + bindings: + kafka: + clientId: + type: string + enum: + - my-app-id diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi-3.0-static.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi-3.0-static.yaml new file mode 100644 index 000000000..358aabfff --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi-3.0-static.yaml @@ -0,0 +1,215 @@ +asyncapi: 3.0.0 +info: + title: Streetlights Kafka API + version: 1.0.0 + description: |- + The Smartylighting Streetlights API allows you to remotely manage the city + lights. + ### Check out its awesome features: + + * Turn a specific streetlight on/off 🌃 + * Dim a specific streetlight 😎 + * Receive real-time information about environmental lighting conditions 📈 + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +defaultContentType: application/json +servers: + scram-connections: + host: test.mykafkacluster.org:18092 + protocol: kafka-secure + description: Test broker secured with scramSha256 + security: + - $ref: '#/components/securitySchemes/saslScram' + tags: + - name: env:test-scram + description: >- + This environment is meant for running internal tests through + scramSha256 + - name: kind:remote + description: This server is a remote server. Not exposed by the application + - name: visibility:private + description: This resource is private and only available to certain users + mtls-connections: + host: test.mykafkacluster.org:28092 + protocol: kafka-secure + description: Test broker secured with X509 + security: + - $ref: '#/components/securitySchemes/certs' + tags: + - name: env:test-mtls + description: This environment is meant for running internal tests through mtls + - name: kind:remote + description: This server is a remote server. Not exposed by the application + - name: visibility:private + description: This resource is private and only available to certain users +channels: + lightingMeasured: + address: smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOn: + address: smartylighting.streetlights.1.0.action.{streetlightId}.turn.on + messages: + turnOn: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOff: + address: smartylighting.streetlights.1.0.action.{streetlightId}.turn.off + messages: + turnOff: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightsDim: + address: smartylighting.streetlights.1.0.action.{streetlightId}.dim + messages: + dimLight: + $ref: '#/components/messages/dimLight' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' + turnOn: + action: send + channel: + $ref: '#/channels/lightTurnOn' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOn/messages/turnOn' + turnOff: + action: send + channel: + $ref: '#/channels/lightTurnOff' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOff/messages/turnOff' + dimLight: + action: send + channel: + $ref: '#/channels/lightsDim' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightsDim/messages/dimLight' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + contentType: application/json + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + examples: + - name: Sample + headers: + my-app-header: 45 + payload: + lumens: 100 + sentAt: "2024-02-06T08:03:38Z" + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/turnOnOffPayload' + dimLight: + name: dimLight + title: Dim light + summary: Command a particular streetlight to dim the lights. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/dimLightPayload' + schemas: + lightMeasuredPayload: + type: object + properties: + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + $ref: '#/components/schemas/sentAt' + turnOnOffPayload: + type: object + properties: + command: + type: string + enum: + - 'on' + - 'off' + description: Whether to turn on or off the light. + sentAt: + $ref: '#/components/schemas/sentAt' + dimLightPayload: + type: object + properties: + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + minimum: 0 + maximum: 100 + sentAt: + $ref: '#/components/schemas/sentAt' + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + securitySchemes: + saslScram: + type: scramSha256 + description: Provide your username and password for SASL/SCRAM authentication + certs: + type: X509 + description: Download the certificate files from service provider + parameters: + streetlightId: + description: The ID of the streetlight. + examples: + - Sample:123 + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + operationTraits: + kafka: + bindings: + kafka: + clientId: + type: string + enum: + - my-app-id diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml new file mode 100644 index 000000000..665ffa5f8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/streetlights-asyncapi.yaml @@ -0,0 +1,89 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.streetlights' +info: + title: Streetlights API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + smartylighting/streetlights/event/lighting/measured: + description: The topic on which measure values may be consumed + subscribe: + summary: Receive informations about streelights measures + operationId: receiveStreelightsMeasures + bindings: + mqtt: + qos: 0 + retain: false + message: + $ref: '#/components/messages/lightMeasured' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: Inform about environmental lighting conditions of a particular streetlight. + contentType: application/json + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + examples: + - dev0: + summary: Example for Device 0 + headers: |- + {"my-app-header": 14} + payload: |- + {"streetlightId":"dev0", "lumens":1000, "sentAt":"{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}"} + - dev1: + summary: Example for Device 1 + headers: + my-app-header: 14 + payload: + streetlightId: dev1 + lumens: 1100 + sentAt: "{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}" + - dev2: + summary: Example for Device 2 + headers: + my-app-header: 14 + payload: + streetlightId: dev2 + lumens: 1200 + sentAt: "{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}" + schemas: + lightMeasuredPayload: + type: object + properties: + streetlightId: + type: string + description: The ID of the streetlight. + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + additionalProperties: false + required: + - streetlightId + - lumens + - sentAt + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-events-asyncapi-2.1.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-events-asyncapi-2.1.yaml new file mode 100644 index 000000000..932fdc04b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-events-asyncapi-2.1.yaml @@ -0,0 +1,58 @@ +asyncapi: '2.1.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User events API + version: 0.1.0 + description: Sample AsyncAPI for user related events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/events: + subscribe: + message: + anyOf: + - $ref: '#/components/messages/UserSignedUp' + - $ref: '#/components/messages/UserResign' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user + examples: + - name: laurent + payload: + displayName: Laurent Broudoux + email: laurent@microcks.io + - name: random + payload: + displayName: '{{randomFullName()}}' + email: '{{randomEmail()}}' + UserResign: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + reason: + type: string + description: Reason of resignation + examples: + - name: john + payload: + displayName: John Doe + resignation: Employee leaves \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-2.1.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-2.1.yaml new file mode 100644 index 000000000..1510b1510 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-2.1.yaml @@ -0,0 +1,78 @@ +asyncapi: '2.1.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: Laurent + headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - name: John + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + - laurent: + my-app-header: 21 + - yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-absolute-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-absolute-ref.yaml new file mode 100644 index 000000000..85f1649bd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-absolute-ref.yaml @@ -0,0 +1,66 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.3.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent@microcks.io + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user-signedup: + description: The topic on which user signed up events may be consumed + messages: + userSignedUp: + $ref: '#/components/messages/userSignedUp' +operations: + publishUserSignedUps: + action: 'send' + channel: + $ref: '#/channels/user-signedup' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/user-signedup/messages/userSignedUp' +components: + messages: + userSignedUp: + description: An event describing that a user just signed up + contentType: avro/binary + schemaFormat: application/vnd.apache.avro+json;version=1.9.0 + traits: + - $ref: './user-signedup-commons.yaml#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + schemaFormat: "application/vnd.apache.avro+json;version=1.9.0" + schema: + $ref: "https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.avsc#/User" + examples: + - name: laurent + summary: Example for Laurent user + headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: + fullName: Laurent Broudoux + email: "laurent@microcks.io" + age: 41 + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-ref.yaml new file mode 100644 index 000000000..741ccf442 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-avro-ref.yaml @@ -0,0 +1,66 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.3.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent@microcks.io + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user-signedup: + description: The topic on which user signed up events may be consumed + messages: + userSignedUp: + $ref: '#/components/messages/userSignedUp' +operations: + publishUserSignedUps: + action: 'send' + channel: + $ref: '#/channels/user-signedup' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/user-signedup/messages/userSignedUp' +components: + messages: + userSignedUp: + description: An event describing that a user just signed up + contentType: avro/binary + schemaFormat: application/vnd.apache.avro+json;version=1.9.0 + traits: + - $ref: './user-signedup-commons.yaml#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + schemaFormat: "application/vnd.apache.avro+json;version=1.9.0" + schema: + $ref: "./user-signedup.avsc#/User" + examples: + - name: laurent + summary: Example for Laurent user + headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: + fullName: Laurent Broudoux + email: "laurent@microcks.io" + age: 41 + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-nameless.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-nameless.yaml new file mode 100644 index 000000000..aab8919ab --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-nameless.yaml @@ -0,0 +1,79 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.3.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent@microcks.io + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user-signedup: + description: The topic on which user signed up events may be consumed + messages: + userSignedUp: + description: An event describing that a user just signed up + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: + fullName: Laurent Broudoux + email: "laurent@microcks.io" + age: 41 + - headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +operations: + publishUserSignedUps: + action: 'send' + channel: + $ref: '#/channels/user-signedup' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/user-signedup/messages/userSignedUp' +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + - laurent: + my-app-header: 21 + - yacine: + my-app-header: 22 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-ref.yaml new file mode 100644 index 000000000..7a030a1bb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0-ref.yaml @@ -0,0 +1,67 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.3.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent@microcks.io + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user-signedup: + description: The topic on which user signed up events may be consumed + messages: + userSignedUp: + $ref: '#/components/messages/userSignedUp' + bindings: + ws: + method: POST +operations: + publishUserSignedUps: + action: 'send' + channel: + $ref: '#/channels/user-signedup' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/user-signedup/messages/userSignedUp' + x-microcks-operation: + frequency: 30 +components: + messages: + userSignedUp: + description: An event describing that a user just signed up + traits: + - $ref: './user-signedup-commons.yaml#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + $ref: './user-signedup-schemas.yaml#/components/schemas/UserInfo' + examples: + - name: laurent + summary: Example for Laurent user + headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: + fullName: Laurent Broudoux + email: "laurent@microcks.io" + age: 41 + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml new file mode 100644 index 000000000..bc9005754 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-3.0.yaml @@ -0,0 +1,86 @@ +asyncapi: 3.0.0 +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.3.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent@microcks.io + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user-signedup: + description: The topic on which user signed up events may be consumed + messages: + userSignedUp: + $ref: '#/components/messages/userSignedUp' +operations: + publishUserSignedUps: + action: 'send' + channel: + $ref: '#/channels/user-signedup' + summary: Receive information about user signed up + messages: + - $ref: '#/channels/user-signedup/messages/userSignedUp' +components: + messages: + userSignedUp: + description: An event describing that a user just signed up + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - name: laurent + summary: Example for Laurent user + headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: + fullName: Laurent Broudoux + email: "laurent@microcks.io" + age: 41 + - name: john + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + - laurent: + my-app-header: 21 + - yacine: + my-app-header: 22 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-gh-master.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-gh-master.yaml new file mode 100644 index 000000000..9c47dda2b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-gh-master.yaml @@ -0,0 +1,76 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - headers: + my-app-header: 23 + sentAt: "2020-03-11T08:03:28Z" + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + - laurent: + my-app-header: 21 + - yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-oneliner.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-oneliner.json new file mode 100644 index 000000000..41a0f8cd9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-oneliner.json @@ -0,0 +1 @@ +{"asyncapi":"2.0.0","id":"urn:io.microcks.example.user-signedup","info":{"title":"User signed-up API","version":"0.1.1","description":"Sample AsyncAPI for user signedup events"},"defaultContentType":"application/json","channels":{"user/signedup":{"description":"The topic on which user signed up events may be consumed","subscribe":{"summary":"Receive informations about user signed up","operationId":"receivedUserSignedUp","message":{"description":"An event describing that a user just signed up.","traits":[{"$ref":"#/components/messageTraits/commonHeaders"}],"payload":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string"},"sendAt":{"type":"string"},"fullName":{"type":"string"},"email":{"type":"string","format":"email"},"age":{"type":"integer","minimum":18}}},"examples":[{"laurent":{"summary":"Example for Laurent user","headers":"{\"my-app-header\": 23}","payload":"{\"id\": \"{{randomString(32)}}\", \"sendAt\": \"{{now()}}\", \"fullName\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}"}},{"john":{"summary":"Example for John Doe user","headers":{"my-app-header":24},"payload":{"id":"{{randomString(32)}}","sendAt":"{{now()}}","fullName":"John Doe","email":"john@microcks.io","age":36}}}]}}}},"components":{"messageTraits":{"commonHeaders":{"headers":{"type":"object","properties":{"my-app-header":{"type":"integer","minimum":0,"maximum":100}}}}}}} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-ws.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-ws.yaml new file mode 100644 index 000000000..d34858b2f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi-ws.yaml @@ -0,0 +1,72 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up WebSocket API + version: 0.1.9 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + bindings: + ws: + method: POST + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + type: object + additionalProperties: false + properties: + uid: + type: string + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + sentAt: + type: string + format: date-time + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23} + payload: |- + {"uid": "{{uuid()}}", "fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 42, "sentAt": "{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}"} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + + payload: + uid: "{{uuid()}}" + fullName: John Doe + email: john@microcks.io + age: 36 + sentAt: "{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}" +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.json new file mode 100644 index 000000000..b0d0c9e96 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.json @@ -0,0 +1,91 @@ +{ + "asyncapi": "2.0.0", + "id": "urn:io.microcks.example.user-signedup", + "info": { + "title": "User signed-up API", + "version": "0.1.0", + "description": "Sample AsyncAPI for user signedup events" + }, + "defaultContentType": "application/json", + "channels": { + "user/signedup": { + "description": "The topic on which user signed up events may be consumed", + "subscribe": { + "summary": "Receive informations about user signed up", + "operationId": "receivedUserSIgnedUp", + "message": { + "description": "An event describing that a user just signed up.", + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ], + "payload": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "sendAt": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer", + "minimum": 18 + } + } + }, + "examples": [ + { + "laurent": { + "summary": "Example for Laurent user", + "headers": "{\"my-app-header\": 23, \"sentAt\": \"2020-03-11T08:03:28Z\"}", + "payload": "{\"id\": \"{{randomString(32)}}\", \"sendAt\": \"{{now()}}\", \"fullName\": \"Laurent Broudoux\", \"email\": \"laurent@microcks.io\", \"age\": 41}" + } + }, + { + "john": { + "summary": "Example for John Doe user", + "headers": { + "my-app-header": 24, + "sentAt": "2020-03-11T08:03:38Z" + }, + "payload": { + "id": "{{randomString(32)}}", + "sendAt": "{{now()}}", + "fullName": "John Doe", + "email": "john@microcks.io", + "age": 36 + } + } + } + ] + } + } + } + }, + "components": { + "messageTraits": { + "commonHeaders": { + "headers": { + "type": "object", + "properties": { + "my-app-header": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml new file mode 100644 index 000000000..41688f759 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-asyncapi.yaml @@ -0,0 +1,79 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml new file mode 100644 index 000000000..2c0579de3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-asyncapi.yaml @@ -0,0 +1,79 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up Avro API + version: 0.1.1 + description: Sample AsyncAPI for user signedup events defined using Avro + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + contentType: avro/binary + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + doc: User information + fields: + - name: fullName + type: string + - name: email + type: string + - name: age + type: int + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml new file mode 100644 index 000000000..8aeb3f30c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-avro-ref-asyncapi.yaml @@ -0,0 +1,71 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up Avro API + version: 0.1.2 + description: Sample AsyncAPI for user signedup events defined using Avro + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + contentType: avro/binary + schemaFormat: application/vnd.apache.avro+json;version=1.9.0 + payload: + $ref: './user-signedup.avsc#/User' + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-cloudevents-binary.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-cloudevents-binary.yaml new file mode 100644 index 000000000..7e040f0d9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-cloudevents-binary.yaml @@ -0,0 +1,70 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up CloudEvents API + version: 0.1.3 + description: Sample AsyncAPI for user signedup events defined using CloudEvents binary + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: 'https://raw.githubusercontent.com/microcks/microcks-quickstarters/main/cloud/cloudevents/cloudevents-v1.0.1-asyncapi-trait.yml' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + contentType: avro/binary + schemaFormat: application/vnd.apache.avro+json;version=1.9.0 + payload: + $ref: './user-signedup.avsc#/User' + examples: + - laurent: + summary: Example for Laurent user + headers: |- + { + "ce_specversion": "1.0", + "ce_type": "com.example.someevent", + "ce_source": "/mycontext/subcontext", + "ce_id": "1234-1234-1234", + "ce_time": "2020-03-11T08:03:28Z", + "content-type": "application/avro", + "sentAt": "2020-03-11T08:03:28Z" + } + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + ce_specversion: "1.0" + ce_type: "io.microcks.example.user-signedup" + ce_source: "/mycontext/subcontext" + ce_id: "{{uuid()}}" + ce_time: "{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}" + content-type: application/avro + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-cloudevents-structured.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-cloudevents-structured.yaml new file mode 100644 index 000000000..70bcfc6fe --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-cloudevents-structured.yaml @@ -0,0 +1,98 @@ +asyncapi: '2.0.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up CloudEvents API + version: 0.1.4 + description: Sample AsyncAPI for user signedup events defined using CloudEvents structured + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + content-type: + type: string + enum: + - 'application/cloudevents+json; charset=UTF-8' + payload: + $ref: '#/components/schemas/userSignedUpPayload' + examples: + - laurent: + summary: Example for Laurent user + headers: |- + { + "sentAt": "2020-03-11T08:03:28Z", + "content-type": "application/cloudevents+json; charset=UTF-8" + } + payload: |- + { + "specversion": "1.0", + "type": "com.example.someevent", + "source": "/mycontext/subcontext", + "id": "1234-1234-1234", + "time": "2020-03-11T08:03:28Z", + "datacontenttype" : "application/json", + "data": { + "fullName": "Laurent Broudoux", + "email": "laurent@microcks.io", + "age": 41 + } + } + - john: + summary: Example for John Doe user + headers: + sentAt: "2020-03-11T08:03:38Z" + content-type: "application/cloudevents+json; charset=UTF-8" + payload: + specversion: "1.0" + type: "io.microcks.example.user-signedup" + source: "/mycontext/subcontext" + id: "{{uuid()}}" + time: "{{now(yyyy-MM-dd'T'HH:mm:SS'Z')}}" + datacontenttype : "application/json" + data: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + schemas: + userSignedUpPayload: + type: object + allOf: + - $ref: 'https://raw.githubusercontent.com/cloudevents/spec/v1.0.1/spec.json' + properties: + data: + $ref: '#/components/schemas/userSignedUpData' + userSignedUpData: + type: object + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-commons.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-commons.yaml new file mode 100644 index 000000000..98d13f604 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-commons.yaml @@ -0,0 +1,28 @@ +components: + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + - laurent: + my-app-header: 21 + - yacine: + my-app-header: 22 + schema: + UserInfo: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml new file mode 100644 index 000000000..94240a3b6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-json-ref-asyncapi.yaml @@ -0,0 +1,79 @@ +asyncapi: '2.4.0' +id: 'urn:io.microcks.example.user-signedup' +info: + title: User signed-up API + version: 0.1.0 + description: Sample AsyncAPI for user signedup events + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +defaultContentType: application/json +channels: + user/signedout: + subscribe: + message: + payload: + $ref: '#/components/schemas/UserSignedupEvent' + user/signedup: + description: The topic on which user signed up events may be consumed + subscribe: + summary: Receive informations about user signed up + operationId: receivedUserSIgnedUp + message: + description: An event describing that a user just signed up. + bindings: + kafka: + key: + type: string + description: Timestamp of event as milliseconds since 1st Jan 1970 + traits: + - $ref: '#/components/messageTraits/commonHeaders' + headers: + type: object + properties: + sentAt: + type: string + format: date-time + description: Date and time when the event was sent + payload: + $ref: '#/components/schemas/UserSignedupEvent' + examples: + - laurent: + summary: Example for Laurent user + headers: |- + {"my-app-header": 23, "sentAt": "2020-03-11T08:03:28Z"} + payload: |- + {"fullName": "Laurent Broudoux", "email": "laurent@microcks.io", "age": 41} + - john: + summary: Example for John Doe user + headers: + my-app-header: 24 + sentAt: "2020-03-11T08:03:38Z" + payload: + fullName: John Doe + email: john@microcks.io + age: 36 +components: + schemas: + UserSignedupEvent: + type: object + allOf: + - $ref: "./user-signedup.json" + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + examples: + laurent: + my-app-header: 21 + yacine: + my-app-header: 22 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schemas.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schemas.yaml new file mode 100644 index 000000000..7518ad6e8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup-schemas.yaml @@ -0,0 +1,14 @@ +components: + schemas: + UserInfo: + type: object + additionalProperties: false + properties: + fullName: + type: string + email: + type: string + format: email + age: + type: integer + minimum: 18 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.avsc b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.avsc new file mode 100644 index 000000000..09b4d4cb6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.avsc @@ -0,0 +1,9 @@ +{"namespace": "microcks.avro", + "type": "record", + "name": "User", + "fields": [ + {"name": "fullName", "type": "string"}, + {"name": "email", "type": "string"}, + {"name": "age", "type": "int"} + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.json new file mode 100644 index 000000000..5c4f0f483 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/asyncapi/user-signedup.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "UserSignedupEvent Specification JSON Schema", + "type": "object", + "additionalProperties": false, + "properties": { + "fullName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer", + "minimum": 18 + } + } +} + + + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-1.0-examples.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-1.0-examples.yml new file mode 100644 index 000000000..2c5d33aa6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-1.0-examples.yml @@ -0,0 +1,179 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: Movie Graph API + version: 1.0 +operations: + allFilms: + affFilms: + request: + body: + query: |- + query { + allFilms { + films { + id + title + episodeID + director + starCount + rating + } + } + } + response: + mediaType: application/json + body: + data: + allFilms: + films: + - id: ZmlsbXM6MQ== + title: A New Hope + episodeID: 4 + director: George Lucas + starCount: 432 + rating: 4.3 + - id: ZmlsbXM6Mg== + title: The Empire Strikes Back + episodeID: 5 + director: Irvin Kershner + starCount: 433 + rating: 4.3 + - id: ZmlsbXM6Mw== + title: Return of the Jedi + episodeID: 6 + director: Richard Marquand + starCount: 434 + rating: 4.3 + - id: ZmlsbXM6NA== + title: The Phantom Menace + episodeID: 1 + director: George Lucas + starCount: 252 + rating: 3.2 + - id: ZmlsbXM6NQ== + title: Attack of the Clones + episodeID: 2 + director: George Lucas + starCount: 320 + rating: 3.9 + - id: ZmlsbXM6Ng== + title: Revenge of the Sith + episodeID: 3 + director: George Lucas + starCount: 410 + rating: 4.1 + film: + film ZmlsbXM6MQ==: + request: + body: + query: |- + query { + film(id: $id) { + id + title + episodeID + director + starCount + rating + } + } + variables: + id: ZmlsbXM6MQ== + response: + mediaType: application/json + body: + data: + film: + id: ZmlsbXM6MQ== + title: A New Hope + episodeID: 4 + director: George Lucas + starCount: 432 + rating: 4.3 + film ZmlsbXM6Mg==: + request: + body: + query: |- + query { + film(id: $id) { + id + title + episodeID + director + starCount + rating + } + } + variables: + id: ZmlsbXM6Mg== + response: + mediaType: application/json + body: + data: + film: + id: ZmlsbXM6Mg== + title: The Empire Strikes Back + episodeID: 5 + director: Irvin Kershner + starCount: 433 + rating: 4.3 + addStar: + addStar to ZmlsbXM6Mg==: + request: + body: + query: |- + mutation AddStar($filmId: String) { + addStar(filmId: $filmId) { + id + title + episodeID + director + starCount + rating + } + } + variables: + filmId: ZmlsbXM6Mg== + response: + mediaType: application/json + body: + data: + addStar: + id: ZmlsbXM6Mg== + title: The Empire Strikes Back + episodeID: 5 + director: Irvin Kershner + starCount: 434 + rating: 4.3 + addReview: + addReview to ZmlsbXM6Mg==: + request: + body: + query: |- + mutation AddReview($filmId: String, $review: Review) { + addReview(filmId: $filmId, review: $review) { + id + title + episodeID + director + starCount + rating + } + } + variables: + filmId: ZmlsbXM6Mg== + review: + rating: 5 + commentary: "Awesome movie, I love it !" + response: + mediaType: application/json + body: + data: + addReview: + id: ZmlsbXM6Mg== + title: The Empire Strikes Back + episodeID: 5 + director: Irvin Kershner + starCount: 433 + rating: 4.4 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-metadata.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-metadata.yml new file mode 100644 index 000000000..5b01825d5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-metadata.yml @@ -0,0 +1,21 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIMetadata +metadata: + name: Movie Graph API + version: 1.0 + labels: + domain: movie + status: stable + team: Team A +operations: + 'addReview': + dispatcher: JSON_BODY + dispatcherRules: |- + { + "exp": "/filmId", + "operator": "equals", + "cases": { + "ZmlsbXM6Mg==": "addReview to ZmlsbXM6Mg==", + "default": "ZmlsbXM6Mg==" + } + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-postman.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-postman.json new file mode 100644 index 000000000..50474bc00 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-postman.json @@ -0,0 +1,245 @@ +{ + "info": { + "_postman_id": "7c00b563-d454-421a-a3b0-17c4e3254cb9", + "name": "Movie Graph API", + "description": "version=1.0 - A Graph API for querying movies", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "queries", + "item": [ + { + "name": "allFilms", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query allFilms {\n allFilms {\n totalCount\n films {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n }\n}", + "variables": "{}" + } + }, + "url": { + "raw": "http://allFilms", + "protocol": "http", + "host": [ + "allFilms" + ] + } + }, + "response": [ + { + "name": "allFilms", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query allFilms {\n allFilms {\n films {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n }\n}", + "variables": "{}" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"allFilms\": {\n \"films\": [\n {\n \"id\": \"ZmlsbXM6MQ==\",\n \"title\": \"A New Hope\",\n \"episodeID\": 4,\n \"director\": \"George Lucas\",\n \"starCount\": 432,\n \"rating\": 4.3\n },\n {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 433,\n \"rating\": 4.3\n },\n {\n \"id\": \"ZmlsbXM6Mw==\",\n \"title\": \"Return of the Jedi\",\n \"episodeID\": 6,\n \"director\": \"Richard Marquand\",\n \"starCount\": 434,\n \"rating\": 4.3\n },\n {\n \"id\": \"ZmlsbXM6NA==\",\n \"title\": \"The Phantom Menace\",\n \"episodeID\": 1,\n \"director\": \"George Lucas\",\n \"starCount\": 252,\n \"rating\": 3.2\n },\n {\n \"id\": \"ZmlsbXM6NQ==\",\n \"title\": \"Attack of the Clones\",\n \"episodeID\": 2,\n \"director\": \"George Lucas\",\n \"starCount\": 320,\n \"rating\": 3.9\n },\n {\n \"id\": \"ZmlsbXM6Ng==\",\n \"title\": \"Revenge of the Sith\",\n \"episodeID\": 3,\n \"director\": \"George Lucas\",\n \"starCount\": 410,\n \"rating\": 4.1\n }\n ]\n }\n }\n}" + } + ] + }, + { + "name": "film", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"id\": \"\"\n}" + } + }, + "url": { + "raw": "http://film", + "protocol": "http", + "host": [ + "film" + ] + } + }, + "response": [ + { + "name": "film ZmlsbXM6MQ==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"id\": \"ZmlsbXM6MQ==\"\n}" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"film\": {\n \"id\": \"ZmlsbXM6MQ==\",\n \"title\": \"A New Hope\",\n \"episodeID\": 4,\n \"director\": \"George Lucas\",\n \"starCount\": 432,\n \"rating\": 4.3\n }\n }\n}" + }, + { + "name": "film ZmlsbXM6Mg==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"id\": \"ZmlsbXM6Mg==\"\n}" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"film\": {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 433,\n \"rating\": 4.3\n }\n }\n}" + } + ] + } + ] + }, + { + "name": "mutations", + "item": [ + { + "name": "addStar", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation AddStar($filmId: String) {\n addStar(filmId: $filmId) {\n id\n starCount\n rating\n }\n}", + "variables": "{\n \"filmId\": \"\"\n}" + } + }, + "url": { + "raw": "http://addStar", + "protocol": "http", + "host": [ + "addStar" + ] + } + }, + "response": [ + { + "name": "addStar to ZmlsbXM6Mg==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation AddStar($filmId: String) {\n addStar(filmId: $filmId) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"filmId\": \"ZmlsbXM6Mg==\"\n}" + } + }, + "url": { + "raw": "http://addStar", + "protocol": "http", + "host": [ + "addStar" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"addStar\": {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 434,\n \"rating\": 4.3\n }\n }\n}" + } + ] + }, + { + "name": "addReview", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://addReview", + "protocol": "http", + "host": [ + "addReview" + ] + } + }, + "response": [ + { + "name": "addReview to ZmlsbXM6Mg==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation AddReview($filmId: String, $review: Review) {\n addReview(filmId: $filmId, review: $review) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}\n", + "variables": "{\n \"filmId\": \"ZmlsbXM6Mg==\",\n \"review\": {\n \"comment\": \"Awesome!\",\n \"rating\": 5\n }\n}\n" + } + }, + "url": { + "raw": "http://addReview", + "protocol": "http", + "host": [ + "addReview" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"addReview\": {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 433,\n \"rating\": 4.4\n }\n }\n}" + } + ] + } + ] + } + ], + "variable": [ + { + "key": "url", + "value": "", + "type": "any", + "description": "URL for the request." + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-to-test-proxy-postman.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-to-test-proxy-postman.json new file mode 100644 index 000000000..3c2dc1630 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-to-test-proxy-postman.json @@ -0,0 +1,98 @@ +{ + "info": { + "_postman_id": "7c00b563-d454-421a-a3b0-17c4e3254cb9", + "name": "Movie Graph Original API", + "description": "version=1.0 - A simple Graph API for testing proxy", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "queries", + "item": [ + { + "name": "film", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n }\n}", + "variables": "{\n \"id\": \"\"\n}" + } + }, + "url": { + "raw": "http://film", + "protocol": "http", + "host": [ + "film" + ] + } + }, + "response": [ + { + "name": "film ZmlsbXM6Mg==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n comment\n }\n}", + "variables": "{\n \"id\": \"ZmlsbXM6Mg==\"\n}" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"film\": {\n \"id\": \"ZmlsbXM6Mg==\",\n \"title\": \"The Empire Strikes Back\",\n \"episodeID\": 5,\n \"director\": \"Irvin Kershner\",\n \"starCount\": 433,\n \"rating\": 4.3,\n \"comment\": \"Original!!!\"\n }\n }\n}" + }, + { + "name": "film ZmlsbXM6MA==", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "query film ($id: String) {\n film (id: $id) {\n id\n title\n episodeID\n director\n starCount\n rating\n comment\n }\n}", + "variables": "{\n \"id\": \"ZmlsbXM6MA==\"\n}" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"data\": {\n \"film\": {\n \"id\": \"ZmlsbXM6MA==\",\n \"title\": \"The Force Awakens\",\n \"episodeID\": 7,\n \"director\": \"J. J. Abrams\",\n \"starCount\": 200,\n \"rating\": 3.0,\n \"comment\": \"Original!!!\"\n }\n }\n}" + } + ] + } + ] + } + ], + "variable": [ + { + "key": "url", + "value": "", + "type": "any", + "description": "URL for the request." + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-to-test-proxy.graphql b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-to-test-proxy.graphql new file mode 100644 index 000000000..4c5194bdd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films-to-test-proxy.graphql @@ -0,0 +1,18 @@ +# microcksId: Movie Graph Original API : 1.0 +schema { + query: Query +} +type Film { + id: String! + title: String! + episodeID: Int! + director: String! + starCount: Int! + rating: Float! + comment: String! +} + +type Query { + film(id: String): Film +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films.graphql b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films.graphql new file mode 100644 index 000000000..f6cd1f0ae --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/films.graphql @@ -0,0 +1,34 @@ +# microcksId: Movie Graph API : 1.0 +schema { + query: Query + mutation: Mutation +} +type Film { + id: String! + title: String! + episodeID: Int! + director: String! + starCount: Int! + rating: Float! +} + +type FilmsConnection { + totalCount: Int! + films: [Film] +} + +input Review { + comment: String + rating: Int +} + +type Query { + allFilms: FilmsConnection + film(id: String): Film +} + +type Mutation { + addStar(filmId: String): Film + addReview(filmId: String, review: Review): Film +} + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/github.graphql b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/github.graphql new file mode 100644 index 000000000..5e05de18e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/github.graphql @@ -0,0 +1,69419 @@ +# microcksId: GitHub API : 1.0 + +schema { + query: Query + mutation: Mutation +} + +""" +Marks an element of a GraphQL schema as only available via a preview header +""" +directive @preview( + """ + The identifier of the API preview that toggles this field. + """ + toggledBy: String! +) on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION + +""" +Defines what type of global IDs are accepted for a mutation argument of type ID. +""" +directive @possibleTypes( + """ + Abstract type of accepted global ID + """ + abstractType: String + + """ + Accepted types of global IDs. + """ + concreteTypes: [String!]! +) on INPUT_FIELD_DEFINITION + +""" +Autogenerated input type of AbortQueuedMigrations +""" +input AbortQueuedMigrationsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization that is running the migrations. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of AbortQueuedMigrations. +""" +type AbortQueuedMigrationsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Did the operation succeed? + """ + success: Boolean +} + +""" +Autogenerated input type of AbortRepositoryMigration +""" +input AbortRepositoryMigrationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the migration to be aborted. + """ + migrationId: ID! @possibleTypes(concreteTypes: ["RepositoryMigration"]) +} + +""" +Autogenerated return type of AbortRepositoryMigration. +""" +type AbortRepositoryMigrationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Did the operation succeed? + """ + success: Boolean +} + +""" +Autogenerated input type of AcceptEnterpriseAdministratorInvitation +""" +input AcceptEnterpriseAdministratorInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the invitation being accepted + """ + invitationId: ID! @possibleTypes(concreteTypes: ["EnterpriseAdministratorInvitation"]) +} + +""" +Autogenerated return type of AcceptEnterpriseAdministratorInvitation. +""" +type AcceptEnterpriseAdministratorInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The invitation that was accepted. + """ + invitation: EnterpriseAdministratorInvitation + + """ + A message confirming the result of accepting an administrator invitation. + """ + message: String +} + +""" +Autogenerated input type of AcceptEnterpriseMemberInvitation +""" +input AcceptEnterpriseMemberInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the invitation being accepted + """ + invitationId: ID! @possibleTypes(concreteTypes: ["EnterpriseMemberInvitation"]) +} + +""" +Autogenerated return type of AcceptEnterpriseMemberInvitation. +""" +type AcceptEnterpriseMemberInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The invitation that was accepted. + """ + invitation: EnterpriseMemberInvitation + + """ + A message confirming the result of accepting an unaffiliated member invitation. + """ + message: String +} + +""" +Autogenerated input type of AcceptTopicSuggestion +""" +input AcceptTopicSuggestionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the suggested topic. + + **Upcoming Change on 2024-04-01 UTC** + **Description:** `name` will be removed. + **Reason:** Suggested topics are no longer supported + """ + name: String + + """ + The Node ID of the repository. + + **Upcoming Change on 2024-04-01 UTC** + **Description:** `repositoryId` will be removed. + **Reason:** Suggested topics are no longer supported + """ + repositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of AcceptTopicSuggestion. +""" +type AcceptTopicSuggestionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The accepted topic. + """ + topic: Topic @deprecated(reason: "Suggested topics are no longer supported Removal on 2024-04-01 UTC.") +} + +""" +Autogenerated input type of AccessUserNamespaceRepository +""" +input AccessUserNamespaceRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise owning the user namespace repository. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the user namespace repository to access. + """ + repositoryId: ID! +} + +""" +Autogenerated return type of AccessUserNamespaceRepository. +""" +type AccessUserNamespaceRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The time that repository access expires at + """ + expiresAt: DateTime + + """ + The repository that is temporarily accessible. + """ + repository: Repository +} + +""" +Represents an object which can take actions on GitHub. Typically a User or Bot. +""" +interface Actor { + """ + A URL pointing to the actor's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The username of the actor. + """ + login: String! + + """ + The HTTP path for this actor. + """ + resourcePath: URI! + + """ + The HTTP URL for this actor. + """ + url: URI! +} + +""" +The connection type for Actor. +""" +type ActorConnection { + """ + A list of edges. + """ + edges: [ActorEdge] + + """ + A list of nodes. + """ + nodes: [Actor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ActorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Actor +} + +""" +Location information for an actor +""" +type ActorLocation { + """ + City + """ + city: String + + """ + Country name + """ + country: String + + """ + Country code + """ + countryCode: String + + """ + Region name + """ + region: String + + """ + Region or state code + """ + regionCode: String +} + +""" +The actor's type. +""" +enum ActorType { + """ + Indicates a team actor. + """ + TEAM + + """ + Indicates a user actor. + """ + USER +} + +""" +Autogenerated input type of AddAssigneesToAssignable +""" +input AddAssigneesToAssignableInput { + """ + The id of the assignable object to add assignees to. + """ + assignableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Assignable") + + """ + The id of users to add as assignees. + """ + assigneeIds: [ID!]! @possibleTypes(concreteTypes: ["User"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of AddAssigneesToAssignable. +""" +type AddAssigneesToAssignablePayload { + """ + The item that was assigned. + """ + assignable: Assignable + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of AddComment +""" +input AddCommentInput { + """ + The contents of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") +} + +""" +Autogenerated return type of AddComment. +""" +type AddCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The edge from the subject's comment connection. + """ + commentEdge: IssueCommentEdge + + """ + The subject + """ + subject: Node + + """ + The edge from the subject's timeline connection. + """ + timelineEdge: IssueTimelineItemEdge +} + +""" +Autogenerated input type of AddDiscussionComment +""" +input AddDiscussionCommentInput { + """ + The contents of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion to comment on. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["Discussion"]) + + """ + The Node ID of the discussion comment within this discussion to reply to. + """ + replyToId: ID @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of AddDiscussionComment. +""" +type AddDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created discussion comment. + """ + comment: DiscussionComment +} + +""" +Autogenerated input type of AddDiscussionPollVote +""" +input AddDiscussionPollVoteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion poll option to vote for. + """ + pollOptionId: ID! @possibleTypes(concreteTypes: ["DiscussionPollOption"]) +} + +""" +Autogenerated return type of AddDiscussionPollVote. +""" +type AddDiscussionPollVotePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The poll option that a vote was added to. + """ + pollOption: DiscussionPollOption +} + +""" +Autogenerated input type of AddEnterpriseOrganizationMember +""" +input AddEnterpriseOrganizationMemberInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise which owns the organization. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization the users will be added to. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The role to assign the users in the organization + """ + role: OrganizationMemberRole + + """ + The IDs of the enterprise members to add. + """ + userIds: [ID!]! +} + +""" +Autogenerated return type of AddEnterpriseOrganizationMember. +""" +type AddEnterpriseOrganizationMemberPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The users who were added to the organization. + """ + users: [User!] +} + +""" +Autogenerated input type of AddEnterpriseSupportEntitlement +""" +input AddEnterpriseSupportEntitlementInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a member who will receive the support entitlement. + """ + login: String! +} + +""" +Autogenerated return type of AddEnterpriseSupportEntitlement. +""" +type AddEnterpriseSupportEntitlementPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of adding the support entitlement. + """ + message: String +} + +""" +Autogenerated input type of AddLabelsToLabelable +""" +input AddLabelsToLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ids of the labels to add. + """ + labelIds: [ID!]! @possibleTypes(concreteTypes: ["Label"]) + + """ + The id of the labelable object to add labels to. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of AddLabelsToLabelable. +""" +type AddLabelsToLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was labeled. + """ + labelable: Labelable +} + +""" +Autogenerated input type of AddProjectCard +""" +input AddProjectCardInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The content of the card. Must be a member of the ProjectCardItem union + """ + contentId: ID @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "ProjectCardItem") + + """ + The note on the card. + """ + note: String + + """ + The Node ID of the ProjectColumn. + """ + projectColumnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of AddProjectCard. +""" +type AddProjectCardPayload { + """ + The edge from the ProjectColumn's card connection. + """ + cardEdge: ProjectCardEdge + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ProjectColumn + """ + projectColumn: ProjectColumn +} + +""" +Autogenerated input type of AddProjectColumn +""" +input AddProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the column. + """ + name: String! + + """ + The Node ID of the project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) +} + +""" +Autogenerated return type of AddProjectColumn. +""" +type AddProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The edge from the project's column connection. + """ + columnEdge: ProjectColumnEdge + + """ + The project + """ + project: Project +} + +""" +Autogenerated input type of AddProjectV2DraftIssue +""" +input AddProjectV2DraftIssueInput { + """ + The IDs of the assignees of the draft issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body of the draft issue. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to add the draft issue to. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The title of the draft issue. A project item can also be created by providing + the URL of an Issue or Pull Request if you have access. + """ + title: String! +} + +""" +Autogenerated return type of AddProjectV2DraftIssue. +""" +type AddProjectV2DraftIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The draft issue added to the project. + """ + projectItem: ProjectV2Item +} + +""" +Autogenerated input type of AddProjectV2ItemById +""" +input AddProjectV2ItemByIdInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the Issue or Pull Request to add. + """ + contentId: ID! + @possibleTypes(concreteTypes: ["DraftIssue", "Issue", "PullRequest"], abstractType: "ProjectV2ItemContent") + + """ + The ID of the Project to add the item to. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of AddProjectV2ItemById. +""" +type AddProjectV2ItemByIdPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item added to the project. + """ + item: ProjectV2Item +} + +""" +Autogenerated input type of AddPullRequestReviewComment +""" +input AddPullRequestReviewCommentInput { + """ + The text of the comment. This field is required + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `body` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The SHA of the commit to comment on. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `commitOID` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + commitOID: GitObjectID + + """ + The comment id to reply to. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `inReplyTo` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + inReplyTo: ID @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) + + """ + The relative path of the file to comment on. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `path` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + path: String + + """ + The line index in the diff to comment on. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `position` will be removed. use addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + position: Int + + """ + The node ID of the pull request reviewing + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `pullRequestId` will be removed. use + addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node ID of the review to modify. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `pullRequestReviewId` will be removed. use + addPullRequestReviewThread or addPullRequestReviewThreadReply instead + **Reason:** We are deprecating the addPullRequestReviewComment mutation + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of AddPullRequestReviewComment. +""" +type AddPullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created comment. + """ + comment: PullRequestReviewComment + + """ + The edge from the review's comment connection. + """ + commentEdge: PullRequestReviewCommentEdge +} + +""" +Autogenerated input type of AddPullRequestReview +""" +input AddPullRequestReviewInput { + """ + The contents of the review body comment. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The review line comments. + + **Upcoming Change on 2023-10-01 UTC** + **Description:** `comments` will be removed. use the `threads` argument instead + **Reason:** We are deprecating comment fields that use diff-relative positioning + """ + comments: [DraftPullRequestReviewComment] + + """ + The commit OID the review pertains to. + """ + commitOID: GitObjectID + + """ + The event to perform on the pull request review. + """ + event: PullRequestReviewEvent + + """ + The Node ID of the pull request to modify. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The review line comment threads. + """ + threads: [DraftPullRequestReviewThread] +} + +""" +Autogenerated return type of AddPullRequestReview. +""" +type AddPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created pull request review. + """ + pullRequestReview: PullRequestReview + + """ + The edge from the pull request's review connection. + """ + reviewEdge: PullRequestReviewEdge +} + +""" +Autogenerated input type of AddPullRequestReviewThread +""" +input AddPullRequestReviewThreadInput { + """ + Body of the thread's first comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The line of the blob to which the thread refers, required for line-level + threads. The end of the line range for multi-line comments. + """ + line: Int + + """ + Path to the file being commented on. + """ + path: String + + """ + The node ID of the pull request reviewing + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node ID of the review to modify. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) + + """ + The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. + """ + side: DiffSide = RIGHT + + """ + The first line of the range to which the comment refers. + """ + startLine: Int + + """ + The side of the diff on which the start line resides. + """ + startSide: DiffSide = RIGHT + + """ + The level at which the comments in the corresponding thread are targeted, can be a diff line or a file + """ + subjectType: PullRequestReviewThreadSubjectType = LINE +} + +""" +Autogenerated return type of AddPullRequestReviewThread. +""" +type AddPullRequestReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created thread. + """ + thread: PullRequestReviewThread +} + +""" +Autogenerated input type of AddPullRequestReviewThreadReply +""" +input AddPullRequestReviewThreadReplyInput { + """ + The text of the reply. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pending review to which the reply will belong. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) + + """ + The Node ID of the thread to which this reply is being written. + """ + pullRequestReviewThreadId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewThread"]) +} + +""" +Autogenerated return type of AddPullRequestReviewThreadReply. +""" +type AddPullRequestReviewThreadReplyPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created reply. + """ + comment: PullRequestReviewComment +} + +""" +Autogenerated input type of AddReaction +""" +input AddReactionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the emoji to react with. + """ + content: ReactionContent! + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! + @possibleTypes( + concreteTypes: [ + "CommitComment" + "Discussion" + "DiscussionComment" + "Issue" + "IssueComment" + "PullRequest" + "PullRequestReview" + "PullRequestReviewComment" + "Release" + "TeamDiscussion" + "TeamDiscussionComment" + ] + abstractType: "Reactable" + ) +} + +""" +Autogenerated return type of AddReaction. +""" +type AddReactionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The reaction object. + """ + reaction: Reaction + + """ + The reaction groups for the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + The reactable subject. + """ + subject: Reactable +} + +""" +Autogenerated input type of AddStar +""" +input AddStarInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Starrable ID to star. + """ + starrableId: ID! @possibleTypes(concreteTypes: ["Gist", "Repository", "Topic"], abstractType: "Starrable") +} + +""" +Autogenerated return type of AddStar. +""" +type AddStarPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The starrable. + """ + starrable: Starrable +} + +""" +Autogenerated input type of AddSubIssue +""" +input AddSubIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the issue. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + Option to replace parent issue if one already exists + """ + replaceParent: Boolean + + """ + The id of the sub-issue. + """ + subIssueId: ID @possibleTypes(concreteTypes: ["Issue"]) + + """ + The url of the sub-issue. + """ + subIssueUrl: String +} + +""" +Autogenerated return type of AddSubIssue. +""" +type AddSubIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The parent issue that the sub-issue was added to. + """ + issue: Issue + + """ + The sub-issue of the parent. + """ + subIssue: Issue +} + +""" +Autogenerated input type of AddUpvote +""" +input AddUpvoteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion or comment to upvote. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["Discussion", "DiscussionComment"], abstractType: "Votable") +} + +""" +Autogenerated return type of AddUpvote. +""" +type AddUpvotePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The votable subject. + """ + subject: Votable +} + +""" +Autogenerated input type of AddVerifiableDomain +""" +input AddVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The URL of the domain + """ + domain: URI! + + """ + The ID of the owner to add the domain to + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Enterprise", "Organization"], abstractType: "VerifiableDomainOwner") +} + +""" +Autogenerated return type of AddVerifiableDomain. +""" +type AddVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verifiable domain that was added. + """ + domain: VerifiableDomain +} + +""" +Represents an 'added_to_merge_queue' event on a given pull request. +""" +type AddedToMergeQueueEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who added this Pull Request to the merge queue + """ + enqueuer: User + + """ + The Node ID of the AddedToMergeQueueEvent object + """ + id: ID! + + """ + The merge queue where this pull request was added to. + """ + mergeQueue: MergeQueue + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'added_to_project' event on a given issue or pull request. +""" +type AddedToProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The Node ID of the AddedToProjectEvent object + """ + id: ID! + + """ + Project referenced by event. + """ + project: Project + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Column name referenced by this project event. + """ + projectColumnName: String! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) +} + +""" +An announcement banner for an enterprise or organization. +""" +type AnnouncementBanner { + """ + The date the announcement was created + """ + createdAt: DateTime! + + """ + The expiration date of the announcement, if any + """ + expiresAt: DateTime + + """ + Whether the announcement can be dismissed by the user + """ + isUserDismissible: Boolean! + + """ + The text of the announcement + """ + message: String +} + +""" +A GitHub App. +""" +type App implements Node { + """ + The client ID of the app. + """ + clientId: String + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the app. + """ + description: String + + """ + The Node ID of the App object + """ + id: ID! + + """ + The IP addresses of the app. + """ + ipAllowListEntries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for IP allow list entries returned. + """ + orderBy: IpAllowListEntryOrder = {field: ALLOW_LIST_VALUE, direction: ASC} + ): IpAllowListEntryConnection! + + """ + The hex color code, without the leading '#', for the logo background. + """ + logoBackgroundColor: String! + + """ + A URL pointing to the app's logo. + """ + logoUrl( + """ + The size of the resulting image. + """ + size: Int + ): URI! + + """ + The name of the app. + """ + name: String! + + """ + A slug based on the name of the app for use in URLs. + """ + slug: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The URL to the app's homepage. + """ + url: URI! +} + +""" +Autogenerated input type of ApproveDeployments +""" +input ApproveDeploymentsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Optional comment for approving deployments + """ + comment: String = "" + + """ + The ids of environments to reject deployments + """ + environmentIds: [ID!]! + + """ + The node ID of the workflow run containing the pending deployments. + """ + workflowRunId: ID! @possibleTypes(concreteTypes: ["WorkflowRun"]) +} + +""" +Autogenerated return type of ApproveDeployments. +""" +type ApproveDeploymentsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The affected deployments. + """ + deployments: [Deployment!] +} + +""" +Autogenerated input type of ApproveVerifiableDomain +""" +input ApproveVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to approve. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of ApproveVerifiableDomain. +""" +type ApproveVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verifiable domain that was approved. + """ + domain: VerifiableDomain +} + +""" +Autogenerated input type of ArchiveProjectV2Item +""" +input ArchiveProjectV2ItemInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the ProjectV2Item to archive. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project to archive the item from. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of ArchiveProjectV2Item. +""" +type ArchiveProjectV2ItemPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item archived from the project. + """ + item: ProjectV2Item +} + +""" +Autogenerated input type of ArchiveRepository +""" +input ArchiveRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the repository to mark as archived. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of ArchiveRepository. +""" +type ArchiveRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that was marked as archived. + """ + repository: Repository +} + +""" +An object that can have users assigned to it. +""" +interface Assignable { + """ + A list of actors assigned to this object. + """ + assignedActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): AssigneeConnection! + + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + A list of suggested actors to assign to this object + """ + suggestedActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If provided, searches users by login or profile name + """ + query: String + ): AssigneeConnection! +} + +""" +Represents an 'assigned' event on any assignable object. +""" +type AssignedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the assignable associated with the event. + """ + assignable: Assignable! + + """ + Identifies the user or mannequin that was assigned. + """ + assignee: Assignee + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the AssignedEvent object + """ + id: ID! + + """ + Identifies the user who was assigned. + """ + user: User + @deprecated(reason: "Assignees can now be mannequins. Use the `assignee` field instead. Removal on 2020-01-01 UTC.") +} + +""" +Types that can be assigned to issues. +""" +union Assignee = Bot | Mannequin | Organization | User + +""" +The connection type for Assignee. +""" +type AssigneeConnection { + """ + A list of edges. + """ + edges: [AssigneeEdge] + + """ + A list of nodes. + """ + nodes: [Assignee] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type AssigneeEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Assignee +} + +""" +An entry in the audit log. +""" +interface AuditEntry { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Types that can initiate an audit log event. +""" +union AuditEntryActor = Bot | Organization | User + +""" +Ordering options for Audit Log connections. +""" +input AuditLogOrder { + """ + The ordering direction. + """ + direction: OrderDirection + + """ + The field to order Audit Logs by. + """ + field: AuditLogOrderField +} + +""" +Properties by which Audit Log connections can be ordered. +""" +enum AuditLogOrderField { + """ + Order audit log entries by timestamp + """ + CREATED_AT +} + +""" +Represents a 'auto_merge_disabled' event on a given pull request. +""" +type AutoMergeDisabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who disabled auto-merge for this Pull Request + """ + disabler: User + + """ + The Node ID of the AutoMergeDisabledEvent object + """ + id: ID! + + """ + PullRequest referenced by event + """ + pullRequest: PullRequest + + """ + The reason auto-merge was disabled + """ + reason: String + + """ + The reason_code relating to why auto-merge was disabled + """ + reasonCode: String +} + +""" +Represents a 'auto_merge_enabled' event on a given pull request. +""" +type AutoMergeEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge for this Pull Request + """ + enabler: User + + """ + The Node ID of the AutoMergeEnabledEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents an auto-merge request for a pull request +""" +type AutoMergeRequest { + """ + The email address of the author of this auto-merge request. + """ + authorEmail: String + + """ + The commit message of the auto-merge request. If a merge queue is required by + the base branch, this value will be set by the merge queue when merging. + """ + commitBody: String + + """ + The commit title of the auto-merge request. If a merge queue is required by + the base branch, this value will be set by the merge queue when merging + """ + commitHeadline: String + + """ + When was this auto-merge request was enabled. + """ + enabledAt: DateTime + + """ + The actor who created the auto-merge request. + """ + enabledBy: Actor + + """ + The merge method of the auto-merge request. If a merge queue is required by + the base branch, this value will be set by the merge queue when merging. + """ + mergeMethod: PullRequestMergeMethod! + + """ + The pull request that this auto-merge request is set against. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'auto_rebase_enabled' event on a given pull request. +""" +type AutoRebaseEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge (rebase) for this Pull Request + """ + enabler: User + + """ + The Node ID of the AutoRebaseEnabledEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'auto_squash_enabled' event on a given pull request. +""" +type AutoSquashEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge (squash) for this Pull Request + """ + enabler: User + + """ + The Node ID of the AutoSquashEnabledEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'automatic_base_change_failed' event on a given pull request. +""" +type AutomaticBaseChangeFailedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the AutomaticBaseChangeFailedEvent object + """ + id: ID! + + """ + The new base for this PR + """ + newBase: String! + + """ + The old base for this PR + """ + oldBase: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'automatic_base_change_succeeded' event on a given pull request. +""" +type AutomaticBaseChangeSucceededEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the AutomaticBaseChangeSucceededEvent object + """ + id: ID! + + """ + The new base for this PR + """ + newBase: String! + + """ + The old base for this PR + """ + oldBase: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +A (potentially binary) string encoded using base64. +""" +scalar Base64String + +""" +Represents a 'base_ref_changed' event on a given issue or pull request. +""" +type BaseRefChangedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the name of the base ref for the pull request after it was changed. + """ + currentRefName: String! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the BaseRefChangedEvent object + """ + id: ID! + + """ + Identifies the name of the base ref for the pull request before it was changed. + """ + previousRefName: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'base_ref_deleted' event on a given pull request. +""" +type BaseRefDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the name of the Ref associated with the `base_ref_deleted` event. + """ + baseRefName: String + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the BaseRefDeletedEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'base_ref_force_pushed' event on a given pull request. +""" +type BaseRefForcePushedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the after commit SHA for the 'base_ref_force_pushed' event. + """ + afterCommit: Commit + + """ + Identifies the before commit SHA for the 'base_ref_force_pushed' event. + """ + beforeCommit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the BaseRefForcePushedEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the fully qualified ref name for the 'base_ref_force_pushed' event. + """ + ref: Ref +} + +""" +Represents non-fractional signed whole numeric values. Since the value may +exceed the size of a 32-bit integer, it's encoded as a string. +""" +scalar BigInt + +""" +Represents a Git blame. +""" +type Blame { + """ + The list of ranges from a Git blame. + """ + ranges: [BlameRange!]! +} + +""" +Represents a range of information from a Git blame. +""" +type BlameRange { + """ + Identifies the recency of the change, from 1 (new) to 10 (old). This is + calculated as a 2-quantile and determines the length of distance between the + median age of all the changes in the file and the recency of the current + range's change. + """ + age: Int! + + """ + Identifies the line author + """ + commit: Commit! + + """ + The ending line for the range + """ + endingLine: Int! + + """ + The starting line for the range + """ + startingLine: Int! +} + +""" +Represents a Git blob. +""" +type Blob implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + Byte size of Blob object + """ + byteSize: Int! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + The Node ID of the Blob object + """ + id: ID! + + """ + Indicates whether the Blob is binary or text. Returns null if unable to determine the encoding. + """ + isBinary: Boolean + + """ + Indicates whether the contents is truncated + """ + isTruncated: Boolean! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! + + """ + UTF8 text data or null if the Blob is binary + """ + text: String +} + +""" +A special type of user which takes actions on behalf of GitHub Apps. +""" +type Bot implements Actor & Node & UniformResourceLocatable { + """ + A URL pointing to the GitHub App's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the Bot object + """ + id: ID! + + """ + The username of the actor. + """ + login: String! + + """ + The HTTP path for this bot + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this bot + """ + url: URI! +} + +""" +Used when either Bot or User are accepted. +""" +union BotOrUser = Bot | User + +""" +Types which can be actors for `BranchActorAllowance` objects. +""" +union BranchActorAllowanceActor = App | Team | User + +""" +Parameters to be used for the branch_name_pattern rule +""" +type BranchNamePatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the branch_name_pattern rule +""" +input BranchNamePatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +A branch protection rule. +""" +type BranchProtectionRule implements Node { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean! + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean! + + """ + Is branch creation a protected operation. + """ + blocksCreations: Boolean! + + """ + A list of conflicts matching branches protection rule and other branch protection rules + """ + branchProtectionRuleConflicts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BranchProtectionRuleConflictConnection! + + """ + A list of actors able to force push for this branch protection rule. + """ + bypassForcePushAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BypassForcePushAllowanceConnection! + + """ + A list of actors able to bypass PRs for this branch protection rule. + """ + bypassPullRequestAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BypassPullRequestAllowanceConnection! + + """ + The actor who created this branch protection rule. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean! + + """ + The Node ID of the BranchProtectionRule object + """ + id: ID! + + """ + Can admins override branch protection. + """ + isAdminEnforced: Boolean! + + """ + Whether users can pull changes from upstream when the branch is locked. Set to + `true` to allow fork syncing. Set to `false` to prevent fork syncing. + """ + lockAllowsFetchAndMerge: Boolean! + + """ + Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. + """ + lockBranch: Boolean! + + """ + Repository refs that are protected by this rule + """ + matchingRefs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters refs with query on name + """ + query: String + ): RefConnection! + + """ + Identifies the protection rule pattern. + """ + pattern: String! + + """ + A list push allowances for this branch protection rule. + """ + pushAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PushAllowanceConnection! + + """ + The repository associated with this branch protection rule. + """ + repository: Repository + + """ + Whether the most recent push must be approved by someone other than the person who pushed it + """ + requireLastPushApproval: Boolean! + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + List of required deployment environments that must be deployed successfully to update matching branches + """ + requiredDeploymentEnvironments: [String] + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String] + + """ + List of required status checks that must pass for commits to be accepted to matching branches. + """ + requiredStatusChecks: [RequiredStatusCheckDescription!] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean! + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean! + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean! + + """ + Are conversations required to be resolved before merging. + """ + requiresConversationResolution: Boolean! + + """ + Does this branch require deployment to specific environments before merging + """ + requiresDeployments: Boolean! + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean! + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean! + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean! + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean! + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean! + + """ + A list review dismissal allowances for this branch protection rule. + """ + reviewDismissalAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReviewDismissalAllowanceConnection! +} + +""" +A conflict between two branch protection rules. +""" +type BranchProtectionRuleConflict { + """ + Identifies the branch protection rule. + """ + branchProtectionRule: BranchProtectionRule + + """ + Identifies the conflicting branch protection rule. + """ + conflictingBranchProtectionRule: BranchProtectionRule + + """ + Identifies the branch ref that has conflicting rules + """ + ref: Ref +} + +""" +The connection type for BranchProtectionRuleConflict. +""" +type BranchProtectionRuleConflictConnection { + """ + A list of edges. + """ + edges: [BranchProtectionRuleConflictEdge] + + """ + A list of nodes. + """ + nodes: [BranchProtectionRuleConflict] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BranchProtectionRuleConflictEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BranchProtectionRuleConflict +} + +""" +The connection type for BranchProtectionRule. +""" +type BranchProtectionRuleConnection { + """ + A list of edges. + """ + edges: [BranchProtectionRuleEdge] + + """ + A list of nodes. + """ + nodes: [BranchProtectionRule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BranchProtectionRuleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BranchProtectionRule +} + +""" +Information about a sponsorship to make for a user or organization with a GitHub +Sponsors profile, as part of sponsoring many users or organizations at once. +""" +input BulkSponsorship { + """ + The amount to pay to the sponsorable in US dollars. Valid values: 1-12000. + """ + amount: Int! + + """ + The ID of the user or organization who is receiving the sponsorship. Required if sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who is receiving the sponsorship. Required if sponsorableId is not given. + """ + sponsorableLogin: String +} + +""" +Types that can represent a repository ruleset bypass actor. +""" +union BypassActor = App | Team + +""" +A user, team, or app who has the ability to bypass a force push requirement on a protected branch. +""" +type BypassForcePushAllowance implements Node { + """ + The actor that can force push. + """ + actor: BranchActorAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user, team, or app. + """ + branchProtectionRule: BranchProtectionRule + + """ + The Node ID of the BypassForcePushAllowance object + """ + id: ID! +} + +""" +The connection type for BypassForcePushAllowance. +""" +type BypassForcePushAllowanceConnection { + """ + A list of edges. + """ + edges: [BypassForcePushAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [BypassForcePushAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BypassForcePushAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BypassForcePushAllowance +} + +""" +A user, team, or app who has the ability to bypass a pull request requirement on a protected branch. +""" +type BypassPullRequestAllowance implements Node { + """ + The actor that can bypass. + """ + actor: BranchActorAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user, team, or app. + """ + branchProtectionRule: BranchProtectionRule + + """ + The Node ID of the BypassPullRequestAllowance object + """ + id: ID! +} + +""" +The connection type for BypassPullRequestAllowance. +""" +type BypassPullRequestAllowanceConnection { + """ + A list of edges. + """ + edges: [BypassPullRequestAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [BypassPullRequestAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BypassPullRequestAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BypassPullRequestAllowance +} + +""" +The Common Vulnerability Scoring System +""" +type CVSS { + """ + The CVSS score associated with this advisory + """ + score: Float! + + """ + The CVSS vector string associated with this advisory + """ + vectorString: String +} + +""" +A common weakness enumeration +""" +type CWE implements Node { + """ + The id of the CWE + """ + cweId: String! + + """ + A detailed description of this CWE + """ + description: String! + + """ + The Node ID of the CWE object + """ + id: ID! + + """ + The name of this CWE + """ + name: String! +} + +""" +The connection type for CWE. +""" +type CWEConnection { + """ + A list of edges. + """ + edges: [CWEEdge] + + """ + A list of nodes. + """ + nodes: [CWE] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CWEEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CWE +} + +""" +Autogenerated input type of CancelEnterpriseAdminInvitation +""" +input CancelEnterpriseAdminInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pending enterprise administrator invitation. + """ + invitationId: ID! @possibleTypes(concreteTypes: ["EnterpriseAdministratorInvitation"]) +} + +""" +Autogenerated return type of CancelEnterpriseAdminInvitation. +""" +type CancelEnterpriseAdminInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The invitation that was canceled. + """ + invitation: EnterpriseAdministratorInvitation + + """ + A message confirming the result of canceling an administrator invitation. + """ + message: String +} + +""" +Autogenerated input type of CancelEnterpriseMemberInvitation +""" +input CancelEnterpriseMemberInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pending enterprise member invitation. + """ + invitationId: ID! @possibleTypes(concreteTypes: ["EnterpriseMemberInvitation"]) +} + +""" +Autogenerated return type of CancelEnterpriseMemberInvitation. +""" +type CancelEnterpriseMemberInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The invitation that was canceled. + """ + invitation: EnterpriseMemberInvitation + + """ + A message confirming the result of canceling an member invitation. + """ + message: String +} + +""" +Autogenerated input type of CancelSponsorship +""" +input CancelSponsorshipInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the user or organization who is acting as the sponsor, paying for + the sponsorship. Required if sponsorLogin is not given. + """ + sponsorId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsor") + + """ + The username of the user or organization who is acting as the sponsor, paying + for the sponsorship. Required if sponsorId is not given. + """ + sponsorLogin: String + + """ + The ID of the user or organization who is receiving the sponsorship. Required if sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who is receiving the sponsorship. Required if sponsorableId is not given. + """ + sponsorableLogin: String +} + +""" +Autogenerated return type of CancelSponsorship. +""" +type CancelSponsorshipPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The tier that was being used at the time of cancellation. + """ + sponsorsTier: SponsorsTier +} + +""" +Autogenerated input type of ChangeUserStatus +""" +input ChangeUserStatusInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The emoji to represent your status. Can either be a native Unicode emoji or an emoji name with colons, e.g., :grinning:. + """ + emoji: String + + """ + If set, the user status will not be shown after this date. + """ + expiresAt: DateTime + + """ + Whether this status should indicate you are not fully available on GitHub, e.g., you are away. + """ + limitedAvailability: Boolean = false + + """ + A short description of your current status. + """ + message: String + + """ + The ID of the organization whose members will be allowed to see the status. If + omitted, the status will be publicly visible. + """ + organizationId: ID @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of ChangeUserStatus. +""" +type ChangeUserStatusPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Your updated status. + """ + status: UserStatus +} + +""" +A single check annotation. +""" +type CheckAnnotation { + """ + The annotation's severity level. + """ + annotationLevel: CheckAnnotationLevel + + """ + The path to the file that this annotation was made on. + """ + blobUrl: URI! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The position of this annotation. + """ + location: CheckAnnotationSpan! + + """ + The annotation's message. + """ + message: String! + + """ + The path that this annotation was made on. + """ + path: String! + + """ + Additional information about the annotation. + """ + rawDetails: String + + """ + The annotation's title + """ + title: String +} + +""" +The connection type for CheckAnnotation. +""" +type CheckAnnotationConnection { + """ + A list of edges. + """ + edges: [CheckAnnotationEdge] + + """ + A list of nodes. + """ + nodes: [CheckAnnotation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Information from a check run analysis to specific lines of code. +""" +input CheckAnnotationData { + """ + Represents an annotation's information level + """ + annotationLevel: CheckAnnotationLevel! + + """ + The location of the annotation + """ + location: CheckAnnotationRange! + + """ + A short description of the feedback for these lines of code. + """ + message: String! + + """ + The path of the file to add an annotation to. + """ + path: String! + + """ + Details about this annotation. + """ + rawDetails: String + + """ + The title that represents the annotation. + """ + title: String +} + +""" +An edge in a connection. +""" +type CheckAnnotationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckAnnotation +} + +""" +Represents an annotation's information level. +""" +enum CheckAnnotationLevel { + """ + An annotation indicating an inescapable error. + """ + FAILURE + + """ + An annotation indicating some information. + """ + NOTICE + + """ + An annotation indicating an ignorable error. + """ + WARNING +} + +""" +A character position in a check annotation. +""" +type CheckAnnotationPosition { + """ + Column number (1 indexed). + """ + column: Int + + """ + Line number (1 indexed). + """ + line: Int! +} + +""" +Information from a check run analysis to specific lines of code. +""" +input CheckAnnotationRange { + """ + The ending column of the range. + """ + endColumn: Int + + """ + The ending line of the range. + """ + endLine: Int! + + """ + The starting column of the range. + """ + startColumn: Int + + """ + The starting line of the range. + """ + startLine: Int! +} + +""" +An inclusive pair of positions for a check annotation. +""" +type CheckAnnotationSpan { + """ + End position (inclusive). + """ + end: CheckAnnotationPosition! + + """ + Start position (inclusive). + """ + start: CheckAnnotationPosition! +} + +""" +The possible states for a check suite or run conclusion. +""" +enum CheckConclusionState { + """ + The check suite or run requires action. + """ + ACTION_REQUIRED + + """ + The check suite or run has been cancelled. + """ + CANCELLED + + """ + The check suite or run has failed. + """ + FAILURE + + """ + The check suite or run was neutral. + """ + NEUTRAL + + """ + The check suite or run was skipped. + """ + SKIPPED + + """ + The check suite or run was marked stale by GitHub. Only GitHub can use this conclusion. + """ + STALE + + """ + The check suite or run has failed at startup. + """ + STARTUP_FAILURE + + """ + The check suite or run has succeeded. + """ + SUCCESS + + """ + The check suite or run has timed out. + """ + TIMED_OUT +} + +""" +A check run. +""" +type CheckRun implements Node & RequirableByPullRequest & UniformResourceLocatable { + """ + The check run's annotations + """ + annotations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckAnnotationConnection + + """ + The check suite that this run is a part of. + """ + checkSuite: CheckSuite! + + """ + Identifies the date and time when the check run was completed. + """ + completedAt: DateTime + + """ + The conclusion of the check run. + """ + conclusion: CheckConclusionState + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The corresponding deployment for this job, if any + """ + deployment: Deployment + + """ + The URL from which to find full details of the check run on the integrator's site. + """ + detailsUrl: URI + + """ + A reference for the check run on the integrator's system. + """ + externalId: String + + """ + The Node ID of the CheckRun object + """ + id: ID! + + """ + Whether this is required to pass before merging for a specific pull request. + """ + isRequired( + """ + The id of the pull request this is required for + """ + pullRequestId: ID + + """ + The number of the pull request this is required for + """ + pullRequestNumber: Int + ): Boolean! + + """ + The name of the check for this check run. + """ + name: String! + + """ + Information about a pending deployment, if any, in this check run + """ + pendingDeploymentRequest: DeploymentRequest + + """ + The permalink to the check run summary. + """ + permalink: URI! + + """ + The repository associated with this check run. + """ + repository: Repository! + + """ + The HTTP path for this check run. + """ + resourcePath: URI! + + """ + Identifies the date and time when the check run was started. + """ + startedAt: DateTime + + """ + The current status of the check run. + """ + status: CheckStatusState! + + """ + The check run's steps + """ + steps( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Step number + """ + number: Int + ): CheckStepConnection + + """ + A string representing the check run's summary + """ + summary: String + + """ + A string representing the check run's text + """ + text: String + + """ + A string representing the check run + """ + title: String + + """ + The HTTP URL for this check run. + """ + url: URI! +} + +""" +Possible further actions the integrator can perform. +""" +input CheckRunAction { + """ + A short explanation of what this action would do. + """ + description: String! + + """ + A reference for the action on the integrator's system. + """ + identifier: String! + + """ + The text to be displayed on a button in the web UI. + """ + label: String! +} + +""" +The connection type for CheckRun. +""" +type CheckRunConnection { + """ + A list of edges. + """ + edges: [CheckRunEdge] + + """ + A list of nodes. + """ + nodes: [CheckRun] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CheckRunEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckRun +} + +""" +The filters that are available when fetching check runs. +""" +input CheckRunFilter { + """ + Filters the check runs created by this application ID. + """ + appId: Int + + """ + Filters the check runs by this name. + """ + checkName: String + + """ + Filters the check runs by this type. + """ + checkType: CheckRunType + + """ + Filters the check runs by these conclusions. + """ + conclusions: [CheckConclusionState!] + + """ + Filters the check runs by this status. Superceded by statuses. + """ + status: CheckStatusState + + """ + Filters the check runs by this status. Overrides status. + """ + statuses: [CheckStatusState!] +} + +""" +Descriptive details about the check run. +""" +input CheckRunOutput { + """ + The annotations that are made as part of the check run. + """ + annotations: [CheckAnnotationData!] + + """ + Images attached to the check run output displayed in the GitHub pull request UI. + """ + images: [CheckRunOutputImage!] + + """ + The summary of the check run (supports Commonmark). + """ + summary: String! + + """ + The details of the check run (supports Commonmark). + """ + text: String + + """ + A title to provide for this check run. + """ + title: String! +} + +""" +Images attached to the check run output displayed in the GitHub pull request UI. +""" +input CheckRunOutputImage { + """ + The alternative text for the image. + """ + alt: String! + + """ + A short image description. + """ + caption: String + + """ + The full URL of the image. + """ + imageUrl: URI! +} + +""" +The possible states of a check run in a status rollup. +""" +enum CheckRunState { + """ + The check run requires action. + """ + ACTION_REQUIRED + + """ + The check run has been cancelled. + """ + CANCELLED + + """ + The check run has been completed. + """ + COMPLETED + + """ + The check run has failed. + """ + FAILURE + + """ + The check run is in progress. + """ + IN_PROGRESS + + """ + The check run was neutral. + """ + NEUTRAL + + """ + The check run is in pending state. + """ + PENDING + + """ + The check run has been queued. + """ + QUEUED + + """ + The check run was skipped. + """ + SKIPPED + + """ + The check run was marked stale by GitHub. Only GitHub can use this conclusion. + """ + STALE + + """ + The check run has failed at startup. + """ + STARTUP_FAILURE + + """ + The check run has succeeded. + """ + SUCCESS + + """ + The check run has timed out. + """ + TIMED_OUT + + """ + The check run is in waiting state. + """ + WAITING +} + +""" +Represents a count of the state of a check run. +""" +type CheckRunStateCount { + """ + The number of check runs with this state. + """ + count: Int! + + """ + The state of a check run. + """ + state: CheckRunState! +} + +""" +The possible types of check runs. +""" +enum CheckRunType { + """ + Every check run available. + """ + ALL + + """ + The latest check run. + """ + LATEST +} + +""" +The possible states for a check suite or run status. +""" +enum CheckStatusState { + """ + The check suite or run has been completed. + """ + COMPLETED + + """ + The check suite or run is in progress. + """ + IN_PROGRESS + + """ + The check suite or run is in pending state. + """ + PENDING + + """ + The check suite or run has been queued. + """ + QUEUED + + """ + The check suite or run has been requested. + """ + REQUESTED + + """ + The check suite or run is in waiting state. + """ + WAITING +} + +""" +A single check step. +""" +type CheckStep { + """ + Identifies the date and time when the check step was completed. + """ + completedAt: DateTime + + """ + The conclusion of the check step. + """ + conclusion: CheckConclusionState + + """ + A reference for the check step on the integrator's system. + """ + externalId: String + + """ + The step's name. + """ + name: String! + + """ + The index of the step in the list of steps of the parent check run. + """ + number: Int! + + """ + Number of seconds to completion. + """ + secondsToCompletion: Int + + """ + Identifies the date and time when the check step was started. + """ + startedAt: DateTime + + """ + The current status of the check step. + """ + status: CheckStatusState! +} + +""" +The connection type for CheckStep. +""" +type CheckStepConnection { + """ + A list of edges. + """ + edges: [CheckStepEdge] + + """ + A list of nodes. + """ + nodes: [CheckStep] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CheckStepEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckStep +} + +""" +A check suite. +""" +type CheckSuite implements Node { + """ + The GitHub App which created this check suite. + """ + app: App + + """ + The name of the branch for this check suite. + """ + branch: Ref + + """ + The check runs associated with a check suite. + """ + checkRuns( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filters the check runs by this type. + """ + filterBy: CheckRunFilter + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckRunConnection + + """ + The commit for this check suite + """ + commit: Commit! + + """ + The conclusion of this check suite. + """ + conclusion: CheckConclusionState + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who triggered the check suite. + """ + creator: User + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the CheckSuite object + """ + id: ID! + + """ + A list of open pull requests matching the check suite. + """ + matchingPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection + + """ + The push that triggered this check suite. + """ + push: Push + + """ + The repository associated with this check suite. + """ + repository: Repository! + + """ + The HTTP path for this check suite + """ + resourcePath: URI! + + """ + The status of this check suite. + """ + status: CheckStatusState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this check suite + """ + url: URI! + + """ + The workflow run associated with this check suite. + """ + workflowRun: WorkflowRun +} + +""" +The auto-trigger preferences that are available for check suites. +""" +input CheckSuiteAutoTriggerPreference { + """ + The node ID of the application that owns the check suite. + """ + appId: ID! + + """ + Set to `true` to enable automatic creation of CheckSuite events upon pushes to the repository. + """ + setting: Boolean! +} + +""" +The connection type for CheckSuite. +""" +type CheckSuiteConnection { + """ + A list of edges. + """ + edges: [CheckSuiteEdge] + + """ + A list of nodes. + """ + nodes: [CheckSuite] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CheckSuiteEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckSuite +} + +""" +The filters that are available when fetching check suites. +""" +input CheckSuiteFilter { + """ + Filters the check suites created by this application ID. + """ + appId: Int + + """ + Filters the check suites by this name. + """ + checkName: String +} + +""" +An object which can have its data claimed or claim data from another. +""" +union Claimable = Mannequin | User + +""" +Autogenerated input type of ClearLabelsFromLabelable +""" +input ClearLabelsFromLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the labelable object to clear the labels from. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of ClearLabelsFromLabelable. +""" +type ClearLabelsFromLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was unlabeled. + """ + labelable: Labelable +} + +""" +Autogenerated input type of ClearProjectV2ItemFieldValue +""" +input ClearProjectV2ItemFieldValueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the field to be cleared. + """ + fieldId: ID! + @possibleTypes( + concreteTypes: ["ProjectV2Field", "ProjectV2IterationField", "ProjectV2SingleSelectField"] + abstractType: "ProjectV2FieldConfiguration" + ) + + """ + The ID of the item to be cleared. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of ClearProjectV2ItemFieldValue. +""" +type ClearProjectV2ItemFieldValuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated item. + """ + projectV2Item: ProjectV2Item +} + +""" +Autogenerated input type of CloneProject +""" +input CloneProjectInput { + """ + The description of the project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not to clone the source project's workflows. + """ + includeWorkflows: Boolean! + + """ + The name of the project. + """ + name: String! + + """ + The visibility of the project, defaults to false (private). + """ + public: Boolean + + """ + The source project to clone. + """ + sourceId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The owner ID to create the project under. + """ + targetOwnerId: ID! @possibleTypes(concreteTypes: ["Organization", "Repository", "User"], abstractType: "ProjectOwner") +} + +""" +Autogenerated return type of CloneProject. +""" +type CloneProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the JobStatus for populating cloned fields. + """ + jobStatusId: String + + """ + The new cloned project. + """ + project: Project +} + +""" +Autogenerated input type of CloneTemplateRepository +""" +input CloneTemplateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A short description of the new repository. + """ + description: String + + """ + Whether to copy all branches from the template to the new repository. Defaults + to copying only the default branch of the template. + """ + includeAllBranches: Boolean = false + + """ + The name of the new repository. + """ + name: String! + + """ + The ID of the owner for the new repository. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "RepositoryOwner") + + """ + The Node ID of the template repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! +} + +""" +Autogenerated return type of CloneTemplateRepository. +""" +type CloneTemplateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new repository. + """ + repository: Repository +} + +""" +An object that can be closed +""" +interface Closable { + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! +} + +""" +Autogenerated input type of CloseDiscussion +""" +input CloseDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the discussion to be closed. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["Discussion"]) + + """ + The reason why the discussion is being closed. + """ + reason: DiscussionCloseReason = RESOLVED +} + +""" +Autogenerated return type of CloseDiscussion. +""" +type CloseDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that was closed. + """ + discussion: Discussion +} + +""" +Autogenerated input type of CloseIssue +""" +input CloseIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue that this is a duplicate of. + """ + duplicateIssueId: ID @possibleTypes(concreteTypes: ["Issue"]) + + """ + ID of the issue to be closed. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The reason the issue is to be closed. + """ + stateReason: IssueClosedStateReason +} + +""" +Autogenerated return type of CloseIssue. +""" +type CloseIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was closed. + """ + issue: Issue +} + +""" +Autogenerated input type of ClosePullRequest +""" +input ClosePullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be closed. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of ClosePullRequest. +""" +type ClosePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was closed. + """ + pullRequest: PullRequest +} + +""" +Represents a 'closed' event on any `Closable`. +""" +type ClosedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Object that was closed. + """ + closable: Closable! + + """ + Object which triggered the creation of this event. + """ + closer: Closer + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ClosedEvent object + """ + id: ID! + + """ + The HTTP path for this closed event. + """ + resourcePath: URI! + + """ + The reason the issue state was changed to closed. + """ + stateReason: IssueStateReason + + """ + The HTTP URL for this closed event. + """ + url: URI! +} + +""" +The object which triggered a `ClosedEvent`. +""" +union Closer = Commit | ProjectV2 | PullRequest + +""" +The Code of Conduct for a repository +""" +type CodeOfConduct implements Node { + """ + The body of the Code of Conduct + """ + body: String + + """ + The Node ID of the CodeOfConduct object + """ + id: ID! + + """ + The key for the Code of Conduct + """ + key: String! + + """ + The formal name of the Code of Conduct + """ + name: String! + + """ + The HTTP path for this Code of Conduct + """ + resourcePath: URI + + """ + The HTTP URL for this Code of Conduct + """ + url: URI +} + +""" +Choose which tools must provide code scanning results before the reference is +updated. When configured, code scanning must be enabled and have results for +both the commit and the reference being updated. +""" +type CodeScanningParameters { + """ + Tools that must provide code scanning results for this rule to pass. + """ + codeScanningTools: [CodeScanningTool!]! +} + +""" +Choose which tools must provide code scanning results before the reference is +updated. When configured, code scanning must be enabled and have results for +both the commit and the reference being updated. +""" +input CodeScanningParametersInput { + """ + Tools that must provide code scanning results for this rule to pass. + """ + codeScanningTools: [CodeScanningToolInput!]! +} + +""" +A tool that must provide code scanning results for this rule to pass. +""" +type CodeScanningTool { + """ + The severity level at which code scanning results that raise alerts block a + reference update. For more information on alert severity levels, see "[About code scanning alerts](${externalDocsUrl}/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels)." + """ + alertsThreshold: String! + + """ + The severity level at which code scanning results that raise security alerts + block a reference update. For more information on security severity levels, + see "[About code scanning alerts](${externalDocsUrl}/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels)." + """ + securityAlertsThreshold: String! + + """ + The name of a code scanning tool + """ + tool: String! +} + +""" +A tool that must provide code scanning results for this rule to pass. +""" +input CodeScanningToolInput { + """ + The severity level at which code scanning results that raise alerts block a + reference update. For more information on alert severity levels, see "[About code scanning alerts](${externalDocsUrl}/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels)." + """ + alertsThreshold: String! + + """ + The severity level at which code scanning results that raise security alerts + block a reference update. For more information on security severity levels, + see "[About code scanning alerts](${externalDocsUrl}/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels)." + """ + securityAlertsThreshold: String! + + """ + The name of a code scanning tool + """ + tool: String! +} + +""" +Collaborators affiliation level with a subject. +""" +enum CollaboratorAffiliation { + """ + All collaborators the authenticated user can see. + """ + ALL + + """ + All collaborators with permissions to an organization-owned subject, regardless of organization membership status. + """ + DIRECT + + """ + All outside collaborators of an organization-owned subject. + """ + OUTSIDE +} + +""" +Represents a comment. +""" +interface Comment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The Node ID of the Comment object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +A comment author association with repository. +""" +enum CommentAuthorAssociation { + """ + Author has been invited to collaborate on the repository. + """ + COLLABORATOR + + """ + Author has previously committed to the repository. + """ + CONTRIBUTOR + + """ + Author has not previously committed to GitHub. + """ + FIRST_TIMER + + """ + Author has not previously committed to the repository. + """ + FIRST_TIME_CONTRIBUTOR + + """ + Author is a placeholder for an unclaimed user. + """ + MANNEQUIN + + """ + Author is a member of the organization that owns the repository. + """ + MEMBER + + """ + Author has no association with the repository. + """ + NONE + + """ + Author is the owner of the repository. + """ + OWNER +} + +""" +The possible errors that will prevent a user from updating a comment. +""" +enum CommentCannotUpdateReason { + """ + Unable to create comment because repository is archived. + """ + ARCHIVED + + """ + You cannot update this comment + """ + DENIED + + """ + You must be the author or have write access to this repository to update this comment. + """ + INSUFFICIENT_ACCESS + + """ + Unable to create comment because issue is locked. + """ + LOCKED + + """ + You must be logged in to update this comment. + """ + LOGIN_REQUIRED + + """ + Repository is under maintenance. + """ + MAINTENANCE + + """ + At least one email address must be verified to update this comment. + """ + VERIFIED_EMAIL_REQUIRED +} + +""" +Represents a 'comment_deleted' event on a given issue or pull request. +""" +type CommentDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The user who authored the deleted comment. + """ + deletedCommentAuthor: Actor + + """ + The Node ID of the CommentDeletedEvent object + """ + id: ID! +} + +""" +Represents a Git commit. +""" +type Commit implements GitObject & Node & Subscribable & UniformResourceLocatable { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The number of additions in this commit. + """ + additions: Int! + + """ + The merged Pull Request that introduced the commit to the repository. If the + commit is not present in the default branch, additionally returns open Pull + Requests associated with the commit + """ + associatedPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests. + """ + orderBy: PullRequestOrder = {field: CREATED_AT, direction: ASC} + ): PullRequestConnection + + """ + Authorship details of the commit. + """ + author: GitActor + + """ + Check if the committer and the author match. + """ + authoredByCommitter: Boolean! + + """ + The datetime when this commit was authored. + """ + authoredDate: DateTime! + + """ + The list of authors for this commit based on the git author and the Co-authored-by + message trailer. The git author will always be first. + """ + authors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GitActorConnection! + + """ + Fetches `git blame` information. + """ + blame( + """ + The file whose Git blame information you want. + """ + path: String! + ): Blame! + + """ + We recommend using the `changedFilesIfAvailable` field instead of + `changedFiles`, as `changedFiles` will cause your request to return an error + if GitHub is unable to calculate the number of changed files. + """ + changedFiles: Int! + @deprecated( + reason: "`changedFiles` will be removed. Use `changedFilesIfAvailable` instead. Removal on 2023-01-01 UTC." + ) + + """ + The number of changed files in this commit. If GitHub is unable to calculate + the number of changed files (for example due to a timeout), this will return + `null`. We recommend using this field instead of `changedFiles`. + """ + changedFilesIfAvailable: Int + + """ + The check suites associated with a commit. + """ + checkSuites( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filters the check suites by this type. + """ + filterBy: CheckSuiteFilter + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckSuiteConnection + + """ + Comments made on the commit. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + The datetime when this commit was committed. + """ + committedDate: DateTime! + + """ + Check if committed via GitHub web UI. + """ + committedViaWeb: Boolean! + + """ + Committer details of the commit. + """ + committer: GitActor + + """ + The number of deletions in this commit. + """ + deletions: Int! + + """ + The deployments associated with a commit. + """ + deployments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Environments to list deployments for + """ + environments: [String!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for deployments returned from the connection. + """ + orderBy: DeploymentOrder = {field: CREATED_AT, direction: ASC} + ): DeploymentConnection + + """ + The tree entry representing the file located at the given path. + """ + file( + """ + The path for the file + """ + path: String! + ): TreeEntry + + """ + The linear commit history starting from (and including) this commit, in the same order as `git log`. + """ + history( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + If non-null, filters history to only show commits with matching authorship. + """ + author: CommitAuthor + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If non-null, filters history to only show commits touching files under this path. + """ + path: String + + """ + Allows specifying a beginning time or date for fetching commits. Unexpected + results may be returned for dates not between 1970-01-01 and 2099-12-13 (inclusive). + """ + since: GitTimestamp + + """ + Allows specifying an ending time or date for fetching commits. Unexpected + results may be returned for dates not between 1970-01-01 and 2099-12-13 (inclusive). + """ + until: GitTimestamp + ): CommitHistoryConnection! + + """ + The Node ID of the Commit object + """ + id: ID! + + """ + The Git commit message + """ + message: String! + + """ + The Git commit message body + """ + messageBody: String! + + """ + The commit message body rendered to HTML. + """ + messageBodyHTML: HTML! + + """ + The Git commit message headline + """ + messageHeadline: String! + + """ + The commit message headline rendered to HTML. + """ + messageHeadlineHTML: HTML! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The organization this commit was made on behalf of. + """ + onBehalfOf: Organization + + """ + The parents of a commit. + """ + parents( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitConnection! + + """ + The datetime when this commit was pushed. + """ + pushedDate: DateTime @deprecated(reason: "`pushedDate` is no longer supported. Removal on 2023-07-01 UTC.") + + """ + The Repository this commit belongs to + """ + repository: Repository! + + """ + The HTTP path for this commit + """ + resourcePath: URI! + + """ + Commit signing information, if present. + """ + signature: GitSignature + + """ + Status information for this commit + """ + status: Status + + """ + Check and Status rollup information for this commit. + """ + statusCheckRollup: StatusCheckRollup + + """ + Returns a list of all submodules in this repository as of this Commit parsed from the .gitmodules file. + """ + submodules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SubmoduleConnection! + + """ + Returns a URL to download a tarball archive for a repository. + Note: For private repositories, these links are temporary and expire after five minutes. + """ + tarballUrl: URI! + + """ + Commit's root Tree + """ + tree: Tree! + + """ + The HTTP path for the tree of this commit + """ + treeResourcePath: URI! + + """ + The HTTP URL for the tree of this commit + """ + treeUrl: URI! + + """ + The HTTP URL for this commit + """ + url: URI! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState + + """ + Returns a URL to download a zipball archive for a repository. + Note: For private repositories, these links are temporary and expire after five minutes. + """ + zipballUrl: URI! +} + +""" +Specifies an author for filtering Git commits. +""" +input CommitAuthor { + """ + Email addresses to filter by. Commits authored by any of the specified email addresses will be returned. + """ + emails: [String!] + + """ + ID of a User to filter by. If non-null, only commits authored by this user + will be returned. This field takes precedence over emails. + """ + id: ID +} + +""" +Parameters to be used for the commit_author_email_pattern rule +""" +type CommitAuthorEmailPatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the commit_author_email_pattern rule +""" +input CommitAuthorEmailPatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Represents a comment on a given Commit. +""" +type CommitComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the comment body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the commit associated with the comment, if the commit exists. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The Node ID of the CommitComment object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies the file path associated with the comment. + """ + path: String + + """ + Identifies the line position associated with the comment. + """ + position: Int + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this commit comment. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this commit comment. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for CommitComment. +""" +type CommitCommentConnection { + """ + A list of edges. + """ + edges: [CommitCommentEdge] + + """ + A list of nodes. + """ + nodes: [CommitComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CommitCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CommitComment +} + +""" +A thread of comments on a commit. +""" +type CommitCommentThread implements Node & RepositoryNode { + """ + The comments that exist in this thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The commit the comments were made on. + """ + commit: Commit + + """ + The Node ID of the CommitCommentThread object + """ + id: ID! + + """ + The file the comments were made on. + """ + path: String + + """ + The position in the diff for the commit that the comment was made on. + """ + position: Int + + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +The connection type for Commit. +""" +type CommitConnection { + """ + A list of edges. + """ + edges: [CommitEdge] + + """ + A list of nodes. + """ + nodes: [Commit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Ordering options for commit contribution connections. +""" +input CommitContributionOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field by which to order commit contributions. + """ + field: CommitContributionOrderField! +} + +""" +Properties by which commit contribution connections can be ordered. +""" +enum CommitContributionOrderField { + """ + Order commit contributions by how many commits they represent. + """ + COMMIT_COUNT + + """ + Order commit contributions by when they were made. + """ + OCCURRED_AT +} + +""" +This aggregates commits made by a user within one repository. +""" +type CommitContributionsByRepository { + """ + The commit contributions, each representing a day. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for commit contributions returned from the connection. + """ + orderBy: CommitContributionOrder = {field: OCCURRED_AT, direction: DESC} + ): CreatedCommitContributionConnection! + + """ + The repository in which the commits were made. + """ + repository: Repository! + + """ + The HTTP path for the user's commits to the repository in this time range. + """ + resourcePath: URI! + + """ + The HTTP URL for the user's commits to the repository in this time range. + """ + url: URI! +} + +""" +An edge in a connection. +""" +type CommitEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Commit +} + +""" +The connection type for Commit. +""" +type CommitHistoryConnection { + """ + A list of edges. + """ + edges: [CommitEdge] + + """ + A list of nodes. + """ + nodes: [Commit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A message to include with a new commit +""" +input CommitMessage { + """ + The body of the message. + """ + body: String + + """ + The headline of the message. + """ + headline: String! +} + +""" +Parameters to be used for the commit_message_pattern rule +""" +type CommitMessagePatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the commit_message_pattern rule +""" +input CommitMessagePatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +A git ref for a commit to be appended to. + +The ref must be a branch, i.e. its fully qualified name must start +with `refs/heads/` (although the input is not required to be fully +qualified). + +The Ref may be specified by its global node ID or by the +`repositoryNameWithOwner` and `branchName`. + +### Examples + +Specify a branch using a global node ID: + + { "id": "MDM6UmVmMTpyZWZzL2hlYWRzL21haW4=" } + +Specify a branch using `repositoryNameWithOwner` and `branchName`: + + { + "repositoryNameWithOwner": "github/graphql-client", + "branchName": "main" + } +""" +input CommittableBranch { + """ + The unqualified name of the branch to append the commit to. + """ + branchName: String + + """ + The Node ID of the Ref to be updated. + """ + id: ID + + """ + The nameWithOwner of the repository to commit to. + """ + repositoryNameWithOwner: String +} + +""" +Parameters to be used for the committer_email_pattern rule +""" +type CommitterEmailPatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the committer_email_pattern rule +""" +input CommitterEmailPatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Represents a comparison between two commit revisions. +""" +type Comparison implements Node { + """ + The number of commits ahead of the base branch. + """ + aheadBy: Int! + + """ + The base revision of this comparison. + """ + baseTarget: GitObject! + + """ + The number of commits behind the base branch. + """ + behindBy: Int! + + """ + The commits which compose this comparison. + """ + commits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ComparisonCommitConnection! + + """ + The head revision of this comparison. + """ + headTarget: GitObject! + + """ + The Node ID of the Comparison object + """ + id: ID! + + """ + The status of this comparison. + """ + status: ComparisonStatus! +} + +""" +The connection type for Commit. +""" +type ComparisonCommitConnection { + """ + The total count of authors and co-authors across all commits. + """ + authorCount: Int! + + """ + A list of edges. + """ + edges: [CommitEdge] + + """ + A list of nodes. + """ + nodes: [Commit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The status of a git comparison between two refs. +""" +enum ComparisonStatus { + """ + The head ref is ahead of the base ref. + """ + AHEAD + + """ + The head ref is behind the base ref. + """ + BEHIND + + """ + The head ref is both ahead and behind of the base ref, indicating git history has diverged. + """ + DIVERGED + + """ + The head ref and base ref are identical. + """ + IDENTICAL +} + +""" +Represents a 'connected' event on a given issue or pull request. +""" +type ConnectedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ConnectedEvent object + """ + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Issue or pull request that made the reference. + """ + source: ReferencedSubject! + + """ + Issue or pull request which was connected. + """ + subject: ReferencedSubject! +} + +""" +The Contributing Guidelines for a repository. +""" +type ContributingGuidelines { + """ + The body of the Contributing Guidelines. + """ + body: String + + """ + The HTTP path for the Contributing Guidelines. + """ + resourcePath: URI + + """ + The HTTP URL for the Contributing Guidelines. + """ + url: URI +} + +""" +Represents a contribution a user made on GitHub, such as opening an issue. +""" +interface Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +A calendar of contributions made on GitHub by a user. +""" +type ContributionCalendar { + """ + A list of hex color codes used in this calendar. The darker the color, the more contributions it represents. + """ + colors: [String!]! + + """ + Determine if the color set was chosen because it's currently Halloween. + """ + isHalloween: Boolean! + + """ + A list of the months of contributions in this calendar. + """ + months: [ContributionCalendarMonth!]! + + """ + The count of total contributions in the calendar. + """ + totalContributions: Int! + + """ + A list of the weeks of contributions in this calendar. + """ + weeks: [ContributionCalendarWeek!]! +} + +""" +Represents a single day of contributions on GitHub by a user. +""" +type ContributionCalendarDay { + """ + The hex color code that represents how many contributions were made on this day compared to others in the calendar. + """ + color: String! + + """ + How many contributions were made by the user on this day. + """ + contributionCount: Int! + + """ + Indication of contributions, relative to other days. Can be used to indicate + which color to represent this day on a calendar. + """ + contributionLevel: ContributionLevel! + + """ + The day this square represents. + """ + date: Date! + + """ + A number representing which day of the week this square represents, e.g., 1 is Monday. + """ + weekday: Int! +} + +""" +A month of contributions in a user's contribution graph. +""" +type ContributionCalendarMonth { + """ + The date of the first day of this month. + """ + firstDay: Date! + + """ + The name of the month. + """ + name: String! + + """ + How many weeks started in this month. + """ + totalWeeks: Int! + + """ + The year the month occurred in. + """ + year: Int! +} + +""" +A week of contributions in a user's contribution graph. +""" +type ContributionCalendarWeek { + """ + The days of contributions in this week. + """ + contributionDays: [ContributionCalendarDay!]! + + """ + The date of the earliest square in this week. + """ + firstDay: Date! +} + +""" +Varying levels of contributions from none to many. +""" +enum ContributionLevel { + """ + Lowest 25% of days of contributions. + """ + FIRST_QUARTILE + + """ + Highest 25% of days of contributions. More contributions than the third quartile. + """ + FOURTH_QUARTILE + + """ + No contributions occurred. + """ + NONE + + """ + Second lowest 25% of days of contributions. More contributions than the first quartile. + """ + SECOND_QUARTILE + + """ + Second highest 25% of days of contributions. More contributions than second quartile, less than the fourth quartile. + """ + THIRD_QUARTILE +} + +""" +Ordering options for contribution connections. +""" +input ContributionOrder { + """ + The ordering direction. + """ + direction: OrderDirection! +} + +""" +A contributions collection aggregates contributions such as opened issues and commits created by a user. +""" +type ContributionsCollection { + """ + Commit contributions made by the user, grouped by repository. + """ + commitContributionsByRepository( + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [CommitContributionsByRepository!]! + + """ + A calendar of this user's contributions on GitHub. + """ + contributionCalendar: ContributionCalendar! + + """ + The years the user has been making contributions with the most recent year first. + """ + contributionYears: [Int!]! + + """ + Determine if this collection's time span ends in the current month. + """ + doesEndInCurrentMonth: Boolean! + + """ + The date of the first restricted contribution the user made in this time + period. Can only be non-null when the user has enabled private contribution counts. + """ + earliestRestrictedContributionDate: Date + + """ + The ending date and time of this collection. + """ + endedAt: DateTime! + + """ + The first issue the user opened on GitHub. This will be null if that issue was + opened outside the collection's time range and ignoreTimeRange is false. If + the issue is not visible but the user has opted to show private contributions, + a RestrictedContribution will be returned. + """ + firstIssueContribution: CreatedIssueOrRestrictedContribution + + """ + The first pull request the user opened on GitHub. This will be null if that + pull request was opened outside the collection's time range and + ignoreTimeRange is not true. If the pull request is not visible but the user + has opted to show private contributions, a RestrictedContribution will be returned. + """ + firstPullRequestContribution: CreatedPullRequestOrRestrictedContribution + + """ + The first repository the user created on GitHub. This will be null if that + first repository was created outside the collection's time range and + ignoreTimeRange is false. If the repository is not visible, then a + RestrictedContribution is returned. + """ + firstRepositoryContribution: CreatedRepositoryOrRestrictedContribution + + """ + Does the user have any more activity in the timeline that occurred prior to the collection's time range? + """ + hasActivityInThePast: Boolean! + + """ + Determine if there are any contributions in this collection. + """ + hasAnyContributions: Boolean! + + """ + Determine if the user made any contributions in this time frame whose details + are not visible because they were made in a private repository. Can only be + true if the user enabled private contribution counts. + """ + hasAnyRestrictedContributions: Boolean! + + """ + Whether or not the collector's time span is all within the same day. + """ + isSingleDay: Boolean! + + """ + A list of issues the user opened. + """ + issueContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first issue ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from the result. + """ + excludePopular: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedIssueContributionConnection! + + """ + Issue contributions made by the user, grouped by repository. + """ + issueContributionsByRepository( + """ + Should the user's first issue ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from the result. + """ + excludePopular: Boolean = false + + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [IssueContributionsByRepository!]! + + """ + When the user signed up for GitHub. This will be null if that sign up date + falls outside the collection's time range and ignoreTimeRange is false. + """ + joinedGitHubContribution: JoinedGitHubContribution + + """ + The date of the most recent restricted contribution the user made in this time + period. Can only be non-null when the user has enabled private contribution counts. + """ + latestRestrictedContributionDate: Date + + """ + When this collection's time range does not include any activity from the user, use this + to get a different collection from an earlier time range that does have activity. + """ + mostRecentCollectionWithActivity: ContributionsCollection + + """ + Returns a different contributions collection from an earlier time range than this one + that does not have any contributions. + """ + mostRecentCollectionWithoutActivity: ContributionsCollection + + """ + The issue the user opened on GitHub that received the most comments in the specified + time frame. + """ + popularIssueContribution: CreatedIssueContribution + + """ + The pull request the user opened on GitHub that received the most comments in the + specified time frame. + """ + popularPullRequestContribution: CreatedPullRequestContribution + + """ + Pull request contributions made by the user. + """ + pullRequestContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first pull request ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from the result. + """ + excludePopular: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestContributionConnection! + + """ + Pull request contributions made by the user, grouped by repository. + """ + pullRequestContributionsByRepository( + """ + Should the user's first pull request ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from the result. + """ + excludePopular: Boolean = false + + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [PullRequestContributionsByRepository!]! + + """ + Pull request review contributions made by the user. Returns the most recently + submitted review for each PR reviewed by the user. + """ + pullRequestReviewContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestReviewContributionConnection! + + """ + Pull request review contributions made by the user, grouped by repository. + """ + pullRequestReviewContributionsByRepository( + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [PullRequestReviewContributionsByRepository!]! + + """ + A list of repositories owned by the user that the user created in this time range. + """ + repositoryContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first repository ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedRepositoryContributionConnection! + + """ + A count of contributions made by the user that the viewer cannot access. Only + non-zero when the user has chosen to share their private contribution counts. + """ + restrictedContributionsCount: Int! + + """ + The beginning date and time of this collection. + """ + startedAt: DateTime! + + """ + How many commits were made by the user in this time span. + """ + totalCommitContributions: Int! + + """ + How many issues the user opened. + """ + totalIssueContributions( + """ + Should the user's first issue ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many pull requests the user opened. + """ + totalPullRequestContributions( + """ + Should the user's first pull request ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many pull request reviews the user left. + """ + totalPullRequestReviewContributions: Int! + + """ + How many different repositories the user committed to. + """ + totalRepositoriesWithContributedCommits: Int! + + """ + How many different repositories the user opened issues in. + """ + totalRepositoriesWithContributedIssues( + """ + Should the user's first issue ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many different repositories the user left pull request reviews in. + """ + totalRepositoriesWithContributedPullRequestReviews: Int! + + """ + How many different repositories the user opened pull requests in. + """ + totalRepositoriesWithContributedPullRequests( + """ + Should the user's first pull request ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many repositories the user created. + """ + totalRepositoryContributions( + """ + Should the user's first repository ever be excluded from this count. + """ + excludeFirst: Boolean = false + ): Int! + + """ + The user who made the contributions in this collection. + """ + user: User! +} + +""" +Autogenerated input type of ConvertProjectCardNoteToIssue +""" +input ConvertProjectCardNoteToIssueInput { + """ + The body of the newly created issue. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ProjectCard ID to convert. + """ + projectCardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + The ID of the repository to create the issue in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title of the newly created issue. Defaults to the card's note text. + """ + title: String +} + +""" +Autogenerated return type of ConvertProjectCardNoteToIssue. +""" +type ConvertProjectCardNoteToIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated ProjectCard. + """ + projectCard: ProjectCard +} + +""" +Autogenerated input type of ConvertProjectV2DraftIssueItemToIssue +""" +input ConvertProjectV2DraftIssueItemToIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the draft issue ProjectV2Item to convert. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the repository to create the issue in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of ConvertProjectV2DraftIssueItemToIssue. +""" +type ConvertProjectV2DraftIssueItemToIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated project item. + """ + item: ProjectV2Item +} + +""" +Autogenerated input type of ConvertPullRequestToDraft +""" +input ConvertPullRequestToDraftInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to convert to draft + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of ConvertPullRequestToDraft. +""" +type ConvertPullRequestToDraftPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that is now a draft. + """ + pullRequest: PullRequest +} + +""" +Represents a 'convert_to_draft' event on a given pull request. +""" +type ConvertToDraftEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ConvertToDraftEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this convert to draft event. + """ + resourcePath: URI! + + """ + The HTTP URL for this convert to draft event. + """ + url: URI! +} + +""" +Represents a 'converted_note_to_issue' event on a given issue or pull request. +""" +type ConvertedNoteToIssueEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the ConvertedNoteToIssueEvent object + """ + id: ID! + + """ + Project referenced by event. + """ + project: Project + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Column name referenced by this project event. + """ + projectColumnName: String! +} + +""" +Represents a 'converted_to_discussion' event on a given issue. +""" +type ConvertedToDiscussionEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The discussion that the issue was converted into. + """ + discussion: Discussion + + """ + The Node ID of the ConvertedToDiscussionEvent object + """ + id: ID! +} + +""" +Copilot endpoint information +""" +type CopilotEndpoints { + """ + Copilot API endpoint + """ + api: String! + + """ + Copilot origin tracker endpoint + """ + originTracker: String! + + """ + Copilot proxy endpoint + """ + proxy: String! + + """ + Copilot telemetry endpoint + """ + telemetry: String! +} + +""" +Autogenerated input type of CopyProjectV2 +""" +input CopyProjectV2Input { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Include draft issues in the new project + """ + includeDraftIssues: Boolean = false + + """ + The owner ID of the new project. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "User"]) + + """ + The ID of the source Project to copy. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The title of the project. + """ + title: String! +} + +""" +Autogenerated return type of CopyProjectV2. +""" +type CopyProjectV2Payload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The copied project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of CreateAttributionInvitation +""" +input CreateAttributionInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the owner scoping the reattributable data. + """ + ownerId: ID! + @possibleTypes(concreteTypes: ["Bot", "Enterprise", "Mannequin", "Organization", "User"], abstractType: "Account") + + """ + The Node ID of the account owning the data to reattribute. + """ + sourceId: ID! + @possibleTypes(concreteTypes: ["Bot", "Enterprise", "Mannequin", "Organization", "User"], abstractType: "Account") + + """ + The Node ID of the account which may claim the data. + """ + targetId: ID! + @possibleTypes(concreteTypes: ["Bot", "Enterprise", "Mannequin", "Organization", "User"], abstractType: "Account") +} + +""" +Autogenerated return type of CreateAttributionInvitation. +""" +type CreateAttributionInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owner scoping the reattributable data. + """ + owner: Organization + + """ + The account owning the data to reattribute. + """ + source: Claimable + + """ + The account which may claim the data. + """ + target: Claimable +} + +""" +Autogenerated input type of CreateBranchProtectionRule +""" +input CreateBranchProtectionRuleInput { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean + + """ + Is branch creation a protected operation. + """ + blocksCreations: Boolean + + """ + A list of User, Team, or App IDs allowed to bypass force push targeting matching branches. + """ + bypassForcePushActorIds: [ID!] + + """ + A list of User, Team, or App IDs allowed to bypass pull requests targeting matching branches. + """ + bypassPullRequestActorIds: [ID!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean + + """ + Can admins override branch protection. + """ + isAdminEnforced: Boolean + + """ + Whether users can pull changes from upstream when the branch is locked. Set to + `true` to allow fork syncing. Set to `false` to prevent fork syncing. + """ + lockAllowsFetchAndMerge: Boolean + + """ + Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. + """ + lockBranch: Boolean + + """ + The glob-like pattern used to determine matching branches. + """ + pattern: String! + + """ + A list of User, Team, or App IDs allowed to push to matching branches. + """ + pushActorIds: [ID!] + + """ + The global relay id of the repository in which a new branch protection rule should be created in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Whether the most recent push must be approved by someone other than the person who pushed it + """ + requireLastPushApproval: Boolean + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + The list of required deployment environments + """ + requiredDeploymentEnvironments: [String!] + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String!] + + """ + The list of required status checks + """ + requiredStatusChecks: [RequiredStatusCheckInput!] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean + + """ + Are conversations required to be resolved before merging. + """ + requiresConversationResolution: Boolean + + """ + Are successful deployments required before merging. + """ + requiresDeployments: Boolean + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean + + """ + A list of User, Team, or App IDs allowed to dismiss reviews on pull requests targeting matching branches. + """ + reviewDismissalActorIds: [ID!] +} + +""" +Autogenerated return type of CreateBranchProtectionRule. +""" +type CreateBranchProtectionRulePayload { + """ + The newly created BranchProtectionRule. + """ + branchProtectionRule: BranchProtectionRule + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateCheckRun +""" +input CreateCheckRunInput { + """ + Possible further actions the integrator can perform, which a user may trigger. + """ + actions: [CheckRunAction!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The time that the check run finished. + """ + completedAt: DateTime + + """ + The final conclusion of the check. + """ + conclusion: CheckConclusionState + + """ + The URL of the integrator's site that has the full details of the check. + """ + detailsUrl: URI + + """ + A reference for the run on the integrator's system. + """ + externalId: String + + """ + The SHA of the head commit. + """ + headSha: GitObjectID! + + """ + The name of the check. + """ + name: String! + + """ + Descriptive details about the run. + """ + output: CheckRunOutput + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The time that the check run began. + """ + startedAt: DateTime + + """ + The current status. + """ + status: RequestableCheckStatusState +} + +""" +Autogenerated return type of CreateCheckRun. +""" +type CreateCheckRunPayload { + """ + The newly created check run. + """ + checkRun: CheckRun + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateCheckSuite +""" +input CreateCheckSuiteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The SHA of the head commit. + """ + headSha: GitObjectID! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateCheckSuite. +""" +type CreateCheckSuitePayload { + """ + The newly created check suite. + """ + checkSuite: CheckSuite + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateCommitOnBranch +""" +input CreateCommitOnBranchInput { + """ + The Ref to be updated. Must be a branch. + """ + branch: CommittableBranch! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The git commit oid expected at the head of the branch prior to the commit + """ + expectedHeadOid: GitObjectID! + + """ + A description of changes to files in this commit. + """ + fileChanges: FileChanges + + """ + The commit message the be included with the commit. + """ + message: CommitMessage! +} + +""" +Autogenerated return type of CreateCommitOnBranch. +""" +type CreateCommitOnBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new commit. + """ + commit: Commit + + """ + The ref which has been updated to point to the new commit. + """ + ref: Ref +} + +""" +Autogenerated input type of CreateDeployment +""" +input CreateDeploymentInput { + """ + Attempt to automatically merge the default branch into the requested ref, defaults to true. + """ + autoMerge: Boolean = true + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Short description of the deployment. + """ + description: String = "" + + """ + Name for the target deployment environment. + """ + environment: String = "production" + + """ + JSON payload with extra information about the deployment. + """ + payload: String = "{}" + + """ + The node ID of the ref to be deployed. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The status contexts to verify against commit status checks. To bypass required + contexts, pass an empty array. Defaults to all unique contexts. + """ + requiredContexts: [String!] + + """ + Specifies a task to execute. + """ + task: String = "deploy" +} + +""" +Autogenerated return type of CreateDeployment. +""" +type CreateDeploymentPayload { + """ + True if the default branch has been auto-merged into the deployment ref. + """ + autoMerged: Boolean + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new deployment. + """ + deployment: Deployment +} + +""" +Autogenerated input type of CreateDeploymentStatus +""" +input CreateDeploymentStatusInput { + """ + Adds a new inactive status to all non-transient, non-production environment + deployments with the same repository and environment name as the created + status's deployment. + """ + autoInactive: Boolean = true + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The node ID of the deployment. + """ + deploymentId: ID! @possibleTypes(concreteTypes: ["Deployment"]) + + """ + A short description of the status. Maximum length of 140 characters. + """ + description: String = "" + + """ + If provided, updates the environment of the deploy. Otherwise, does not modify the environment. + """ + environment: String + + """ + Sets the URL for accessing your environment. + """ + environmentUrl: String = "" + + """ + The log URL to associate with this status. This URL should contain + output to keep the user updated while the task is running or serve as + historical information for what happened in the deployment. + """ + logUrl: String = "" + + """ + The state of the deployment. + """ + state: DeploymentStatusState! +} + +""" +Autogenerated return type of CreateDeploymentStatus. +""" +type CreateDeploymentStatusPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new deployment status. + """ + deploymentStatus: DeploymentStatus +} + +""" +Autogenerated input type of CreateDiscussion +""" +input CreateDiscussionInput { + """ + The body of the discussion. + """ + body: String! + + """ + The id of the discussion category to associate with this discussion. + """ + categoryId: ID! @possibleTypes(concreteTypes: ["DiscussionCategory"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the repository on which to create the discussion. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title of the discussion. + """ + title: String! +} + +""" +Autogenerated return type of CreateDiscussion. +""" +type CreateDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that was just created. + """ + discussion: Discussion +} + +""" +Autogenerated input type of CreateEnterpriseOrganization +""" +input CreateEnterpriseOrganizationInput { + """ + The logins for the administrators of the new organization. + """ + adminLogins: [String!]! + + """ + The email used for sending billing receipts. + """ + billingEmail: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise owning the new organization. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the new organization. + """ + login: String! + + """ + The profile name of the new organization. + """ + profileName: String! +} + +""" +Autogenerated return type of CreateEnterpriseOrganization. +""" +type CreateEnterpriseOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise that owns the created organization. + """ + enterprise: Enterprise + + """ + The organization that was created. + """ + organization: Organization +} + +""" +Autogenerated input type of CreateEnvironment +""" +input CreateEnvironmentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the environment. + """ + name: String! + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateEnvironment. +""" +type CreateEnvironmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new or existing environment. + """ + environment: Environment +} + +""" +Autogenerated input type of CreateIpAllowListEntry +""" +input CreateIpAllowListEntryInput { + """ + An IP address or range of addresses in CIDR notation. + """ + allowListValue: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether the IP allow list entry is active when an IP allow list is enabled. + """ + isActive: Boolean! + + """ + An optional name for the IP allow list entry. + """ + name: String + + """ + The ID of the owner for which to create the new IP allow list entry. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["App", "Enterprise", "Organization"], abstractType: "IpAllowListOwner") +} + +""" +Autogenerated return type of CreateIpAllowListEntry. +""" +type CreateIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was created. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of CreateIssue +""" +input CreateIssueInput { + """ + The Node ID of assignees for this issue. + """ + assigneeIds: [ID!] + @possibleTypes( + concreteTypes: ["Bot", "EnterpriseUserAccount", "Mannequin", "Organization", "User"] + abstractType: "Actor" + ) + + """ + The body for the issue description. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of an issue template in the repository, assigns labels and assignees from the template to the issue + """ + issueTemplate: String + + """ + The Node ID of the issue type for this issue + """ + issueTypeId: ID + + """ + An array of Node IDs of labels for this issue. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + The Node ID of the milestone for this issue. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + The Node ID of the parent issue to add this new issue to + """ + parentIssueId: ID @possibleTypes(concreteTypes: ["Issue"]) + + """ + An array of Node IDs for projects associated with this issue. + """ + projectIds: [ID!] @possibleTypes(concreteTypes: ["Project"]) + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title for the issue. + """ + title: String! +} + +""" +Autogenerated return type of CreateIssue. +""" +type CreateIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new issue. + """ + issue: Issue +} + +""" +Autogenerated input type of CreateIssueType +""" +input CreateIssueTypeInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Color for the issue type + """ + color: IssueTypeColor + + """ + Description of the new issue type + """ + description: String + + """ + Whether or not the issue type is enabled on the org level + """ + isEnabled: Boolean! + + """ + Name of the new issue type + """ + name: String! + + """ + The ID for the organization on which the issue type is created + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of CreateIssueType. +""" +type CreateIssueTypePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created issue type + """ + issueType: IssueType +} + +""" +Autogenerated input type of CreateLabel +""" +input CreateLabelInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A 6 character hex code, without the leading #, identifying the color of the label. + """ + color: String! + + """ + A brief description of the label, such as its purpose. + """ + description: String + + """ + The name of the label. + """ + name: String! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateLabel. +""" +type CreateLabelPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new label. + """ + label: Label +} + +""" +Autogenerated input type of CreateLinkedBranch +""" +input CreateLinkedBranchInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue to link to. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The name of the new branch. Defaults to issue number and title. + """ + name: String + + """ + The commit SHA to base the new branch on. + """ + oid: GitObjectID! + + """ + ID of the repository to create the branch in. Defaults to the issue repository. + """ + repositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateLinkedBranch. +""" +type CreateLinkedBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was linked to. + """ + issue: Issue + + """ + The new branch issue reference. + """ + linkedBranch: LinkedBranch +} + +""" +Autogenerated input type of CreateMigrationSource +""" +input CreateMigrationSourceInput { + """ + The migration source access token. + """ + accessToken: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The GitHub personal access token of the user importing to the target repository. + """ + githubPat: String + + """ + The migration source name. + """ + name: String! + + """ + The ID of the organization that will own the migration source. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The migration source type. + """ + type: MigrationSourceType! + + """ + The migration source URL, for example `https://github.com` or `https://monalisa.ghe.com`. + """ + url: String +} + +""" +Autogenerated return type of CreateMigrationSource. +""" +type CreateMigrationSourcePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The created migration source. + """ + migrationSource: MigrationSource +} + +""" +Autogenerated input type of CreateProject +""" +input CreateProjectInput { + """ + The description of project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project. + """ + name: String! + + """ + The owner ID to create the project under. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "Repository", "User"], abstractType: "ProjectOwner") + + """ + A list of repository IDs to create as linked repositories for the project + """ + repositoryIds: [ID!] @possibleTypes(concreteTypes: ["Repository"]) + + """ + The name of the GitHub-provided template. + """ + template: ProjectTemplate +} + +""" +Autogenerated return type of CreateProject. +""" +type CreateProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new project. + """ + project: Project +} + +""" +Autogenerated input type of CreateProjectV2Field +""" +input CreateProjectV2FieldInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The data type of the field. + """ + dataType: ProjectV2CustomFieldType! + + """ + Configuration for an iteration field. + """ + iterationConfiguration: ProjectV2IterationFieldConfigurationInput + + """ + The name of the field. + """ + name: String! + + """ + The ID of the Project to create the field in. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + Options for a single select field. At least one value is required if data_type is SINGLE_SELECT + """ + singleSelectOptions: [ProjectV2SingleSelectFieldOptionInput!] +} + +""" +Autogenerated return type of CreateProjectV2Field. +""" +type CreateProjectV2FieldPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new field. + """ + projectV2Field: ProjectV2FieldConfiguration +} + +""" +Autogenerated input type of CreateProjectV2 +""" +input CreateProjectV2Input { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owner ID to create the project under. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "User"]) + + """ + The repository to link the project to. + """ + repositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) + + """ + The team to link the project to. The team will be granted read permissions. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + The title of the project. + """ + title: String! +} + +""" +Autogenerated return type of CreateProjectV2. +""" +type CreateProjectV2Payload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of CreateProjectV2StatusUpdate +""" +input CreateProjectV2StatusUpdateInput { + """ + The body of the status update. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to create the status update in. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The start date of the status update. + """ + startDate: Date + + """ + The status of the status update. + """ + status: ProjectV2StatusUpdateStatus + + """ + The target date of the status update. + """ + targetDate: Date +} + +""" +Autogenerated return type of CreateProjectV2StatusUpdate. +""" +type CreateProjectV2StatusUpdatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The status update updated in the project. + """ + statusUpdate: ProjectV2StatusUpdate +} + +""" +Autogenerated input type of CreatePullRequest +""" +input CreatePullRequestInput { + """ + The name of the branch you want your changes pulled into. This should be an existing branch + on the current repository. You cannot update the base branch on a pull request to point + to another repository. + """ + baseRefName: String! + + """ + The contents of the pull request. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Indicates whether this pull request should be a draft. + """ + draft: Boolean = false + + """ + The name of the branch where your changes are implemented. For cross-repository pull requests + in the same network, namespace `head_ref_name` with a user like this: `username:branch`. + """ + headRefName: String! + + """ + The Node ID of the head repository. + """ + headRepositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean = true + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title of the pull request. + """ + title: String! +} + +""" +Autogenerated return type of CreatePullRequest. +""" +type CreatePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of CreateRef +""" +input CreateRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The fully qualified name of the new Ref (ie: `refs/heads/my_new_branch`). + """ + name: String! + + """ + The GitObjectID that the new Ref shall target. Must point to a commit. + """ + oid: GitObjectID! + + """ + The Node ID of the Repository to create the Ref in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateRef. +""" +type CreateRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created ref. + """ + ref: Ref +} + +""" +Autogenerated input type of CreateRepository +""" +input CreateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A short description of the new repository. + """ + description: String + + """ + Indicates if the repository should have the issues feature enabled. + """ + hasIssuesEnabled: Boolean = true + + """ + Indicates if the repository should have the wiki feature enabled. + """ + hasWikiEnabled: Boolean = false + + """ + The URL for a web page about this repository. + """ + homepageUrl: URI + + """ + The name of the new repository. + """ + name: String! + + """ + The ID of the owner for the new repository. + """ + ownerId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "RepositoryOwner") + + """ + When an organization is specified as the owner, this ID identifies the team + that should be granted access to the new repository. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + Whether this repository should be marked as a template such that anyone who + can access it can create new repositories with the same files and directory structure. + """ + template: Boolean = false + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! +} + +""" +Autogenerated return type of CreateRepository. +""" +type CreateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new repository. + """ + repository: Repository +} + +""" +Autogenerated input type of CreateRepositoryRuleset +""" +input CreateRepositoryRulesetInput { + """ + A list of actors that are allowed to bypass rules in this ruleset. + """ + bypassActors: [RepositoryRulesetBypassActorInput!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The set of conditions for this ruleset + """ + conditions: RepositoryRuleConditionsInput! + + """ + The enforcement level for this ruleset + """ + enforcement: RuleEnforcement! + + """ + The name of the ruleset. + """ + name: String! + + """ + The list of rules for this ruleset + """ + rules: [RepositoryRuleInput!] + + """ + The global relay id of the source in which a new ruleset should be created in. + """ + sourceId: ID! @possibleTypes(concreteTypes: ["Enterprise", "Organization", "Repository"], abstractType: "RuleSource") + + """ + The target of the ruleset. + """ + target: RepositoryRulesetTarget +} + +""" +Autogenerated return type of CreateRepositoryRuleset. +""" +type CreateRepositoryRulesetPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created Ruleset. + """ + ruleset: RepositoryRuleset +} + +""" +Autogenerated input type of CreateSponsorsListing +""" +input CreateSponsorsListingInput { + """ + The country or region where the sponsorable's bank account is located. + Required if fiscalHostLogin is not specified, ignored when fiscalHostLogin is specified. + """ + billingCountryOrRegionCode: SponsorsCountryOrRegionCode + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The email address we should use to contact you about the GitHub Sponsors + profile being created. This will not be shared publicly. Must be a verified + email address already on your GitHub account. Only relevant when the + sponsorable is yourself. Defaults to your primary email address on file if omitted. + """ + contactEmail: String + + """ + The username of the supported fiscal host's GitHub organization, if you want + to receive sponsorship payouts through a fiscal host rather than directly to a + bank account. For example, 'Open-Source-Collective' for Open Source Collective + or 'numfocus' for numFOCUS. Case insensitive. See https://docs.github.com/sponsors/receiving-sponsorships-through-github-sponsors/using-a-fiscal-host-to-receive-github-sponsors-payouts + for more information. + """ + fiscalHostLogin: String + + """ + The URL for your profile page on the fiscal host's website, e.g., + https://opencollective.com/babel or https://numfocus.org/project/bokeh. + Required if fiscalHostLogin is specified. + """ + fiscallyHostedProjectProfileUrl: String + + """ + Provide an introduction to serve as the main focus that appears on your GitHub + Sponsors profile. It's a great opportunity to help potential sponsors learn + more about you, your work, and why their sponsorship is important to you. + GitHub-flavored Markdown is supported. + """ + fullDescription: String + + """ + The country or region where the sponsorable resides. This is for tax purposes. + Required if the sponsorable is yourself, ignored when sponsorableLogin + specifies an organization. + """ + residenceCountryOrRegionCode: SponsorsCountryOrRegionCode + + """ + The username of the organization to create a GitHub Sponsors profile for, if + desired. Defaults to creating a GitHub Sponsors profile for the authenticated + user if omitted. + """ + sponsorableLogin: String +} + +""" +Autogenerated return type of CreateSponsorsListing. +""" +type CreateSponsorsListingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new GitHub Sponsors profile. + """ + sponsorsListing: SponsorsListing +} + +""" +Autogenerated input type of CreateSponsorsTier +""" +input CreateSponsorsTierInput { + """ + The value of the new tier in US dollars. Valid values: 1-12000. + """ + amount: Int! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A description of what this tier is, what perks sponsors might receive, what a sponsorship at this tier means for you, etc. + """ + description: String! + + """ + Whether sponsorships using this tier should happen monthly/yearly or just once. + """ + isRecurring: Boolean = true + + """ + Whether to make the tier available immediately for sponsors to choose. + Defaults to creating a draft tier that will not be publicly visible. + """ + publish: Boolean = false + + """ + Optional ID of the private repository that sponsors at this tier should gain + read-only access to. Must be owned by an organization. + """ + repositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) + + """ + Optional name of the private repository that sponsors at this tier should gain + read-only access to. Must be owned by an organization. Necessary if + repositoryOwnerLogin is given. Will be ignored if repositoryId is given. + """ + repositoryName: String + + """ + Optional login of the organization owner of the private repository that + sponsors at this tier should gain read-only access to. Necessary if + repositoryName is given. Will be ignored if repositoryId is given. + """ + repositoryOwnerLogin: String + + """ + The ID of the user or organization who owns the GitHub Sponsors profile. + Defaults to the current user if omitted and sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who owns the GitHub Sponsors profile. + Defaults to the current user if omitted and sponsorableId is not given. + """ + sponsorableLogin: String + + """ + Optional message new sponsors at this tier will receive. + """ + welcomeMessage: String +} + +""" +Autogenerated return type of CreateSponsorsTier. +""" +type CreateSponsorsTierPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new tier. + """ + sponsorsTier: SponsorsTier +} + +""" +Autogenerated input type of CreateSponsorship +""" +input CreateSponsorshipInput { + """ + The amount to pay to the sponsorable in US dollars. Required if a tierId is not specified. Valid values: 1-12000. + """ + amount: Int + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether the sponsorship should happen monthly/yearly or just this one time. Required if a tierId is not specified. + """ + isRecurring: Boolean + + """ + Specify whether others should be able to see that the sponsor is sponsoring + the sponsorable. Public visibility still does not reveal which tier is used. + """ + privacyLevel: SponsorshipPrivacy = PUBLIC + + """ + Whether the sponsor should receive email updates from the sponsorable. + """ + receiveEmails: Boolean = true + + """ + The ID of the user or organization who is acting as the sponsor, paying for + the sponsorship. Required if sponsorLogin is not given. + """ + sponsorId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsor") + + """ + The username of the user or organization who is acting as the sponsor, paying + for the sponsorship. Required if sponsorId is not given. + """ + sponsorLogin: String + + """ + The ID of the user or organization who is receiving the sponsorship. Required if sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who is receiving the sponsorship. Required if sponsorableId is not given. + """ + sponsorableLogin: String + + """ + The ID of one of sponsorable's existing tiers to sponsor at. Required if amount is not specified. + """ + tierId: ID @possibleTypes(concreteTypes: ["SponsorsTier"]) +} + +""" +Autogenerated return type of CreateSponsorship. +""" +type CreateSponsorshipPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The sponsorship that was started. + """ + sponsorship: Sponsorship +} + +""" +Autogenerated input type of CreateSponsorships +""" +input CreateSponsorshipsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Specify whether others should be able to see that the sponsor is sponsoring + the sponsorables. Public visibility still does not reveal the dollar value of + the sponsorship. + """ + privacyLevel: SponsorshipPrivacy = PUBLIC + + """ + Whether the sponsor should receive email updates from the sponsorables. + """ + receiveEmails: Boolean = false + + """ + Whether the sponsorships created should continue each billing cycle for the + sponsor (monthly or annually), versus lasting only a single month. Defaults to + one-time sponsorships. + """ + recurring: Boolean = false + + """ + The username of the user or organization who is acting as the sponsor, paying for the sponsorships. + """ + sponsorLogin: String! + + """ + The list of maintainers to sponsor and for how much apiece. + """ + sponsorships: [BulkSponsorship!]! +} + +""" +Autogenerated return type of CreateSponsorships. +""" +type CreateSponsorshipsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The users and organizations who received a sponsorship. + """ + sponsorables: [Sponsorable!] +} + +""" +Autogenerated input type of CreateTeamDiscussionComment +""" +input CreateTeamDiscussionCommentInput { + """ + The content of the comment. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `body` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the discussion to which the comment belongs. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `discussionId` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + discussionId: ID @possibleTypes(concreteTypes: ["TeamDiscussion"]) +} + +""" +Autogenerated return type of CreateTeamDiscussionComment. +""" +type CreateTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new comment. + """ + teamDiscussionComment: TeamDiscussionComment + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) +} + +""" +Autogenerated input type of CreateTeamDiscussion +""" +input CreateTeamDiscussionInput { + """ + The content of the discussion. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `body` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + If true, restricts the visibility of this discussion to team members and + organization owners. If false or not specified, allows any organization member + to view this discussion. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `private` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + private: Boolean + + """ + The ID of the team to which the discussion belongs. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `teamId` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + The title of the discussion. This field is required. + + **Upcoming Change on 2024-07-01 UTC** + **Description:** `title` will be removed. Follow the guide at + https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to + find a suitable replacement. + **Reason:** The Team Discussions feature is deprecated in favor of Organization Discussions. + """ + title: String +} + +""" +Autogenerated return type of CreateTeamDiscussion. +""" +type CreateTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new discussion. + """ + teamDiscussion: TeamDiscussion + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) +} + +""" +Autogenerated input type of CreateUserList +""" +input CreateUserListInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A description of the list + """ + description: String + + """ + Whether or not the list is private + """ + isPrivate: Boolean = false + + """ + The name of the new list + """ + name: String! +} + +""" +Autogenerated return type of CreateUserList. +""" +type CreateUserListPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The list that was just created + """ + list: UserList + + """ + The user who created the list + """ + viewer: User +} + +""" +Represents the contribution a user made by committing to a repository. +""" +type CreatedCommitContribution implements Contribution { + """ + How many commits were made on this day to this repository by the user. + """ + commitCount: Int! + + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The repository the user made a commit in. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedCommitContribution. +""" +type CreatedCommitContributionConnection { + """ + A list of edges. + """ + edges: [CreatedCommitContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedCommitContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of commits across days and repositories in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedCommitContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedCommitContribution +} + +""" +Represents the contribution a user made on GitHub by opening an issue. +""" +type CreatedIssueContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + The issue that was opened. + """ + issue: Issue! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedIssueContribution. +""" +type CreatedIssueContributionConnection { + """ + A list of edges. + """ + edges: [CreatedIssueContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedIssueContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedIssueContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedIssueContribution +} + +""" +Represents either a issue the viewer can access or a restricted contribution. +""" +union CreatedIssueOrRestrictedContribution = CreatedIssueContribution | RestrictedContribution + +""" +Represents the contribution a user made on GitHub by opening a pull request. +""" +type CreatedPullRequestContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The pull request that was opened. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedPullRequestContribution. +""" +type CreatedPullRequestContributionConnection { + """ + A list of edges. + """ + edges: [CreatedPullRequestContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedPullRequestContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedPullRequestContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedPullRequestContribution +} + +""" +Represents either a pull request the viewer can access or a restricted contribution. +""" +union CreatedPullRequestOrRestrictedContribution = CreatedPullRequestContribution | RestrictedContribution + +""" +Represents the contribution a user made by leaving a review on a pull request. +""" +type CreatedPullRequestReviewContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The pull request the user reviewed. + """ + pullRequest: PullRequest! + + """ + The review the user left on the pull request. + """ + pullRequestReview: PullRequestReview! + + """ + The repository containing the pull request that the user reviewed. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedPullRequestReviewContribution. +""" +type CreatedPullRequestReviewContributionConnection { + """ + A list of edges. + """ + edges: [CreatedPullRequestReviewContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedPullRequestReviewContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedPullRequestReviewContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedPullRequestReviewContribution +} + +""" +Represents the contribution a user made on GitHub by creating a repository. +""" +type CreatedRepositoryContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The repository that was created. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedRepositoryContribution. +""" +type CreatedRepositoryContributionConnection { + """ + A list of edges. + """ + edges: [CreatedRepositoryContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedRepositoryContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedRepositoryContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedRepositoryContribution +} + +""" +Represents either a repository the viewer can access or a restricted contribution. +""" +union CreatedRepositoryOrRestrictedContribution = CreatedRepositoryContribution | RestrictedContribution + +""" +Represents a mention made by one issue or pull request to another. +""" +type CrossReferencedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the CrossReferencedEvent object + """ + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Identifies when the reference was made. + """ + referencedAt: DateTime! + + """ + The HTTP path for this pull request. + """ + resourcePath: URI! + + """ + Issue or pull request that made the reference. + """ + source: ReferencedSubject! + + """ + Issue or pull request to which the reference was made. + """ + target: ReferencedSubject! + + """ + The HTTP URL for this pull request. + """ + url: URI! + + """ + Checks if the target will be closed when the source is merged. + """ + willCloseTarget: Boolean! +} + +""" +The Common Vulnerability Scoring System +""" +type CvssSeverities { + """ + The CVSS v3 severity associated with this advisory + """ + cvssV3: CVSS + + """ + The CVSS v4 severity associated with this advisory + """ + cvssV4: CVSS +} + +""" +An ISO-8601 encoded date string. +""" +scalar Date + +""" +An ISO-8601 encoded UTC date string. +""" +scalar DateTime + +""" +Autogenerated input type of DeclineTopicSuggestion +""" +input DeclineTopicSuggestionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the suggested topic. + + **Upcoming Change on 2024-04-01 UTC** + **Description:** `name` will be removed. + **Reason:** Suggested topics are no longer supported + """ + name: String + + """ + The reason why the suggested topic is declined. + + **Upcoming Change on 2024-04-01 UTC** + **Description:** `reason` will be removed. + **Reason:** Suggested topics are no longer supported + """ + reason: TopicSuggestionDeclineReason + + """ + The Node ID of the repository. + + **Upcoming Change on 2024-04-01 UTC** + **Description:** `repositoryId` will be removed. + **Reason:** Suggested topics are no longer supported + """ + repositoryId: ID @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of DeclineTopicSuggestion. +""" +type DeclineTopicSuggestionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The declined topic. + """ + topic: Topic @deprecated(reason: "Suggested topics are no longer supported Removal on 2024-04-01 UTC.") +} + +""" +The possible base permissions for repositories. +""" +enum DefaultRepositoryPermissionField { + """ + Can read, write, and administrate repos by default + """ + ADMIN + + """ + No access + """ + NONE + + """ + Can read repos by default + """ + READ + + """ + Can read and write repos by default + """ + WRITE +} + +""" +Entities that can be deleted. +""" +interface Deletable { + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! +} + +""" +Autogenerated input type of DeleteBranchProtectionRule +""" +input DeleteBranchProtectionRuleInput { + """ + The global relay id of the branch protection rule to be deleted. + """ + branchProtectionRuleId: ID! @possibleTypes(concreteTypes: ["BranchProtectionRule"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of DeleteBranchProtectionRule. +""" +type DeleteBranchProtectionRulePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteDeployment +""" +input DeleteDeploymentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the deployment to be deleted. + """ + id: ID! @possibleTypes(concreteTypes: ["Deployment"]) +} + +""" +Autogenerated return type of DeleteDeployment. +""" +type DeleteDeploymentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteDiscussionComment +""" +input DeleteDiscussionCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node id of the discussion comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of DeleteDiscussionComment. +""" +type DeleteDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion comment that was just deleted. + """ + comment: DiscussionComment +} + +""" +Autogenerated input type of DeleteDiscussion +""" +input DeleteDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the discussion to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["Discussion"]) +} + +""" +Autogenerated return type of DeleteDiscussion. +""" +type DeleteDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that was just deleted. + """ + discussion: Discussion +} + +""" +Autogenerated input type of DeleteEnvironment +""" +input DeleteEnvironmentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the environment to be deleted. + """ + id: ID! @possibleTypes(concreteTypes: ["Environment"]) +} + +""" +Autogenerated return type of DeleteEnvironment. +""" +type DeleteEnvironmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteIpAllowListEntry +""" +input DeleteIpAllowListEntryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IP allow list entry to delete. + """ + ipAllowListEntryId: ID! @possibleTypes(concreteTypes: ["IpAllowListEntry"]) +} + +""" +Autogenerated return type of DeleteIpAllowListEntry. +""" +type DeleteIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was deleted. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of DeleteIssueComment +""" +input DeleteIssueCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["IssueComment"]) +} + +""" +Autogenerated return type of DeleteIssueComment. +""" +type DeleteIssueCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteIssue +""" +input DeleteIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to delete. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of DeleteIssue. +""" +type DeleteIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository the issue belonged to + """ + repository: Repository +} + +""" +Autogenerated input type of DeleteIssueType +""" +input DeleteIssueTypeInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue type to delete + """ + issueTypeId: ID! @possibleTypes(concreteTypes: ["IssueType"]) +} + +""" +Autogenerated return type of DeleteIssueType. +""" +type DeleteIssueTypePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the deleted issue type + """ + deletedIssueTypeId: ID +} + +""" +Autogenerated input type of DeleteLabel +""" +input DeleteLabelInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the label to be deleted. + """ + id: ID! @possibleTypes(concreteTypes: ["Label"]) +} + +""" +Autogenerated return type of DeleteLabel. +""" +type DeleteLabelPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteLinkedBranch +""" +input DeleteLinkedBranchInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the linked branch + """ + linkedBranchId: ID! @possibleTypes(concreteTypes: ["LinkedBranch"]) +} + +""" +Autogenerated return type of DeleteLinkedBranch. +""" +type DeleteLinkedBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue the linked branch was unlinked from. + """ + issue: Issue +} + +""" +Autogenerated input type of DeletePackageVersion +""" +input DeletePackageVersionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the package version to be deleted. + """ + packageVersionId: ID! @possibleTypes(concreteTypes: ["PackageVersion"]) +} + +""" +Autogenerated return type of DeletePackageVersion. +""" +type DeletePackageVersionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not the operation succeeded. + """ + success: Boolean +} + +""" +Autogenerated input type of DeleteProjectCard +""" +input DeleteProjectCardInput { + """ + The id of the card to delete. + """ + cardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of DeleteProjectCard. +""" +type DeleteProjectCardPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The column the deleted card was in. + """ + column: ProjectColumn + + """ + The deleted card ID. + """ + deletedCardId: ID +} + +""" +Autogenerated input type of DeleteProjectColumn +""" +input DeleteProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to delete. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of DeleteProjectColumn. +""" +type DeleteProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted column ID. + """ + deletedColumnId: ID + + """ + The project the deleted column was in. + """ + project: Project +} + +""" +Autogenerated input type of DeleteProject +""" +input DeleteProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Project ID to update. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) +} + +""" +Autogenerated return type of DeleteProject. +""" +type DeleteProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository or organization the project was removed from. + """ + owner: ProjectOwner +} + +""" +Autogenerated input type of DeleteProjectV2Field +""" +input DeleteProjectV2FieldInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the field to delete. + """ + fieldId: ID! + @possibleTypes( + concreteTypes: ["ProjectV2Field", "ProjectV2IterationField", "ProjectV2SingleSelectField"] + abstractType: "ProjectV2FieldConfiguration" + ) +} + +""" +Autogenerated return type of DeleteProjectV2Field. +""" +type DeleteProjectV2FieldPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted field. + """ + projectV2Field: ProjectV2FieldConfiguration +} + +""" +Autogenerated input type of DeleteProjectV2 +""" +input DeleteProjectV2Input { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to delete. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated input type of DeleteProjectV2Item +""" +input DeleteProjectV2ItemInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the item to be removed. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project from which the item should be removed. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of DeleteProjectV2Item. +""" +type DeleteProjectV2ItemPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the deleted item. + """ + deletedItemId: ID +} + +""" +Autogenerated return type of DeleteProjectV2. +""" +type DeleteProjectV2Payload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted Project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of DeleteProjectV2StatusUpdate +""" +input DeleteProjectV2StatusUpdateInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the status update to be removed. + """ + statusUpdateId: ID! @possibleTypes(concreteTypes: ["ProjectV2StatusUpdate"]) +} + +""" +Autogenerated return type of DeleteProjectV2StatusUpdate. +""" +type DeleteProjectV2StatusUpdatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the deleted status update. + """ + deletedStatusUpdateId: ID + + """ + The project the deleted status update was in. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of DeleteProjectV2Workflow +""" +input DeleteProjectV2WorkflowInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the workflow to be removed. + """ + workflowId: ID! @possibleTypes(concreteTypes: ["ProjectV2Workflow"]) +} + +""" +Autogenerated return type of DeleteProjectV2Workflow. +""" +type DeleteProjectV2WorkflowPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the deleted workflow. + """ + deletedWorkflowId: ID + + """ + The project the deleted workflow was in. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of DeletePullRequestReviewComment +""" +input DeletePullRequestReviewCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) +} + +""" +Autogenerated return type of DeletePullRequestReviewComment. +""" +type DeletePullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request review the deleted comment belonged to. + """ + pullRequestReview: PullRequestReview + + """ + The deleted pull request review comment. + """ + pullRequestReviewComment: PullRequestReviewComment +} + +""" +Autogenerated input type of DeletePullRequestReview +""" +input DeletePullRequestReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request review to delete. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of DeletePullRequestReview. +""" +type DeletePullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +Autogenerated input type of DeleteRef +""" +input DeleteRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the Ref to be deleted. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) +} + +""" +Autogenerated return type of DeleteRef. +""" +type DeleteRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteRepositoryRuleset +""" +input DeleteRepositoryRulesetInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The global relay id of the repository ruleset to be deleted. + """ + repositoryRulesetId: ID! @possibleTypes(concreteTypes: ["RepositoryRuleset"]) +} + +""" +Autogenerated return type of DeleteRepositoryRuleset. +""" +type DeleteRepositoryRulesetPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteTeamDiscussionComment +""" +input DeleteTeamDiscussionCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussionComment"]) +} + +""" +Autogenerated return type of DeleteTeamDiscussionComment. +""" +type DeleteTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteTeamDiscussion +""" +input DeleteTeamDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion ID to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussion"]) +} + +""" +Autogenerated return type of DeleteTeamDiscussion. +""" +type DeleteTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteUserList +""" +input DeleteUserListInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the list to delete. + """ + listId: ID! @possibleTypes(concreteTypes: ["UserList"]) +} + +""" +Autogenerated return type of DeleteUserList. +""" +type DeleteUserListPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owner of the list that will be deleted + """ + user: User +} + +""" +Autogenerated input type of DeleteVerifiableDomain +""" +input DeleteVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of DeleteVerifiableDomain. +""" +type DeleteVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owning account from which the domain was deleted. + """ + owner: VerifiableDomainOwner +} + +""" +Represents a 'demilestoned' event on a given issue or pull request. +""" +type DemilestonedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the DemilestonedEvent object + """ + id: ID! + + """ + Identifies the milestone title associated with the 'demilestoned' event. + """ + milestoneTitle: String! + + """ + Object referenced by event. + """ + subject: MilestoneItem! +} + +""" +A Dependabot Update for a dependency in a repository +""" +type DependabotUpdate implements RepositoryNode { + """ + The error from a dependency update + """ + error: DependabotUpdateError + + """ + The associated pull request + """ + pullRequest: PullRequest + + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +An error produced from a Dependabot Update +""" +type DependabotUpdateError { + """ + The body of the error + """ + body: String! + + """ + The error code + """ + errorType: String! + + """ + The title of the error + """ + title: String! +} + +""" +A dependency manifest entry +""" +type DependencyGraphDependency { + """ + Does the dependency itself have dependencies? + """ + hasDependencies: Boolean! + + """ + The original name of the package, as it appears in the manifest. + """ + packageLabel: String! + @deprecated( + reason: "`packageLabel` will be removed. Use normalized `packageName` field instead. Removal on 2022-10-01 UTC." + ) + + """ + The dependency package manager + """ + packageManager: String + + """ + The name of the package in the canonical form used by the package manager. + """ + packageName: String! + + """ + Public preview: The dependency package URL + """ + packageUrl: URI + + """ + Public preview: The relationship of the dependency. Can be direct, transitive, or unknown + """ + relationship: String! + + """ + The repository containing the package + """ + repository: Repository + + """ + The dependency version requirements + """ + requirements: String! +} + +""" +The connection type for DependencyGraphDependency. +""" +type DependencyGraphDependencyConnection { + """ + A list of edges. + """ + edges: [DependencyGraphDependencyEdge] + + """ + A list of nodes. + """ + nodes: [DependencyGraphDependency] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DependencyGraphDependencyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DependencyGraphDependency +} + +""" +The possible ecosystems of a dependency graph package. +""" +enum DependencyGraphEcosystem { + """ + GitHub Actions + """ + ACTIONS + + """ + PHP packages hosted at packagist.org + """ + COMPOSER + + """ + Go modules + """ + GO + + """ + Java artifacts hosted at the Maven central repository + """ + MAVEN + + """ + JavaScript packages hosted at npmjs.com + """ + NPM + + """ + .NET packages hosted at the NuGet Gallery + """ + NUGET + + """ + Python packages hosted at PyPI.org + """ + PIP + + """ + Dart packages hosted at pub.dev + """ + PUB + + """ + Ruby gems hosted at RubyGems.org + """ + RUBYGEMS + + """ + Rust crates + """ + RUST + + """ + Swift packages + """ + SWIFT +} + +""" +Dependency manifest for a repository +""" +type DependencyGraphManifest implements Node { + """ + Path to view the manifest file blob + """ + blobPath: String! + + """ + A list of manifest dependencies + """ + dependencies( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DependencyGraphDependencyConnection + + """ + The number of dependencies listed in the manifest + """ + dependenciesCount: Int + + """ + Is the manifest too big to parse? + """ + exceedsMaxSize: Boolean! + + """ + Fully qualified manifest filename + """ + filename: String! + + """ + The Node ID of the DependencyGraphManifest object + """ + id: ID! + + """ + Were we able to parse the manifest? + """ + parseable: Boolean! + + """ + The repository containing the manifest + """ + repository: Repository! +} + +""" +The connection type for DependencyGraphManifest. +""" +type DependencyGraphManifestConnection { + """ + A list of edges. + """ + edges: [DependencyGraphManifestEdge] + + """ + A list of nodes. + """ + nodes: [DependencyGraphManifest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DependencyGraphManifestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DependencyGraphManifest +} + +""" +A repository deploy key. +""" +type DeployKey implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Whether or not the deploy key is enabled by policy at the Enterprise or Organization level. + """ + enabled: Boolean! + + """ + The Node ID of the DeployKey object + """ + id: ID! + + """ + The deploy key. + """ + key: String! + + """ + Whether or not the deploy key is read only. + """ + readOnly: Boolean! + + """ + The deploy key title. + """ + title: String! + + """ + Whether or not the deploy key has been verified. + """ + verified: Boolean! +} + +""" +The connection type for DeployKey. +""" +type DeployKeyConnection { + """ + A list of edges. + """ + edges: [DeployKeyEdge] + + """ + A list of nodes. + """ + nodes: [DeployKey] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeployKeyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeployKey +} + +""" +Represents a 'deployed' event on a given pull request. +""" +type DeployedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The deployment associated with the 'deployed' event. + """ + deployment: Deployment! + + """ + The Node ID of the DeployedEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The ref associated with the 'deployed' event. + """ + ref: Ref +} + +""" +Represents triggered deployment instance. +""" +type Deployment implements Node { + """ + Identifies the commit sha of the deployment. + """ + commit: Commit + + """ + Identifies the oid of the deployment commit, even if the commit has been deleted. + """ + commitOid: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who triggered the deployment. + """ + creator: Actor! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The deployment description. + """ + description: String + + """ + The latest environment to which this deployment was made. + """ + environment: String + + """ + The Node ID of the Deployment object + """ + id: ID! + + """ + The latest environment to which this deployment was made. + """ + latestEnvironment: String + + """ + The latest status of this deployment. + """ + latestStatus: DeploymentStatus + + """ + The original environment to which this deployment was made. + """ + originalEnvironment: String + + """ + Extra information that a deployment system might need. + """ + payload: String + + """ + Identifies the Ref of the deployment, if the deployment was created by ref. + """ + ref: Ref + + """ + Identifies the repository associated with the deployment. + """ + repository: Repository! + + """ + The current state of the deployment. + """ + state: DeploymentState + + """ + A list of statuses associated with the deployment. + """ + statuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentStatusConnection + + """ + The deployment task. + """ + task: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for Deployment. +""" +type DeploymentConnection { + """ + A list of edges. + """ + edges: [DeploymentEdge] + + """ + A list of nodes. + """ + nodes: [Deployment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Deployment +} + +""" +Represents a 'deployment_environment_changed' event on a given pull request. +""" +type DeploymentEnvironmentChangedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The deployment status that updated the deployment environment. + """ + deploymentStatus: DeploymentStatus! + + """ + The Node ID of the DeploymentEnvironmentChangedEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Ordering options for deployment connections +""" +input DeploymentOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order deployments by. + """ + field: DeploymentOrderField! +} + +""" +Properties by which deployment connections can be ordered. +""" +enum DeploymentOrderField { + """ + Order collection by creation time + """ + CREATED_AT +} + +""" +A protection rule. +""" +type DeploymentProtectionRule { + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Whether deployments to this environment can be approved by the user who created the deployment. + """ + preventSelfReview: Boolean + + """ + The teams or users that can review the deployment + """ + reviewers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentReviewerConnection! + + """ + The timeout in minutes for this protection rule. + """ + timeout: Int! + + """ + The type of protection rule. + """ + type: DeploymentProtectionRuleType! +} + +""" +The connection type for DeploymentProtectionRule. +""" +type DeploymentProtectionRuleConnection { + """ + A list of edges. + """ + edges: [DeploymentProtectionRuleEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentProtectionRule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentProtectionRuleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentProtectionRule +} + +""" +The possible protection rule types. +""" +enum DeploymentProtectionRuleType { + """ + Branch policy + """ + BRANCH_POLICY + + """ + Required reviewers + """ + REQUIRED_REVIEWERS + + """ + Wait timer + """ + WAIT_TIMER +} + +""" +A request to deploy a workflow run to an environment. +""" +type DeploymentRequest { + """ + Whether or not the current user can approve the deployment + """ + currentUserCanApprove: Boolean! + + """ + The target environment of the deployment + """ + environment: Environment! + + """ + The teams or users that can review the deployment + """ + reviewers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentReviewerConnection! + + """ + The wait timer in minutes configured in the environment + """ + waitTimer: Int! + + """ + The wait timer in minutes configured in the environment + """ + waitTimerStartedAt: DateTime +} + +""" +The connection type for DeploymentRequest. +""" +type DeploymentRequestConnection { + """ + A list of edges. + """ + edges: [DeploymentRequestEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentRequest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentRequestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentRequest +} + +""" +A deployment review. +""" +type DeploymentReview implements Node { + """ + The comment the user left. + """ + comment: String! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The environments approved or rejected + """ + environments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): EnvironmentConnection! + + """ + The Node ID of the DeploymentReview object + """ + id: ID! + + """ + The decision of the user. + """ + state: DeploymentReviewState! + + """ + The user that reviewed the deployment. + """ + user: User! +} + +""" +The connection type for DeploymentReview. +""" +type DeploymentReviewConnection { + """ + A list of edges. + """ + edges: [DeploymentReviewEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentReview] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentReviewEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentReview +} + +""" +The possible states for a deployment review. +""" +enum DeploymentReviewState { + """ + The deployment was approved. + """ + APPROVED + + """ + The deployment was rejected. + """ + REJECTED +} + +""" +Users and teams. +""" +union DeploymentReviewer = Team | User + +""" +The connection type for DeploymentReviewer. +""" +type DeploymentReviewerConnection { + """ + A list of edges. + """ + edges: [DeploymentReviewerEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentReviewer] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentReviewerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentReviewer +} + +""" +The possible states in which a deployment can be. +""" +enum DeploymentState { + """ + The pending deployment was not updated after 30 minutes. + """ + ABANDONED + + """ + The deployment is currently active. + """ + ACTIVE + + """ + An inactive transient deployment. + """ + DESTROYED + + """ + The deployment experienced an error. + """ + ERROR + + """ + The deployment has failed. + """ + FAILURE + + """ + The deployment is inactive. + """ + INACTIVE + + """ + The deployment is in progress. + """ + IN_PROGRESS + + """ + The deployment is pending. + """ + PENDING + + """ + The deployment has queued + """ + QUEUED + + """ + The deployment was successful. + """ + SUCCESS + + """ + The deployment is waiting. + """ + WAITING +} + +""" +Describes the status of a given deployment attempt. +""" +type DeploymentStatus implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who triggered the deployment. + """ + creator: Actor! + + """ + Identifies the deployment associated with status. + """ + deployment: Deployment! + + """ + Identifies the description of the deployment. + """ + description: String + + """ + Identifies the environment of the deployment at the time of this deployment status + """ + environment: String + + """ + Identifies the environment URL of the deployment. + """ + environmentUrl: URI + + """ + The Node ID of the DeploymentStatus object + """ + id: ID! + + """ + Identifies the log URL of the deployment. + """ + logUrl: URI + + """ + Identifies the current state of the deployment. + """ + state: DeploymentStatusState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for DeploymentStatus. +""" +type DeploymentStatusConnection { + """ + A list of edges. + """ + edges: [DeploymentStatusEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentStatus] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentStatusEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentStatus +} + +""" +The possible states for a deployment status. +""" +enum DeploymentStatusState { + """ + The deployment experienced an error. + """ + ERROR + + """ + The deployment has failed. + """ + FAILURE + + """ + The deployment is inactive. + """ + INACTIVE + + """ + The deployment is in progress. + """ + IN_PROGRESS + + """ + The deployment is pending. + """ + PENDING + + """ + The deployment is queued + """ + QUEUED + + """ + The deployment was successful. + """ + SUCCESS + + """ + The deployment is waiting. + """ + WAITING +} + +""" +Autogenerated input type of DequeuePullRequest +""" +input DequeuePullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the pull request to be dequeued. + """ + id: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of DequeuePullRequest. +""" +type DequeuePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The merge queue entry of the dequeued pull request. + """ + mergeQueueEntry: MergeQueueEntry +} + +""" +The possible sides of a diff. +""" +enum DiffSide { + """ + The left side of the diff. + """ + LEFT + + """ + The right side of the diff. + """ + RIGHT +} + +""" +Autogenerated input type of DisablePullRequestAutoMerge +""" +input DisablePullRequestAutoMergeInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to disable auto merge on. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of DisablePullRequestAutoMerge. +""" +type DisablePullRequestAutoMergePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request auto merge was disabled on. + """ + pullRequest: PullRequest +} + +""" +Represents a 'disconnected' event on a given issue or pull request. +""" +type DisconnectedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the DisconnectedEvent object + """ + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Issue or pull request from which the issue was disconnected. + """ + source: ReferencedSubject! + + """ + Issue or pull request which was disconnected. + """ + subject: ReferencedSubject! +} + +""" +A discussion in a repository. +""" +type Discussion implements Closable & Comment & Deletable & Labelable & Lockable & Node & Reactable & RepositoryNode & Subscribable & Updatable & Votable { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + The comment chosen as this discussion's answer, if any. + """ + answer: DiscussionComment + + """ + The time when a user chose this discussion's answer, if answered. + """ + answerChosenAt: DateTime + + """ + The user who chose this discussion's answer, if answered. + """ + answerChosenBy: Actor + + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The main text of the discussion post. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + The category for this discussion. + """ + category: DiscussionCategory! + + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + The replies to the discussion. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DiscussionCommentConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The Node ID of the Discussion object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Only return answered/unanswered discussions + """ + isAnswered: Boolean + + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + `true` if the object is locked + """ + locked: Boolean! + + """ + The number identifying this discussion within the repository. + """ + number: Int! + + """ + The poll associated with this discussion, if one exists. + """ + poll: DiscussionPoll + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The path for this discussion. + """ + resourcePath: URI! + + """ + Identifies the reason for the discussion's state. + """ + stateReason: DiscussionStateReason + + """ + The title of this discussion. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + Number of upvotes that this subject has received. + """ + upvoteCount: Int! + + """ + The URL for this discussion. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Indicates if the viewer can edit labels for this object. + """ + viewerCanLabel: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Whether or not the current user can add or remove an upvote on this subject. + """ + viewerCanUpvote: Boolean! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Whether or not the current user has already upvoted this subject. + """ + viewerHasUpvoted: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +A category for discussions in a repository. +""" +type DiscussionCategory implements Node & RepositoryNode { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + A description of this category. + """ + description: String + + """ + An emoji representing this category. + """ + emoji: String! + + """ + This category's emoji rendered as HTML. + """ + emojiHTML: HTML! + + """ + The Node ID of the DiscussionCategory object + """ + id: ID! + + """ + Whether or not discussions in this category support choosing an answer with the markDiscussionCommentAsAnswer mutation. + """ + isAnswerable: Boolean! + + """ + The name of this category. + """ + name: String! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The slug of this category. + """ + slug: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for DiscussionCategory. +""" +type DiscussionCategoryConnection { + """ + A list of edges. + """ + edges: [DiscussionCategoryEdge] + + """ + A list of nodes. + """ + nodes: [DiscussionCategory] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DiscussionCategoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DiscussionCategory +} + +""" +The possible reasons for closing a discussion. +""" +enum DiscussionCloseReason { + """ + The discussion is a duplicate of another + """ + DUPLICATE + + """ + The discussion is no longer relevant + """ + OUTDATED + + """ + The discussion has been resolved + """ + RESOLVED +} + +""" +A comment on a discussion. +""" +type DiscussionComment implements Comment & Deletable & Minimizable & Node & Reactable & Updatable & UpdatableComment & Votable { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The time when this replied-to comment was deleted + """ + deletedAt: DateTime + + """ + The discussion this comment was created in + """ + discussion: Discussion + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The Node ID of the DiscussionComment object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Has this comment been chosen as the answer of its discussion? + """ + isAnswer: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The threaded replies to this comment. + """ + replies( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DiscussionCommentConnection! + + """ + The discussion comment this comment is a reply to + """ + replyTo: DiscussionComment + + """ + The path for this discussion comment. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + Number of upvotes that this subject has received. + """ + upvoteCount: Int! + + """ + The URL for this discussion comment. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can the current user mark this comment as an answer? + """ + viewerCanMarkAsAnswer: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Can the current user unmark this comment as an answer? + """ + viewerCanUnmarkAsAnswer: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Whether or not the current user can add or remove an upvote on this subject. + """ + viewerCanUpvote: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Whether or not the current user has already upvoted this subject. + """ + viewerHasUpvoted: Boolean! +} + +""" +The connection type for DiscussionComment. +""" +type DiscussionCommentConnection { + """ + A list of edges. + """ + edges: [DiscussionCommentEdge] + + """ + A list of nodes. + """ + nodes: [DiscussionComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DiscussionCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DiscussionComment +} + +""" +The connection type for Discussion. +""" +type DiscussionConnection { + """ + A list of edges. + """ + edges: [DiscussionEdge] + + """ + A list of nodes. + """ + nodes: [Discussion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DiscussionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Discussion +} + +""" +Ways in which lists of discussions can be ordered upon return. +""" +input DiscussionOrder { + """ + The direction in which to order discussions by the specified field. + """ + direction: OrderDirection! + + """ + The field by which to order discussions. + """ + field: DiscussionOrderField! +} + +""" +Properties by which discussion connections can be ordered. +""" +enum DiscussionOrderField { + """ + Order discussions by creation time. + """ + CREATED_AT + + """ + Order discussions by most recent modification time. + """ + UPDATED_AT +} + +""" +A poll for a discussion. +""" +type DiscussionPoll implements Node { + """ + The discussion that this poll belongs to. + """ + discussion: Discussion + + """ + The Node ID of the DiscussionPoll object + """ + id: ID! + + """ + The options for this poll. + """ + options( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + How to order the options for the discussion poll. + """ + orderBy: DiscussionPollOptionOrder = {field: AUTHORED_ORDER, direction: ASC} + ): DiscussionPollOptionConnection + + """ + The question that is being asked by this poll. + """ + question: String! + + """ + The total number of votes that have been cast for this poll. + """ + totalVoteCount: Int! + + """ + Indicates if the viewer has permission to vote in this poll. + """ + viewerCanVote: Boolean! + + """ + Indicates if the viewer has voted for any option in this poll. + """ + viewerHasVoted: Boolean! +} + +""" +An option for a discussion poll. +""" +type DiscussionPollOption implements Node { + """ + The Node ID of the DiscussionPollOption object + """ + id: ID! + + """ + The text for this option. + """ + option: String! + + """ + The discussion poll that this option belongs to. + """ + poll: DiscussionPoll + + """ + The total number of votes that have been cast for this option. + """ + totalVoteCount: Int! + + """ + Indicates if the viewer has voted for this option in the poll. + """ + viewerHasVoted: Boolean! +} + +""" +The connection type for DiscussionPollOption. +""" +type DiscussionPollOptionConnection { + """ + A list of edges. + """ + edges: [DiscussionPollOptionEdge] + + """ + A list of nodes. + """ + nodes: [DiscussionPollOption] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DiscussionPollOptionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DiscussionPollOption +} + +""" +Ordering options for discussion poll option connections. +""" +input DiscussionPollOptionOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order poll options by. + """ + field: DiscussionPollOptionOrderField! +} + +""" +Properties by which discussion poll option connections can be ordered. +""" +enum DiscussionPollOptionOrderField { + """ + Order poll options by the order that the poll author specified when creating the poll. + """ + AUTHORED_ORDER + + """ + Order poll options by the number of votes it has. + """ + VOTE_COUNT +} + +""" +The possible states of a discussion. +""" +enum DiscussionState { + """ + A discussion that has been closed + """ + CLOSED + + """ + A discussion that is open + """ + OPEN +} + +""" +The possible state reasons of a discussion. +""" +enum DiscussionStateReason { + """ + The discussion is a duplicate of another + """ + DUPLICATE + + """ + The discussion is no longer relevant + """ + OUTDATED + + """ + The discussion was reopened + """ + REOPENED + + """ + The discussion has been resolved + """ + RESOLVED +} + +""" +Autogenerated input type of DismissPullRequestReview +""" +input DismissPullRequestReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The contents of the pull request review dismissal message. + """ + message: String! + + """ + The Node ID of the pull request review to modify. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of DismissPullRequestReview. +""" +type DismissPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The dismissed pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +The possible reasons that a Dependabot alert was dismissed. +""" +enum DismissReason { + """ + A fix has already been started + """ + FIX_STARTED + + """ + This alert is inaccurate or incorrect + """ + INACCURATE + + """ + Vulnerable code is not actually used + """ + NOT_USED + + """ + No bandwidth to fix this + """ + NO_BANDWIDTH + + """ + Risk is tolerable to this project + """ + TOLERABLE_RISK +} + +""" +Autogenerated input type of DismissRepositoryVulnerabilityAlert +""" +input DismissRepositoryVulnerabilityAlertInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The reason the Dependabot alert is being dismissed. + """ + dismissReason: DismissReason! + + """ + The Dependabot alert ID to dismiss. + """ + repositoryVulnerabilityAlertId: ID! @possibleTypes(concreteTypes: ["RepositoryVulnerabilityAlert"]) +} + +""" +Autogenerated return type of DismissRepositoryVulnerabilityAlert. +""" +type DismissRepositoryVulnerabilityAlertPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Dependabot alert that was dismissed + """ + repositoryVulnerabilityAlert: RepositoryVulnerabilityAlert +} + +""" +A draft issue within a project. +""" +type DraftIssue implements Node { + """ + A list of users to assigned to this draft issue. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The body of the draft issue. + """ + body: String! + + """ + The body of the draft issue rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body of the draft issue rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created this draft issue. + """ + creator: Actor + + """ + The Node ID of the DraftIssue object + """ + id: ID! + + """ + List of items linked with the draft issue (currently draft issue can be linked to only one item). + """ + projectV2Items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ItemConnection! + + """ + Projects that link to this draft issue (currently draft issue can be linked to only one project). + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! + + """ + The title of the draft issue + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Specifies a review comment to be left with a Pull Request Review. +""" +input DraftPullRequestReviewComment { + """ + Body of the comment to leave. + """ + body: String! + + """ + Path to the file being commented on. + """ + path: String! + + """ + Position in the file to leave a comment on. + """ + position: Int! +} + +""" +Specifies a review comment thread to be left with a Pull Request Review. +""" +input DraftPullRequestReviewThread { + """ + Body of the comment to leave. + """ + body: String! + + """ + The line of the blob to which the thread refers. The end of the line range for + multi-line comments. Required if not using positioning. + """ + line: Int + + """ + Path to the file being commented on. Required if not using positioning. + """ + path: String + + """ + The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. + """ + side: DiffSide = RIGHT + + """ + The first line of the range to which the comment refers. + """ + startLine: Int + + """ + The side of the diff on which the start line resides. + """ + startSide: DiffSide = RIGHT +} + +""" +The Exploit Prediction Scoring System +""" +type EPSS { + """ + The EPSS percentage represents the likelihood of a CVE being exploited. + """ + percentage: Float + + """ + The EPSS percentile represents the relative rank of the CVE's likelihood of being exploited compared to other CVEs. + """ + percentile: Float +} + +""" +Autogenerated input type of EnablePullRequestAutoMerge +""" +input EnablePullRequestAutoMergeInput { + """ + The email address to associate with this merge. + """ + authorEmail: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Commit body to use for the commit when the PR is mergable; if omitted, a + default message will be used. NOTE: when merging with a merge queue any input + value for commit message is ignored. + """ + commitBody: String + + """ + Commit headline to use for the commit when the PR is mergable; if omitted, a + default message will be used. NOTE: when merging with a merge queue any input + value for commit headline is ignored. + """ + commitHeadline: String + + """ + The expected head OID of the pull request. + """ + expectedHeadOid: GitObjectID + + """ + The merge method to use. If omitted, defaults to `MERGE`. NOTE: when merging + with a merge queue any input value for merge method is ignored. + """ + mergeMethod: PullRequestMergeMethod = MERGE + + """ + ID of the pull request to enable auto-merge on. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of EnablePullRequestAutoMerge. +""" +type EnablePullRequestAutoMergePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request auto-merge was enabled on. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of EnqueuePullRequest +""" +input EnqueuePullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The expected head OID of the pull request. + """ + expectedHeadOid: GitObjectID + + """ + Add the pull request to the front of the queue. + """ + jump: Boolean + + """ + The ID of the pull request to enqueue. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of EnqueuePullRequest. +""" +type EnqueuePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The merge queue entry for the enqueued pull request. + """ + mergeQueueEntry: MergeQueueEntry +} + +""" +An account to manage multiple organizations with consolidated policy and billing. +""" +type Enterprise implements Node { + """ + The announcement banner set on this enterprise, if any. Only visible to members of the enterprise. + """ + announcementBanner: AnnouncementBanner + + """ + A URL pointing to the enterprise's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The enterprise's billing email. + """ + billingEmail: String + + """ + Enterprise billing information visible to enterprise billing managers. + """ + billingInfo: EnterpriseBillingInfo + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the enterprise. + """ + description: String + + """ + The description of the enterprise as HTML. + """ + descriptionHTML: HTML! + + """ + The Node ID of the Enterprise object + """ + id: ID! + + """ + The location of the enterprise. + """ + location: String + + """ + A list of users who are members of this enterprise. + """ + members( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Only return members within the selected GitHub Enterprise deployment + """ + deployment: EnterpriseUserDeployment + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Only return members with this two-factor authentication status. Does not + include members who only have an account on a GitHub Enterprise Server instance. + + **Upcoming Change on 2025-04-01 UTC** + **Description:** `hasTwoFactorEnabled` will be removed. Use `two_factor_method_security` instead. + **Reason:** `has_two_factor_enabled` will be removed. + """ + hasTwoFactorEnabled: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for members returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + Only return members within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + + """ + The role of the user in the enterprise organization or server. + """ + role: EnterpriseUserAccountMembershipRole + + """ + Only return members with this type of two-factor authentication method. Does + not include members who only have an account on a GitHub Enterprise Server instance. + """ + twoFactorMethodSecurity: TwoFactorCredentialSecurityType = null + ): EnterpriseMemberConnection! + + """ + The name of the enterprise. + """ + name: String! + + """ + A list of organizations that belong to this enterprise. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations returned from the connection. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + The viewer's role in an organization. + """ + viewerOrganizationRole: RoleInOrganization + ): OrganizationConnection! + + """ + Enterprise information visible to enterprise owners or enterprise owners' + personal access tokens (classic) with read:enterprise or admin:enterprise scope. + """ + ownerInfo: EnterpriseOwnerInfo + + """ + The raw content of the enterprise README. + """ + readme: String + + """ + The content of the enterprise README as HTML. + """ + readmeHTML: HTML! + + """ + The HTTP path for this enterprise. + """ + resourcePath: URI! + + """ + Returns a single ruleset from the current enterprise by ID. + """ + ruleset( + """ + The ID of the ruleset to be returned. + """ + databaseId: Int! + ): RepositoryRuleset + + """ + A list of rulesets for this enterprise. + """ + rulesets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryRulesetConnection + + """ + The URL-friendly identifier for the enterprise. + """ + slug: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this enterprise. + """ + url: URI! + + """ + A list of repositories that belong to users. Only available for enterprises with Enterprise Managed Users. + """ + userNamespaceRepositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection. + """ + orderBy: RepositoryOrder = {field: NAME, direction: ASC} + + """ + The search string to look for. + """ + query: String + ): UserNamespaceRepositoryConnection! + + """ + Is the current viewer an admin of this enterprise? + """ + viewerIsAdmin: Boolean! + + """ + The URL of the enterprise website. + """ + websiteUrl: URI +} + +""" +The connection type for User. +""" +type EnterpriseAdministratorConnection { + """ + A list of edges. + """ + edges: [EnterpriseAdministratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is an administrator of an enterprise. +""" +type EnterpriseAdministratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User + + """ + The role of the administrator. + """ + role: EnterpriseAdministratorRole! +} + +""" +An invitation for a user to become an owner or billing manager of an enterprise. +""" +type EnterpriseAdministratorInvitation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email of the person who was invited to the enterprise. + """ + email: String + + """ + The enterprise the invitation is for. + """ + enterprise: Enterprise! + + """ + The Node ID of the EnterpriseAdministratorInvitation object + """ + id: ID! + + """ + The user who was invited to the enterprise. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User + + """ + The invitee's pending role in the enterprise (owner or billing_manager). + """ + role: EnterpriseAdministratorRole! +} + +""" +The connection type for EnterpriseAdministratorInvitation. +""" +type EnterpriseAdministratorInvitationConnection { + """ + A list of edges. + """ + edges: [EnterpriseAdministratorInvitationEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseAdministratorInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseAdministratorInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseAdministratorInvitation +} + +""" +Ordering options for enterprise administrator invitation connections +""" +input EnterpriseAdministratorInvitationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise administrator invitations by. + """ + field: EnterpriseAdministratorInvitationOrderField! +} + +""" +Properties by which enterprise administrator invitation connections can be ordered. +""" +enum EnterpriseAdministratorInvitationOrderField { + """ + Order enterprise administrator member invitations by creation time + """ + CREATED_AT +} + +""" +The possible administrator roles in an enterprise account. +""" +enum EnterpriseAdministratorRole { + """ + Represents a billing manager of the enterprise account. + """ + BILLING_MANAGER + + """ + Represents an owner of the enterprise account. + """ + OWNER + + """ + Unaffiliated member of the enterprise account without an admin role. + """ + UNAFFILIATED +} + +""" +The possible values for the enterprise allow private repository forking policy value. +""" +enum EnterpriseAllowPrivateRepositoryForkingPolicyValue { + """ + Members can fork a repository to an organization within this enterprise. + """ + ENTERPRISE_ORGANIZATIONS + + """ + Members can fork a repository to their enterprise-managed user account or an organization inside this enterprise. + """ + ENTERPRISE_ORGANIZATIONS_USER_ACCOUNTS + + """ + Members can fork a repository to their user account or an organization, either inside or outside of this enterprise. + """ + EVERYWHERE + + """ + Members can fork a repository only within the same organization (intra-org). + """ + SAME_ORGANIZATION + + """ + Members can fork a repository to their user account or within the same organization. + """ + SAME_ORGANIZATION_USER_ACCOUNTS + + """ + Members can fork a repository to their user account. + """ + USER_ACCOUNTS +} + +""" +Metadata for an audit entry containing enterprise account information. +""" +interface EnterpriseAuditEntryData { + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI +} + +""" +Enterprise billing information visible to enterprise billing managers and owners. +""" +type EnterpriseBillingInfo { + """ + The number of licenseable users/emails across the enterprise. + """ + allLicensableUsersCount: Int! + + """ + The number of data packs used by all organizations owned by the enterprise. + """ + assetPacks: Int! + + """ + The bandwidth quota in GB for all organizations owned by the enterprise. + """ + bandwidthQuota: Float! + + """ + The bandwidth usage in GB for all organizations owned by the enterprise. + """ + bandwidthUsage: Float! + + """ + The bandwidth usage as a percentage of the bandwidth quota. + """ + bandwidthUsagePercentage: Int! + + """ + The storage quota in GB for all organizations owned by the enterprise. + """ + storageQuota: Float! + + """ + The storage usage in GB for all organizations owned by the enterprise. + """ + storageUsage: Float! + + """ + The storage usage as a percentage of the storage quota. + """ + storageUsagePercentage: Int! + + """ + The number of available licenses across all owned organizations based on the unique number of billable users. + """ + totalAvailableLicenses: Int! + + """ + The total number of licenses allocated. + """ + totalLicenses: Int! +} + +""" +The connection type for Enterprise. +""" +type EnterpriseConnection { + """ + A list of edges. + """ + edges: [EnterpriseEdge] + + """ + A list of nodes. + """ + nodes: [Enterprise] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The possible values for the enterprise base repository permission setting. +""" +enum EnterpriseDefaultRepositoryPermissionSettingValue { + """ + Organization members will be able to clone, pull, push, and add new collaborators to all organization repositories. + """ + ADMIN + + """ + Organization members will only be able to clone and pull public repositories. + """ + NONE + + """ + Organizations in the enterprise choose base repository permissions for their members. + """ + NO_POLICY + + """ + Organization members will be able to clone and pull all organization repositories. + """ + READ + + """ + Organization members will be able to clone, pull, and push all organization repositories. + """ + WRITE +} + +""" +The possible values for an enabled/no policy enterprise setting. +""" +enum EnterpriseDisallowedMethodsSettingValue { + """ + The setting prevents insecure 2FA methods from being used by members of the enterprise. + """ + INSECURE + + """ + There is no policy set for preventing insecure 2FA methods from being used by members of the enterprise. + """ + NO_POLICY +} + +""" +An edge in a connection. +""" +type EnterpriseEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Enterprise +} + +""" +The possible values for an enabled/disabled enterprise setting. +""" +enum EnterpriseEnabledDisabledSettingValue { + """ + The setting is disabled for organizations in the enterprise. + """ + DISABLED + + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED + + """ + There is no policy set for organizations in the enterprise. + """ + NO_POLICY +} + +""" +The possible values for an enabled/no policy enterprise setting. +""" +enum EnterpriseEnabledSettingValue { + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED + + """ + There is no policy set for organizations in the enterprise. + """ + NO_POLICY +} + +""" +The connection type for OrganizationInvitation. +""" +type EnterpriseFailedInvitationConnection { + """ + A list of edges. + """ + edges: [EnterpriseFailedInvitationEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the total count of unique users in the connection. + """ + totalUniqueUserCount: Int! +} + +""" +A failed invitation to be a member in an enterprise organization. +""" +type EnterpriseFailedInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationInvitation +} + +""" +An identity provider configured to provision identities for an enterprise. +Visible to enterprise owners or enterprise owners' personal access tokens +(classic) with read:enterprise or admin:enterprise scope. +""" +type EnterpriseIdentityProvider implements Node { + """ + The digest algorithm used to sign SAML requests for the identity provider. + """ + digestMethod: SamlDigestAlgorithm + + """ + The enterprise this identity provider belongs to. + """ + enterprise: Enterprise + + """ + ExternalIdentities provisioned by this identity provider. + """ + externalIdentities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter to external identities with the users login + """ + login: String + + """ + Filter to external identities with valid org membership only + """ + membersOnly: Boolean + + """ + Filter to external identities with the users userName/NameID attribute + """ + userName: String + ): ExternalIdentityConnection! + + """ + The Node ID of the EnterpriseIdentityProvider object + """ + id: ID! + + """ + The x509 certificate used by the identity provider to sign assertions and responses. + """ + idpCertificate: X509Certificate + + """ + The Issuer Entity ID for the SAML identity provider. + """ + issuer: String + + """ + Recovery codes that can be used by admins to access the enterprise if the identity provider is unavailable. + """ + recoveryCodes: [String!] + + """ + The signature algorithm used to sign SAML requests for the identity provider. + """ + signatureMethod: SamlSignatureAlgorithm + + """ + The URL endpoint for the identity provider's SAML SSO. + """ + ssoUrl: URI +} + +""" +An object that is a member of an enterprise. +""" +union EnterpriseMember = EnterpriseUserAccount | User + +""" +The connection type for EnterpriseMember. +""" +type EnterpriseMemberConnection { + """ + A list of edges. + """ + edges: [EnterpriseMemberEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseMember] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is a member of an enterprise through one or more organizations. +""" +type EnterpriseMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseMember +} + +""" +An invitation for a user to become an unaffiliated member of an enterprise. +""" +type EnterpriseMemberInvitation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email of the person who was invited to the enterprise. + """ + email: String + + """ + The enterprise the invitation is for. + """ + enterprise: Enterprise! + + """ + The Node ID of the EnterpriseMemberInvitation object + """ + id: ID! + + """ + The user who was invited to the enterprise. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User +} + +""" +The connection type for EnterpriseMemberInvitation. +""" +type EnterpriseMemberInvitationConnection { + """ + A list of edges. + """ + edges: [EnterpriseMemberInvitationEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseMemberInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseMemberInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseMemberInvitation +} + +""" +Ordering options for enterprise administrator invitation connections +""" +input EnterpriseMemberInvitationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise member invitations by. + """ + field: EnterpriseMemberInvitationOrderField! +} + +""" +Properties by which enterprise member invitation connections can be ordered. +""" +enum EnterpriseMemberInvitationOrderField { + """ + Order enterprise member invitations by creation time + """ + CREATED_AT +} + +""" +Ordering options for enterprise member connections. +""" +input EnterpriseMemberOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise members by. + """ + field: EnterpriseMemberOrderField! +} + +""" +Properties by which enterprise member connections can be ordered. +""" +enum EnterpriseMemberOrderField { + """ + Order enterprise members by creation time + """ + CREATED_AT + + """ + Order enterprise members by login + """ + LOGIN +} + +""" +The possible values for the enterprise members can create repositories setting. +""" +enum EnterpriseMembersCanCreateRepositoriesSettingValue { + """ + Members will be able to create public and private repositories. + """ + ALL + + """ + Members will not be able to create public or private repositories. + """ + DISABLED + + """ + Organization owners choose whether to allow members to create repositories. + """ + NO_POLICY + + """ + Members will be able to create only private repositories. + """ + PRIVATE + + """ + Members will be able to create only public repositories. + """ + PUBLIC +} + +""" +The possible values for the members can make purchases setting. +""" +enum EnterpriseMembersCanMakePurchasesSettingValue { + """ + The setting is disabled for organizations in the enterprise. + """ + DISABLED + + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED +} + +""" +The possible values we have for filtering Platform::Objects::User#enterprises. +""" +enum EnterpriseMembershipType { + """ + Returns all enterprises in which the user is an admin. + """ + ADMIN + + """ + Returns all enterprises in which the user is a member, admin, or billing manager. + """ + ALL + + """ + Returns all enterprises in which the user is a billing manager. + """ + BILLING_MANAGER + + """ + Returns all enterprises in which the user is a member of an org that is owned by the enterprise. + """ + ORG_MEMBERSHIP +} + +""" +Ordering options for enterprises. +""" +input EnterpriseOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprises by. + """ + field: EnterpriseOrderField! +} + +""" +Properties by which enterprise connections can be ordered. +""" +enum EnterpriseOrderField { + """ + Order enterprises by name + """ + NAME +} + +""" +The connection type for Organization. +""" +type EnterpriseOrganizationMembershipConnection { + """ + A list of edges. + """ + edges: [EnterpriseOrganizationMembershipEdge] + + """ + A list of nodes. + """ + nodes: [Organization] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An enterprise organization that a user is a member of. +""" +type EnterpriseOrganizationMembershipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Organization + + """ + The role of the user in the enterprise membership. + """ + role: EnterpriseUserAccountMembershipRole! +} + +""" +The connection type for User. +""" +type EnterpriseOutsideCollaboratorConnection { + """ + A list of edges. + """ + edges: [EnterpriseOutsideCollaboratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is an outside collaborator of an enterprise through one or more organizations. +""" +type EnterpriseOutsideCollaboratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User + + """ + The enterprise organization repositories this user is a member of. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories. + """ + orderBy: RepositoryOrder = {field: NAME, direction: ASC} + ): EnterpriseRepositoryInfoConnection! +} + +""" +Enterprise information visible to enterprise owners or enterprise owners' +personal access tokens (classic) with read:enterprise or admin:enterprise scope. +""" +type EnterpriseOwnerInfo { + """ + A list of all of the administrators for this enterprise. + """ + admins( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Only return administrators with this two-factor authentication status. + + **Upcoming Change on 2025-04-01 UTC** + **Description:** `hasTwoFactorEnabled` will be removed. Use `two_factor_method_security` instead. + **Reason:** `has_two_factor_enabled` will be removed. + """ + hasTwoFactorEnabled: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for administrators returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + Only return members within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + + """ + The role to filter by. + """ + role: EnterpriseAdministratorRole + + """ + Only return outside collaborators with this type of two-factor authentication method. + """ + twoFactorMethodSecurity: TwoFactorCredentialSecurityType = null + ): EnterpriseAdministratorConnection! + + """ + A list of users in the enterprise who currently have two-factor authentication disabled. + """ + affiliatedUsersWithTwoFactorDisabled( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + Whether or not affiliated users with two-factor authentication disabled exist in the enterprise. + """ + affiliatedUsersWithTwoFactorDisabledExist: Boolean! + + """ + The setting value for whether private repository forking is enabled for repositories in organizations in this enterprise. + """ + allowPrivateRepositoryForkingSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided private repository forking setting value. + """ + allowPrivateRepositoryForkingSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The value for the allow private repository forking policy on the enterprise. + """ + allowPrivateRepositoryForkingSettingPolicyValue: EnterpriseAllowPrivateRepositoryForkingPolicyValue + + """ + The setting value for base repository permissions for organizations in this enterprise. + """ + defaultRepositoryPermissionSetting: EnterpriseDefaultRepositoryPermissionSettingValue! + + """ + A list of enterprise organizations configured with the provided base repository permission. + """ + defaultRepositoryPermissionSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The permission to find organizations for. + """ + value: DefaultRepositoryPermissionField! + ): OrganizationConnection! + + """ + A list of domains owned by the enterprise. Visible to enterprise owners or + enterprise owners' personal access tokens (classic) with admin:enterprise scope. + """ + domains( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter whether or not the domain is approved. + """ + isApproved: Boolean = null + + """ + Filter whether or not the domain is verified. + """ + isVerified: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for verifiable domains returned. + """ + orderBy: VerifiableDomainOrder = {field: DOMAIN, direction: ASC} + ): VerifiableDomainConnection! + + """ + Enterprise Server installations owned by the enterprise. + """ + enterpriseServerInstallations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Whether or not to only return installations discovered via GitHub Connect. + """ + connectedOnly: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server installations returned. + """ + orderBy: EnterpriseServerInstallationOrder = {field: HOST_NAME, direction: ASC} + ): EnterpriseServerInstallationConnection! + + """ + A list of failed invitations in the enterprise. + """ + failedInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The search string to look for. + """ + query: String + ): EnterpriseFailedInvitationConnection! + + """ + The setting value for whether the enterprise has an IP allow list enabled. + """ + ipAllowListEnabledSetting: IpAllowListEnabledSettingValue! + + """ + The IP addresses that are allowed to access resources owned by the enterprise. + Visible to enterprise owners or enterprise owners' personal access tokens + (classic) with admin:enterprise scope. + """ + ipAllowListEntries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for IP allow list entries returned. + """ + orderBy: IpAllowListEntryOrder = {field: ALLOW_LIST_VALUE, direction: ASC} + ): IpAllowListEntryConnection! + + """ + The setting value for whether the enterprise has IP allow list configuration for installed GitHub Apps enabled. + """ + ipAllowListForInstalledAppsEnabledSetting: IpAllowListForInstalledAppsEnabledSettingValue! + + """ + Whether or not the base repository permission is currently being updated. + """ + isUpdatingDefaultRepositoryPermission: Boolean! + + """ + Whether the two-factor authentication requirement is currently being enforced. + """ + isUpdatingTwoFactorRequirement: Boolean! + + """ + The setting value for whether organization members with admin permissions on a + repository can change repository visibility. + """ + membersCanChangeRepositoryVisibilitySetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided can change repository visibility setting value. + """ + membersCanChangeRepositoryVisibilitySettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members of organizations in the enterprise can create internal repositories. + """ + membersCanCreateInternalRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create private repositories. + """ + membersCanCreatePrivateRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create public repositories. + """ + membersCanCreatePublicRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create repositories. + """ + membersCanCreateRepositoriesSetting: EnterpriseMembersCanCreateRepositoriesSettingValue + + """ + A list of enterprise organizations configured with the provided repository creation setting value. + """ + membersCanCreateRepositoriesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting to find organizations for. + """ + value: OrganizationMembersCanCreateRepositoriesSettingValue! + ): OrganizationConnection! + + """ + The setting value for whether members with admin permissions for repositories can delete issues. + """ + membersCanDeleteIssuesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can delete issues setting value. + """ + membersCanDeleteIssuesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members with admin permissions for repositories can delete or transfer repositories. + """ + membersCanDeleteRepositoriesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can delete repositories setting value. + """ + membersCanDeleteRepositoriesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members of organizations in the enterprise can invite outside collaborators. + """ + membersCanInviteCollaboratorsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can invite collaborators setting value. + """ + membersCanInviteCollaboratorsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + Indicates whether members of this enterprise's organizations can purchase additional services for those organizations. + """ + membersCanMakePurchasesSetting: EnterpriseMembersCanMakePurchasesSettingValue! + + """ + The setting value for whether members with admin permissions for repositories can update protected branches. + """ + membersCanUpdateProtectedBranchesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can update protected branches setting value. + """ + membersCanUpdateProtectedBranchesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members can view dependency insights. + """ + membersCanViewDependencyInsightsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can view dependency insights setting value. + """ + membersCanViewDependencyInsightsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + Indicates if email notification delivery for this enterprise is restricted to verified or approved domains. + """ + notificationDeliveryRestrictionEnabledSetting: NotificationRestrictionSettingValue! + + """ + The OIDC Identity Provider for the enterprise. + """ + oidcProvider: OIDCProvider + + """ + The setting value for whether organization projects are enabled for organizations in this enterprise. + """ + organizationProjectsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided organization projects setting value. + """ + organizationProjectsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + A list of outside collaborators across the repositories in the enterprise. + """ + outsideCollaborators( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Only return outside collaborators with this two-factor authentication status. + + **Upcoming Change on 2025-04-01 UTC** + **Description:** `hasTwoFactorEnabled` will be removed. Use `two_factor_method_security` instead. + **Reason:** `has_two_factor_enabled` will be removed. + """ + hasTwoFactorEnabled: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The login of one specific outside collaborator. + """ + login: String + + """ + Ordering options for outside collaborators returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + Only return outside collaborators within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + + """ + Only return outside collaborators with this type of two-factor authentication method. + """ + twoFactorMethodSecurity: TwoFactorCredentialSecurityType = null + + """ + Only return outside collaborators on repositories with this visibility. + """ + visibility: RepositoryVisibility + ): EnterpriseOutsideCollaboratorConnection! + + """ + A list of pending administrator invitations for the enterprise. + """ + pendingAdminInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pending enterprise administrator invitations returned from the connection. + """ + orderBy: EnterpriseAdministratorInvitationOrder = {field: CREATED_AT, direction: DESC} + + """ + The search string to look for. + """ + query: String + + """ + The role to filter by. + """ + role: EnterpriseAdministratorRole + ): EnterpriseAdministratorInvitationConnection! + + """ + A list of pending collaborator invitations across the repositories in the enterprise. + """ + pendingCollaboratorInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pending repository collaborator invitations returned from the connection. + """ + orderBy: RepositoryInvitationOrder = {field: CREATED_AT, direction: DESC} + + """ + The search string to look for. + """ + query: String + ): RepositoryInvitationConnection! + + """ + A list of pending member invitations for organizations in the enterprise. + """ + pendingMemberInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Only return invitations matching this invitation source + """ + invitationSource: OrganizationInvitationSource + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Only return invitations within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + ): EnterprisePendingMemberInvitationConnection! + + """ + A list of pending unaffiliated member invitations for the enterprise. + """ + pendingUnaffiliatedMemberInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pending enterprise member invitations returned from the connection. + """ + orderBy: EnterpriseMemberInvitationOrder = {field: CREATED_AT, direction: DESC} + + """ + The search string to look for. + """ + query: String + ): EnterpriseMemberInvitationConnection! + + """ + The setting value for whether deploy keys are enabled for repositories in organizations in this enterprise. + """ + repositoryDeployKeySetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided deploy keys setting value. + """ + repositoryDeployKeySettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether repository projects are enabled in this enterprise. + """ + repositoryProjectsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided repository projects setting value. + """ + repositoryProjectsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The SAML Identity Provider for the enterprise. + """ + samlIdentityProvider: EnterpriseIdentityProvider + + """ + A list of enterprise organizations configured with the SAML single sign-on setting value. + """ + samlIdentityProviderSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: IdentityProviderConfigurationState! + ): OrganizationConnection! + + """ + A list of members with a support entitlement. + """ + supportEntitlements( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for support entitlement users returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + ): EnterpriseMemberConnection! + + """ + The setting value for whether team discussions are enabled for organizations in this enterprise. + """ + teamDiscussionsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided team discussions setting value. + """ + teamDiscussionsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for what methods of two-factor authentication the enterprise prevents its users from having. + """ + twoFactorDisallowedMethodsSetting: EnterpriseDisallowedMethodsSettingValue! + + """ + The setting value for whether the enterprise requires two-factor authentication for its organizations and users. + """ + twoFactorRequiredSetting: EnterpriseEnabledSettingValue! + + """ + A list of enterprise organizations configured with the two-factor authentication setting value. + """ + twoFactorRequiredSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! +} + +""" +The connection type for OrganizationInvitation. +""" +type EnterprisePendingMemberInvitationConnection { + """ + A list of edges. + """ + edges: [EnterprisePendingMemberInvitationEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the total count of unique users in the connection. + """ + totalUniqueUserCount: Int! +} + +""" +An invitation to be a member in an enterprise organization. +""" +type EnterprisePendingMemberInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationInvitation +} + +""" +A subset of repository information queryable from an enterprise. +""" +type EnterpriseRepositoryInfo implements Node { + """ + The Node ID of the EnterpriseRepositoryInfo object + """ + id: ID! + + """ + Identifies if the repository is private or internal. + """ + isPrivate: Boolean! + + """ + The repository's name. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! +} + +""" +The connection type for EnterpriseRepositoryInfo. +""" +type EnterpriseRepositoryInfoConnection { + """ + A list of edges. + """ + edges: [EnterpriseRepositoryInfoEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseRepositoryInfo] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseRepositoryInfoEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseRepositoryInfo +} + +""" +An Enterprise Server installation. +""" +type EnterpriseServerInstallation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The customer name to which the Enterprise Server installation belongs. + """ + customerName: String! + + """ + The host name of the Enterprise Server installation. + """ + hostName: String! + + """ + The Node ID of the EnterpriseServerInstallation object + """ + id: ID! + + """ + Whether or not the installation is connected to an Enterprise Server installation via GitHub Connect. + """ + isConnected: Boolean! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + User accounts on this Enterprise Server installation. + """ + userAccounts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user accounts returned from the connection. + """ + orderBy: EnterpriseServerUserAccountOrder = {field: LOGIN, direction: ASC} + ): EnterpriseServerUserAccountConnection! + + """ + User accounts uploads for the Enterprise Server installation. + """ + userAccountsUploads( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user accounts uploads returned from the connection. + """ + orderBy: EnterpriseServerUserAccountsUploadOrder = {field: CREATED_AT, direction: DESC} + ): EnterpriseServerUserAccountsUploadConnection! +} + +""" +The connection type for EnterpriseServerInstallation. +""" +type EnterpriseServerInstallationConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerInstallationEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerInstallation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerInstallationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerInstallation +} + +""" +The connection type for EnterpriseServerInstallation. +""" +type EnterpriseServerInstallationMembershipConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerInstallationMembershipEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerInstallation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An Enterprise Server installation that a user is a member of. +""" +type EnterpriseServerInstallationMembershipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerInstallation + + """ + The role of the user in the enterprise membership. + """ + role: EnterpriseUserAccountMembershipRole! +} + +""" +Ordering options for Enterprise Server installation connections. +""" +input EnterpriseServerInstallationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order Enterprise Server installations by. + """ + field: EnterpriseServerInstallationOrderField! +} + +""" +Properties by which Enterprise Server installation connections can be ordered. +""" +enum EnterpriseServerInstallationOrderField { + """ + Order Enterprise Server installations by creation time + """ + CREATED_AT + + """ + Order Enterprise Server installations by customer name + """ + CUSTOMER_NAME + + """ + Order Enterprise Server installations by host name + """ + HOST_NAME +} + +""" +A user account on an Enterprise Server installation. +""" +type EnterpriseServerUserAccount implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + User emails belonging to this user account. + """ + emails( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user account emails returned from the connection. + """ + orderBy: EnterpriseServerUserAccountEmailOrder = {field: EMAIL, direction: ASC} + ): EnterpriseServerUserAccountEmailConnection! + + """ + The Enterprise Server installation on which this user account exists. + """ + enterpriseServerInstallation: EnterpriseServerInstallation! + + """ + The Node ID of the EnterpriseServerUserAccount object + """ + id: ID! + + """ + Whether the user account is a site administrator on the Enterprise Server installation. + """ + isSiteAdmin: Boolean! + + """ + The login of the user account on the Enterprise Server installation. + """ + login: String! + + """ + The profile name of the user account on the Enterprise Server installation. + """ + profileName: String + + """ + The date and time when the user account was created on the Enterprise Server installation. + """ + remoteCreatedAt: DateTime! + + """ + The ID of the user account on the Enterprise Server installation. + """ + remoteUserId: Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for EnterpriseServerUserAccount. +""" +type EnterpriseServerUserAccountConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccount] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccount +} + +""" +An email belonging to a user account on an Enterprise Server installation. +""" +type EnterpriseServerUserAccountEmail implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email address. + """ + email: String! + + """ + The Node ID of the EnterpriseServerUserAccountEmail object + """ + id: ID! + + """ + Indicates whether this is the primary email of the associated user account. + """ + isPrimary: Boolean! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user account to which the email belongs. + """ + userAccount: EnterpriseServerUserAccount! +} + +""" +The connection type for EnterpriseServerUserAccountEmail. +""" +type EnterpriseServerUserAccountEmailConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountEmailEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccountEmail] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountEmailEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccountEmail +} + +""" +Ordering options for Enterprise Server user account email connections. +""" +input EnterpriseServerUserAccountEmailOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order emails by. + """ + field: EnterpriseServerUserAccountEmailOrderField! +} + +""" +Properties by which Enterprise Server user account email connections can be ordered. +""" +enum EnterpriseServerUserAccountEmailOrderField { + """ + Order emails by email + """ + EMAIL +} + +""" +Ordering options for Enterprise Server user account connections. +""" +input EnterpriseServerUserAccountOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user accounts by. + """ + field: EnterpriseServerUserAccountOrderField! +} + +""" +Properties by which Enterprise Server user account connections can be ordered. +""" +enum EnterpriseServerUserAccountOrderField { + """ + Order user accounts by login + """ + LOGIN + + """ + Order user accounts by creation time on the Enterprise Server installation + """ + REMOTE_CREATED_AT +} + +""" +A user accounts upload from an Enterprise Server installation. +""" +type EnterpriseServerUserAccountsUpload implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The enterprise to which this upload belongs. + """ + enterprise: Enterprise! + + """ + The Enterprise Server installation for which this upload was generated. + """ + enterpriseServerInstallation: EnterpriseServerInstallation! + + """ + The Node ID of the EnterpriseServerUserAccountsUpload object + """ + id: ID! + + """ + The name of the file uploaded. + """ + name: String! + + """ + The synchronization state of the upload + """ + syncState: EnterpriseServerUserAccountsUploadSyncState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for EnterpriseServerUserAccountsUpload. +""" +type EnterpriseServerUserAccountsUploadConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountsUploadEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccountsUpload] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountsUploadEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccountsUpload +} + +""" +Ordering options for Enterprise Server user accounts upload connections. +""" +input EnterpriseServerUserAccountsUploadOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user accounts uploads by. + """ + field: EnterpriseServerUserAccountsUploadOrderField! +} + +""" +Properties by which Enterprise Server user accounts upload connections can be ordered. +""" +enum EnterpriseServerUserAccountsUploadOrderField { + """ + Order user accounts uploads by creation time + """ + CREATED_AT +} + +""" +Synchronization state of the Enterprise Server user accounts upload +""" +enum EnterpriseServerUserAccountsUploadSyncState { + """ + The synchronization of the upload failed. + """ + FAILURE + + """ + The synchronization of the upload is pending. + """ + PENDING + + """ + The synchronization of the upload succeeded. + """ + SUCCESS +} + +""" +An account for a user who is an admin of an enterprise or a member of an enterprise through one or more organizations. +""" +type EnterpriseUserAccount implements Actor & Node { + """ + A URL pointing to the enterprise user account's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The enterprise in which this user account exists. + """ + enterprise: Enterprise! + + """ + A list of Enterprise Server installations this user is a member of. + """ + enterpriseInstallations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for installations returned from the connection. + """ + orderBy: EnterpriseServerInstallationOrder = {field: HOST_NAME, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + The role of the user in the installation. + """ + role: EnterpriseUserAccountMembershipRole + ): EnterpriseServerInstallationMembershipConnection! + + """ + The Node ID of the EnterpriseUserAccount object + """ + id: ID! + + """ + An identifier for the enterprise user account, a login or email address + """ + login: String! + + """ + The name of the enterprise user account + """ + name: String + + """ + A list of enterprise organizations this user is a member of. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations returned from the connection. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + The role of the user in the enterprise organization. + """ + role: EnterpriseUserAccountMembershipRole + ): EnterpriseOrganizationMembershipConnection! + + """ + The HTTP path for this user. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this user. + """ + url: URI! + + """ + The user within the enterprise. + """ + user: User +} + +""" +The possible roles for enterprise membership. +""" +enum EnterpriseUserAccountMembershipRole { + """ + The user is a member of an organization in the enterprise. + """ + MEMBER + + """ + The user is an owner of an organization in the enterprise. + """ + OWNER + + """ + The user is not an owner of the enterprise, and not a member or owner of any + organizations in the enterprise; only for EMU-enabled enterprises. + """ + UNAFFILIATED +} + +""" +The possible GitHub Enterprise deployments where this user can exist. +""" +enum EnterpriseUserDeployment { + """ + The user is part of a GitHub Enterprise Cloud deployment. + """ + CLOUD + + """ + The user is part of a GitHub Enterprise Server deployment. + """ + SERVER +} + +""" +An environment. +""" +type Environment implements Node { + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the Environment object + """ + id: ID! + + """ + Indicates whether or not this environment is currently pinned to the repository + """ + isPinned: Boolean + + """ + The latest completed deployment with status success, failure, or error if it exists + """ + latestCompletedDeployment: Deployment + + """ + The name of the environment + """ + name: String! + + """ + The position of the environment if it is pinned, null if it is not pinned + """ + pinnedPosition: Int + + """ + The protection rules defined for this environment + """ + protectionRules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentProtectionRuleConnection! +} + +""" +The connection type for Environment. +""" +type EnvironmentConnection { + """ + A list of edges. + """ + edges: [EnvironmentEdge] + + """ + A list of nodes. + """ + nodes: [Environment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnvironmentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Environment +} + +""" +Properties by which environments connections can be ordered +""" +enum EnvironmentOrderField { + """ + Order environments by name. + """ + NAME +} + +""" +Properties by which environments connections can be ordered +""" +enum EnvironmentPinnedFilterField { + """ + All environments will be returned. + """ + ALL + + """ + Environments exclude pinned will be returned + """ + NONE + + """ + Only pinned environment will be returned + """ + ONLY +} + +""" +Ordering options for environments +""" +input Environments { + """ + The direction in which to order environments by the specified field. + """ + direction: OrderDirection! + + """ + The field to order environments by. + """ + field: EnvironmentOrderField! +} + +""" +An external identity provisioned by SAML SSO or SCIM. If SAML is configured on +the organization, the external identity is visible to (1) organization owners, +(2) organization owners' personal access tokens (classic) with read:org or +admin:org scope, (3) GitHub App with an installation token with read or write +access to members. If SAML is configured on the enterprise, the external +identity is visible to (1) enterprise owners, (2) enterprise owners' personal +access tokens (classic) with read:enterprise or admin:enterprise scope. +""" +type ExternalIdentity implements Node { + """ + The GUID for this identity + """ + guid: String! + + """ + The Node ID of the ExternalIdentity object + """ + id: ID! + + """ + Organization invitation for this SCIM-provisioned external identity + """ + organizationInvitation: OrganizationInvitation + + """ + SAML Identity attributes + """ + samlIdentity: ExternalIdentitySamlAttributes + + """ + SCIM Identity attributes + """ + scimIdentity: ExternalIdentityScimAttributes + + """ + User linked to this external identity. Will be NULL if this identity has not been claimed by an organization member. + """ + user: User +} + +""" +An attribute for the External Identity attributes collection +""" +type ExternalIdentityAttribute { + """ + The attribute metadata as JSON + """ + metadata: String + + """ + The attribute name + """ + name: String! + + """ + The attribute value + """ + value: String! +} + +""" +The connection type for ExternalIdentity. +""" +type ExternalIdentityConnection { + """ + A list of edges. + """ + edges: [ExternalIdentityEdge] + + """ + A list of nodes. + """ + nodes: [ExternalIdentity] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ExternalIdentityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ExternalIdentity +} + +""" +SAML attributes for the External Identity +""" +type ExternalIdentitySamlAttributes { + """ + SAML Identity attributes + """ + attributes: [ExternalIdentityAttribute!]! + + """ + The emails associated with the SAML identity + """ + emails: [UserEmailMetadata!] + + """ + Family name of the SAML identity + """ + familyName: String + + """ + Given name of the SAML identity + """ + givenName: String + + """ + The groups linked to this identity in IDP + """ + groups: [String!] + + """ + The NameID of the SAML identity + """ + nameId: String + + """ + The userName of the SAML identity + """ + username: String +} + +""" +SCIM attributes for the External Identity +""" +type ExternalIdentityScimAttributes { + """ + The emails associated with the SCIM identity + """ + emails: [UserEmailMetadata!] + + """ + Family name of the SCIM identity + """ + familyName: String + + """ + Given name of the SCIM identity + """ + givenName: String + + """ + The groups linked to this identity in IDP + """ + groups: [String!] + + """ + The userName of the SCIM identity + """ + username: String +} + +""" +A command to add a file at the given path with the given contents as part of a +commit. Any existing file at that that path will be replaced. +""" +input FileAddition { + """ + The base64 encoded contents of the file + """ + contents: Base64String! + + """ + The path in the repository where the file will be located + """ + path: String! +} + +""" +A description of a set of changes to a file tree to be made as part of +a git commit, modeled as zero or more file `additions` and zero or more +file `deletions`. + +Both fields are optional; omitting both will produce a commit with no +file changes. + +`deletions` and `additions` describe changes to files identified +by their path in the git tree using unix-style path separators, i.e. +`/`. The root of a git tree is an empty string, so paths are not +slash-prefixed. + +`path` values must be unique across all `additions` and `deletions` +provided. Any duplication will result in a validation error. + +### Encoding + +File contents must be provided in full for each `FileAddition`. + +The `contents` of a `FileAddition` must be encoded using RFC 4648 +compliant base64, i.e. correct padding is required and no characters +outside the standard alphabet may be used. Invalid base64 +encoding will be rejected with a validation error. + +The encoded contents may be binary. + +For text files, no assumptions are made about the character encoding of +the file contents (after base64 decoding). No charset transcoding or +line-ending normalization will be performed; it is the client's +responsibility to manage the character encoding of files they provide. +However, for maximum compatibility we recommend using UTF-8 encoding +and ensuring that all files in a repository use a consistent +line-ending convention (`\n` or `\r\n`), and that all files end +with a newline. + +### Modeling file changes + +Each of the the five types of conceptual changes that can be made in a +git commit can be described using the `FileChanges` type as follows: + +1. New file addition: create file `hello world\n` at path `docs/README.txt`: + + { + "additions" [ + { + "path": "docs/README.txt", + "contents": base64encode("hello world\n") + } + ] + } + +2. Existing file modification: change existing `docs/README.txt` to have new + content `new content here\n`: + + { + "additions" [ + { + "path": "docs/README.txt", + "contents": base64encode("new content here\n") + } + ] + } + +3. Existing file deletion: remove existing file `docs/README.txt`. + Note that the path is required to exist -- specifying a + path that does not exist on the given branch will abort the + commit and return an error. + + { + "deletions" [ + { + "path": "docs/README.txt" + } + ] + } + + +4. File rename with no changes: rename `docs/README.txt` with + previous content `hello world\n` to the same content at + `newdocs/README.txt`: + + { + "deletions" [ + { + "path": "docs/README.txt", + } + ], + "additions" [ + { + "path": "newdocs/README.txt", + "contents": base64encode("hello world\n") + } + ] + } + + +5. File rename with changes: rename `docs/README.txt` with + previous content `hello world\n` to a file at path + `newdocs/README.txt` with content `new contents\n`: + + { + "deletions" [ + { + "path": "docs/README.txt", + } + ], + "additions" [ + { + "path": "newdocs/README.txt", + "contents": base64encode("new contents\n") + } + ] + } +""" +input FileChanges { + """ + File to add or change. + """ + additions: [FileAddition!] = [] + + """ + Files to delete. + """ + deletions: [FileDeletion!] = [] +} + +""" +A command to delete the file at the given path as part of a commit. +""" +input FileDeletion { + """ + The path to delete + """ + path: String! +} + +""" +Prevent commits that include files with specified file extensions from being pushed to the commit graph. +""" +type FileExtensionRestrictionParameters { + """ + The file extensions that are restricted from being pushed to the commit graph. + """ + restrictedFileExtensions: [String!]! +} + +""" +Prevent commits that include files with specified file extensions from being pushed to the commit graph. +""" +input FileExtensionRestrictionParametersInput { + """ + The file extensions that are restricted from being pushed to the commit graph. + """ + restrictedFileExtensions: [String!]! +} + +""" +Prevent commits that include changes in specified file and folder paths from +being pushed to the commit graph. This includes absolute paths that contain file names. +""" +type FilePathRestrictionParameters { + """ + The file paths that are restricted from being pushed to the commit graph. + """ + restrictedFilePaths: [String!]! +} + +""" +Prevent commits that include changes in specified file and folder paths from +being pushed to the commit graph. This includes absolute paths that contain file names. +""" +input FilePathRestrictionParametersInput { + """ + The file paths that are restricted from being pushed to the commit graph. + """ + restrictedFilePaths: [String!]! +} + +""" +The possible viewed states of a file . +""" +enum FileViewedState { + """ + The file has new changes since last viewed. + """ + DISMISSED + + """ + The file has not been marked as viewed. + """ + UNVIEWED + + """ + The file has been marked as viewed. + """ + VIEWED +} + +""" +Autogenerated input type of FollowOrganization +""" +input FollowOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the organization to follow. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of FollowOrganization. +""" +type FollowOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization that was followed. + """ + organization: Organization +} + +""" +Autogenerated input type of FollowUser +""" +input FollowUserInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the user to follow. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of FollowUser. +""" +type FollowUserPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was followed. + """ + user: User +} + +""" +The connection type for User. +""" +type FollowerConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The connection type for User. +""" +type FollowingConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A funding platform link for a repository. +""" +type FundingLink { + """ + The funding platform this link is for. + """ + platform: FundingPlatform! + + """ + The configured URL for this funding link. + """ + url: URI! +} + +""" +The possible funding platforms for repository funding links. +""" +enum FundingPlatform { + """ + Buy Me a Coffee funding platform. + """ + BUY_ME_A_COFFEE + + """ + Community Bridge funding platform. + """ + COMMUNITY_BRIDGE + + """ + Custom funding platform. + """ + CUSTOM + + """ + GitHub funding platform. + """ + GITHUB + + """ + IssueHunt funding platform. + """ + ISSUEHUNT + + """ + Ko-fi funding platform. + """ + KO_FI + + """ + LFX Crowdfunding funding platform. + """ + LFX_CROWDFUNDING + + """ + Liberapay funding platform. + """ + LIBERAPAY + + """ + Open Collective funding platform. + """ + OPEN_COLLECTIVE + + """ + Patreon funding platform. + """ + PATREON + + """ + Polar funding platform. + """ + POLAR + + """ + thanks.dev funding platform. + """ + THANKS_DEV + + """ + Tidelift funding platform. + """ + TIDELIFT +} + +""" +A generic hovercard context with a message and icon +""" +type GenericHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! +} + +""" +A Gist. +""" +type Gist implements Node & Starrable & UniformResourceLocatable { + """ + A list of comments associated with the gist + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GistCommentConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The gist description. + """ + description: String + + """ + The files in this gist. + """ + files( + """ + The maximum number of files to return. + """ + limit: Int = 10 + + """ + The oid of the files to return + """ + oid: GitObjectID + ): [GistFile] + + """ + A list of forks associated with the gist + """ + forks( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for gists returned from the connection + """ + orderBy: GistOrder + ): GistConnection! + + """ + The Node ID of the Gist object + """ + id: ID! + + """ + Identifies if the gist is a fork. + """ + isFork: Boolean! + + """ + Whether the gist is public or not. + """ + isPublic: Boolean! + + """ + The gist name. + """ + name: String! + + """ + The gist owner. + """ + owner: RepositoryOwner + + """ + Identifies when the gist was last pushed to. + """ + pushedAt: DateTime + + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this Gist. + """ + url: URI! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +Represents a comment on an Gist. +""" +type GistComment implements Comment & Deletable & Minimizable & Node & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the gist. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the comment body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The associated gist. + """ + gist: Gist! + + """ + The Node ID of the GistComment object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for GistComment. +""" +type GistCommentConnection { + """ + A list of edges. + """ + edges: [GistCommentEdge] + + """ + A list of nodes. + """ + nodes: [GistComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GistCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: GistComment +} + +""" +The connection type for Gist. +""" +type GistConnection { + """ + A list of edges. + """ + edges: [GistEdge] + + """ + A list of nodes. + """ + nodes: [Gist] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GistEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Gist +} + +""" +A file in a gist. +""" +type GistFile { + """ + The file name encoded to remove characters that are invalid in URL paths. + """ + encodedName: String + + """ + The gist file encoding. + """ + encoding: String + + """ + The file extension from the file name. + """ + extension: String + + """ + Indicates if this file is an image. + """ + isImage: Boolean! + + """ + Whether the file's contents were truncated. + """ + isTruncated: Boolean! + + """ + The programming language this file is written in. + """ + language: Language + + """ + The gist file name. + """ + name: String + + """ + The gist file size in bytes. + """ + size: Int + + """ + UTF8 text data or null if the file is binary + """ + text( + """ + Optionally truncate the returned file to this length. + """ + truncate: Int + ): String +} + +""" +Ordering options for gist connections +""" +input GistOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: GistOrderField! +} + +""" +Properties by which gist connections can be ordered. +""" +enum GistOrderField { + """ + Order gists by creation time + """ + CREATED_AT + + """ + Order gists by push time + """ + PUSHED_AT + + """ + Order gists by update time + """ + UPDATED_AT +} + +""" +The privacy of a Gist +""" +enum GistPrivacy { + """ + Gists that are public and secret + """ + ALL + + """ + Public + """ + PUBLIC + + """ + Secret + """ + SECRET +} + +""" +Represents an actor in a Git commit (ie. an author or committer). +""" +type GitActor { + """ + A URL pointing to the author's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The timestamp of the Git action (authoring or committing). + """ + date: GitTimestamp + + """ + The email in the Git commit. + """ + email: String + + """ + The name in the Git commit. + """ + name: String + + """ + The GitHub user corresponding to the email field. Null if no such user exists. + """ + user: User +} + +""" +The connection type for GitActor. +""" +type GitActorConnection { + """ + A list of edges. + """ + edges: [GitActorEdge] + + """ + A list of nodes. + """ + nodes: [GitActor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GitActorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: GitActor +} + +""" +Represents information about the GitHub instance. +""" +type GitHubMetadata { + """ + Returns a String that's a SHA of `github-services` + """ + gitHubServicesSha: GitObjectID! + + """ + IP addresses that users connect to for git operations + """ + gitIpAddresses: [String!] + + """ + IP addresses that GitHub Enterprise Importer uses for outbound connections + """ + githubEnterpriseImporterIpAddresses: [String!] + + """ + IP addresses that service hooks are sent from + """ + hookIpAddresses: [String!] + + """ + IP addresses that the importer connects from + """ + importerIpAddresses: [String!] + + """ + Whether or not users are verified + """ + isPasswordAuthenticationVerifiable: Boolean! + + """ + IP addresses for GitHub Pages' A records + """ + pagesIpAddresses: [String!] +} + +""" +Represents a Git object. +""" +interface GitObject { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + The Node ID of the GitObject object + """ + id: ID! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! +} + +""" +A Git object ID. +""" +scalar GitObjectID + +""" +A fully qualified reference name (e.g. `refs/heads/master`). +""" +scalar GitRefname + +""" +Git SSH string +""" +scalar GitSSHRemote + +""" +Information about a signature (GPG or S/MIME) on a Commit or Tag. +""" +interface GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + The date the signature was verified, if valid + """ + verifiedAt: DateTime + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +The state of a Git signature. +""" +enum GitSignatureState { + """ + The signing certificate or its chain could not be verified + """ + BAD_CERT + + """ + Invalid email used for signing + """ + BAD_EMAIL + + """ + Signing key expired + """ + EXPIRED_KEY + + """ + Internal error - the GPG verification service misbehaved + """ + GPGVERIFY_ERROR + + """ + Internal error - the GPG verification service is unavailable at the moment + """ + GPGVERIFY_UNAVAILABLE + + """ + Invalid signature + """ + INVALID + + """ + Malformed signature + """ + MALFORMED_SIG + + """ + The usage flags for the key that signed this don't allow signing + """ + NOT_SIGNING_KEY + + """ + Email used for signing not known to GitHub + """ + NO_USER + + """ + Valid signature, though certificate revocation check failed + """ + OCSP_ERROR + + """ + Valid signature, pending certificate revocation checking + """ + OCSP_PENDING + + """ + One or more certificates in chain has been revoked + """ + OCSP_REVOKED + + """ + Key used for signing not known to GitHub + """ + UNKNOWN_KEY + + """ + Unknown signature type + """ + UNKNOWN_SIG_TYPE + + """ + Unsigned + """ + UNSIGNED + + """ + Email used for signing unverified on GitHub + """ + UNVERIFIED_EMAIL + + """ + Valid signature and verified by GitHub + """ + VALID +} + +""" +An ISO-8601 encoded date string. Unlike the DateTime type, GitTimestamp is not converted in UTC. +""" +scalar GitTimestamp + +""" +Represents a GPG signature on a Commit or Tag. +""" +type GpgSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Hex-encoded ID of the key that signed this object. + """ + keyId: String + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + The date the signature was verified, if valid + """ + verifiedAt: DateTime + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Autogenerated input type of GrantEnterpriseOrganizationsMigratorRole +""" +input GrantEnterpriseOrganizationsMigratorRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise to which all organizations managed by it will be granted the migrator role. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the user to grant the migrator role + """ + login: String! +} + +""" +Autogenerated return type of GrantEnterpriseOrganizationsMigratorRole. +""" +type GrantEnterpriseOrganizationsMigratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organizations that had the migrator role applied to for the given user. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationConnection +} + +""" +Autogenerated input type of GrantMigratorRole +""" +input GrantMigratorRoleInput { + """ + The user login or Team slug to grant the migrator role. + """ + actor: String! + + """ + Specifies the type of the actor, can be either USER or TEAM. + """ + actorType: ActorType! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization that the user/team belongs to. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of GrantMigratorRole. +""" +type GrantMigratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Did the operation succeed? + """ + success: Boolean +} + +""" +A string containing HTML code. +""" +scalar HTML + +""" +Represents a 'head_ref_deleted' event on a given pull request. +""" +type HeadRefDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the Ref associated with the `head_ref_deleted` event. + """ + headRef: Ref + + """ + Identifies the name of the Ref associated with the `head_ref_deleted` event. + """ + headRefName: String! + + """ + The Node ID of the HeadRefDeletedEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'head_ref_force_pushed' event on a given pull request. +""" +type HeadRefForcePushedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the after commit SHA for the 'head_ref_force_pushed' event. + """ + afterCommit: Commit + + """ + Identifies the before commit SHA for the 'head_ref_force_pushed' event. + """ + beforeCommit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the HeadRefForcePushedEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the fully qualified ref name for the 'head_ref_force_pushed' event. + """ + ref: Ref +} + +""" +Represents a 'head_ref_restored' event on a given pull request. +""" +type HeadRefRestoredEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the HeadRefRestoredEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Detail needed to display a hovercard for a user +""" +type Hovercard { + """ + Each of the contexts for this hovercard + """ + contexts: [HovercardContext!]! +} + +""" +An individual line of a hovercard +""" +interface HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! +} + +""" +The possible states in which authentication can be configured with an identity provider. +""" +enum IdentityProviderConfigurationState { + """ + Authentication with an identity provider is configured but not enforced. + """ + CONFIGURED + + """ + Authentication with an identity provider is configured and enforced. + """ + ENFORCED + + """ + Authentication with an identity provider is not configured. + """ + UNCONFIGURED +} + +""" +Autogenerated input type of ImportProject +""" +input ImportProjectInput { + """ + The description of Project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A list of columns containing issues and pull requests. + """ + columnImports: [ProjectColumnImport!]! + + """ + The name of Project. + """ + name: String! + + """ + The name of the Organization or User to create the Project under. + """ + ownerName: String! + + """ + Whether the Project is public or not. + """ + public: Boolean = false +} + +""" +Autogenerated return type of ImportProject. +""" +type ImportProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new Project! + """ + project: Project +} + +""" +Autogenerated input type of InviteEnterpriseAdmin +""" +input InviteEnterpriseAdminInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The email of the person to invite as an administrator. + """ + email: String + + """ + The ID of the enterprise to which you want to invite an administrator. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a user to invite as an administrator. + """ + invitee: String + + """ + The role of the administrator. + """ + role: EnterpriseAdministratorRole +} + +""" +Autogenerated return type of InviteEnterpriseAdmin. +""" +type InviteEnterpriseAdminPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The created enterprise administrator invitation. + """ + invitation: EnterpriseAdministratorInvitation +} + +""" +Autogenerated input type of InviteEnterpriseMember +""" +input InviteEnterpriseMemberInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The email of the person to invite as an unaffiliated member. + """ + email: String + + """ + The ID of the enterprise to which you want to invite an unaffiliated member. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a user to invite as an unaffiliated member. + """ + invitee: String +} + +""" +Autogenerated return type of InviteEnterpriseMember. +""" +type InviteEnterpriseMemberPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The created enterprise member invitation. + """ + invitation: EnterpriseMemberInvitation +} + +""" +The possible values for the IP allow list enabled setting. +""" +enum IpAllowListEnabledSettingValue { + """ + The setting is disabled for the owner. + """ + DISABLED + + """ + The setting is enabled for the owner. + """ + ENABLED +} + +""" +An IP address or range of addresses that is allowed to access an owner's resources. +""" +type IpAllowListEntry implements Node { + """ + A single IP address or range of IP addresses in CIDR notation. + """ + allowListValue: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the IpAllowListEntry object + """ + id: ID! + + """ + Whether the entry is currently active. + """ + isActive: Boolean! + + """ + The name of the IP allow list entry. + """ + name: String + + """ + The owner of the IP allow list entry. + """ + owner: IpAllowListOwner! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for IpAllowListEntry. +""" +type IpAllowListEntryConnection { + """ + A list of edges. + """ + edges: [IpAllowListEntryEdge] + + """ + A list of nodes. + """ + nodes: [IpAllowListEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type IpAllowListEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IpAllowListEntry +} + +""" +Ordering options for IP allow list entry connections. +""" +input IpAllowListEntryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order IP allow list entries by. + """ + field: IpAllowListEntryOrderField! +} + +""" +Properties by which IP allow list entry connections can be ordered. +""" +enum IpAllowListEntryOrderField { + """ + Order IP allow list entries by the allow list value. + """ + ALLOW_LIST_VALUE + + """ + Order IP allow list entries by creation time. + """ + CREATED_AT +} + +""" +The possible values for the IP allow list configuration for installed GitHub Apps setting. +""" +enum IpAllowListForInstalledAppsEnabledSettingValue { + """ + The setting is disabled for the owner. + """ + DISABLED + + """ + The setting is enabled for the owner. + """ + ENABLED +} + +""" +Types that can own an IP allow list. +""" +union IpAllowListOwner = App | Enterprise | Organization + +""" +An Issue is a place to discuss ideas, enhancements, tasks, and bugs for a project. +""" +type Issue implements Assignable & Closable & Comment & Deletable & Labelable & Lockable & Node & ProjectV2Owner & Reactable & RepositoryNode & Subscribable & SubscribableThread & UniformResourceLocatable & Updatable & UpdatableComment { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + A list of actors assigned to this object. + """ + assignedActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): AssigneeConnection! + + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the body of the issue. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The http path for this issue body + """ + bodyResourcePath: URI! + + """ + Identifies the body of the issue rendered to text. + """ + bodyText: String! + + """ + The http URL for this issue body + """ + bodyUrl: URI! + + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + List of open pull requests referenced from this issue + """ + closedByPullRequestsReferences( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Include closed PRs in results + """ + includeClosedPrs: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Return results ordered by state + """ + orderByState: Boolean = false + + """ + Return only manually linked PRs + """ + userLinkedOnly: Boolean = false + ): PullRequestConnection + + """ + A list of comments associated with the Issue. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The hovercard information for this issue + """ + hovercard( + """ + Whether or not to include notification contexts + """ + includeNotificationContexts: Boolean = true + ): Hovercard! + + """ + The Node ID of the Issue object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Indicates whether or not this issue is currently pinned to the repository issues list + """ + isPinned: Boolean + + """ + Is this issue read by the viewer + """ + isReadByViewer: Boolean + + """ + The issue type for this Issue + """ + issueType: IssueType + + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Branches linked to this issue. + """ + linkedBranches( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): LinkedBranchConnection! + + """ + `true` if the object is locked + """ + locked: Boolean! + + """ + Identifies the milestone associated with the issue. + """ + milestone: Milestone + + """ + Identifies the issue number. + """ + number: Int! + + """ + The parent entity of the issue. + """ + parent: Issue + + """ + A list of Users that are participating in the Issue conversation. + """ + participants( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + List of project cards associated with this issue. + """ + projectCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + List of project items associated with this issue. + """ + projectItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Include archived items. + """ + includeArchived: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ItemConnection! + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter projects based on user role. + """ + minPermissionLevel: ProjectV2PermissionLevel = READ + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the owner. + """ + query: String + ): ProjectV2Connection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this issue + """ + resourcePath: URI! + + """ + Identifies the state of the issue. + """ + state: IssueState! + + """ + Identifies the reason for the issue state. + """ + stateReason( + """ + Whether or not to return state reason for duplicates + """ + enableDuplicate: Boolean = false + ): IssueStateReason + + """ + A list of sub-issues associated with the Issue. + """ + subIssues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): IssueConnection! + + """ + Summary of the state of an issue's sub-issues + """ + subIssuesSummary: SubIssuesSummary! + + """ + A list of suggested actors to assign to this object + """ + suggestedActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If provided, searches users by login or profile name + """ + query: String + ): AssigneeConnection! + + """ + A list of events, comments, commits, etc. associated with the issue. + """ + timeline( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows filtering timeline events by a `since` timestamp. + """ + since: DateTime + ): IssueTimelineConnection! + @deprecated(reason: "`timeline` will be removed Use Issue.timelineItems instead. Removal on 2020-10-01 UTC.") + + """ + A list of events, comments, commits, etc. associated with the issue. + """ + timelineItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter timeline items by type. + """ + itemTypes: [IssueTimelineItemsItemType!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter timeline items by a `since` timestamp. + """ + since: DateTime + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): IssueTimelineItemsConnection! + + """ + Identifies the issue title. + """ + title: String! + + """ + Identifies the issue title rendered to HTML. + """ + titleHTML: String! + + """ + A list of issues that track this issue + """ + trackedInIssues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): IssueConnection! + + """ + A list of issues tracked inside the current issue + """ + trackedIssues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): IssueConnection! + + """ + The number of tracked issues for this issue + """ + trackedIssuesCount( + """ + Limit the count to tracked issues with the specified states. + """ + states: [TrackedIssueStates] + ): Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Indicates if the viewer can edit labels for this object. + """ + viewerCanLabel: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState + + """ + Identifies the viewer's thread subscription form action. + """ + viewerThreadSubscriptionFormAction: ThreadSubscriptionFormAction + + """ + Identifies the viewer's thread subscription status. + """ + viewerThreadSubscriptionStatus: ThreadSubscriptionState +} + +""" +The possible state reasons of a closed issue. +""" +enum IssueClosedStateReason { + """ + An issue that has been closed as completed + """ + COMPLETED + + """ + An issue that has been closed as a duplicate + """ + DUPLICATE + + """ + An issue that has been closed as not planned + """ + NOT_PLANNED +} + +""" +Represents a comment on an Issue. +""" +type IssueComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The Node ID of the IssueComment object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + Identifies the issue associated with the comment. + """ + issue: Issue! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Returns the pull request associated with the comment, if this comment was made on a + pull request. + """ + pullRequest: PullRequest + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this issue comment + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue comment + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for IssueComment. +""" +type IssueCommentConnection { + """ + A list of edges. + """ + edges: [IssueCommentEdge] + + """ + A list of nodes. + """ + nodes: [IssueComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type IssueCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueComment +} + +""" +Ways in which lists of issue comments can be ordered upon return. +""" +input IssueCommentOrder { + """ + The direction in which to order issue comments by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order issue comments by. + """ + field: IssueCommentOrderField! +} + +""" +Properties by which issue comment connections can be ordered. +""" +enum IssueCommentOrderField { + """ + Order issue comments by update time + """ + UPDATED_AT +} + +""" +The connection type for Issue. +""" +type IssueConnection { + """ + A list of edges. + """ + edges: [IssueEdge] + + """ + A list of nodes. + """ + nodes: [Issue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates issues opened by a user within one repository. +""" +type IssueContributionsByRepository { + """ + The issue contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedIssueContributionConnection! + + """ + The repository in which the issues were opened. + """ + repository: Repository! +} + +""" +An edge in a connection. +""" +type IssueEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Issue +} + +""" +Ways in which to filter lists of issues. +""" +input IssueFilters { + """ + List issues assigned to given name. Pass in `null` for issues with no assigned + user, and `*` for issues assigned to any user. + """ + assignee: String + + """ + List issues created by given name. + """ + createdBy: String + + """ + List issues where the list of label names exist on the issue. + """ + labels: [String!] + + """ + List issues where the given name is mentioned in the issue. + """ + mentioned: String + + """ + List issues by given milestone argument. If an string representation of an + integer is passed, it should refer to a milestone by its database ID. Pass in + `null` for issues with no milestone, and `*` for issues that are assigned to any milestone. + """ + milestone: String + + """ + List issues by given milestone argument. If an string representation of an + integer is passed, it should refer to a milestone by its number field. Pass in + `null` for issues with no milestone, and `*` for issues that are assigned to any milestone. + """ + milestoneNumber: String + + """ + List issues that have been updated at or after the given date. + """ + since: DateTime + + """ + List issues filtered by the list of states given. + """ + states: [IssueState!] + + """ + List issues filtered by the type given, only supported by searches on repositories. + """ + type: String + + """ + List issues subscribed to by viewer. + """ + viewerSubscribed: Boolean = false +} + +""" +Used for return value of Repository.issueOrPullRequest. +""" +union IssueOrPullRequest = Issue | PullRequest + +""" +Ways in which lists of issues can be ordered upon return. +""" +input IssueOrder { + """ + The direction in which to order issues by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order issues by. + """ + field: IssueOrderField! +} + +""" +Properties by which issue connections can be ordered. +""" +enum IssueOrderField { + """ + Order issues by comment count + """ + COMMENTS + + """ + Order issues by creation time + """ + CREATED_AT + + """ + Order issues by update time + """ + UPDATED_AT +} + +""" +The possible states of an issue. +""" +enum IssueState { + """ + An issue that has been closed + """ + CLOSED + + """ + An issue that is still open + """ + OPEN +} + +""" +The possible state reasons of an issue. +""" +enum IssueStateReason { + """ + An issue that has been closed as completed + """ + COMPLETED + + """ + An issue that has been closed as a duplicate. To retrieve this value, set + `(enableDuplicate: true)` when querying the stateReason field. + """ + DUPLICATE + + """ + An issue that has been closed as not planned + """ + NOT_PLANNED + + """ + An issue that has been reopened + """ + REOPENED +} + +""" +A repository issue template. +""" +type IssueTemplate { + """ + The template purpose. + """ + about: String + + """ + The suggested assignees. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The suggested issue body. + """ + body: String + + """ + The template filename. + """ + filename: String! + + """ + The suggested issue labels + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The template name. + """ + name: String! + + """ + The suggested issue title. + """ + title: String + + """ + The suggested issue type + """ + type: IssueType +} + +""" +The connection type for IssueTimelineItem. +""" +type IssueTimelineConnection { + """ + A list of edges. + """ + edges: [IssueTimelineItemEdge] + + """ + A list of nodes. + """ + nodes: [IssueTimelineItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An item in an issue timeline +""" +union IssueTimelineItem = + AssignedEvent + | ClosedEvent + | Commit + | CrossReferencedEvent + | DemilestonedEvent + | IssueComment + | LabeledEvent + | LockedEvent + | MilestonedEvent + | ReferencedEvent + | RenamedTitleEvent + | ReopenedEvent + | SubscribedEvent + | TransferredEvent + | UnassignedEvent + | UnlabeledEvent + | UnlockedEvent + | UnsubscribedEvent + | UserBlockedEvent + +""" +An edge in a connection. +""" +type IssueTimelineItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueTimelineItem +} + +""" +An item in an issue timeline +""" +union IssueTimelineItems = + AddedToProjectEvent + | AssignedEvent + | ClosedEvent + | CommentDeletedEvent + | ConnectedEvent + | ConvertedNoteToIssueEvent + | ConvertedToDiscussionEvent + | CrossReferencedEvent + | DemilestonedEvent + | DisconnectedEvent + | IssueComment + | IssueTypeAddedEvent + | IssueTypeChangedEvent + | IssueTypeRemovedEvent + | LabeledEvent + | LockedEvent + | MarkedAsDuplicateEvent + | MentionedEvent + | MilestonedEvent + | MovedColumnsInProjectEvent + | ParentIssueAddedEvent + | ParentIssueRemovedEvent + | PinnedEvent + | ReferencedEvent + | RemovedFromProjectEvent + | RenamedTitleEvent + | ReopenedEvent + | SubIssueAddedEvent + | SubIssueRemovedEvent + | SubscribedEvent + | TransferredEvent + | UnassignedEvent + | UnlabeledEvent + | UnlockedEvent + | UnmarkedAsDuplicateEvent + | UnpinnedEvent + | UnsubscribedEvent + | UserBlockedEvent + +""" +The connection type for IssueTimelineItems. +""" +type IssueTimelineItemsConnection { + """ + A list of edges. + """ + edges: [IssueTimelineItemsEdge] + + """ + Identifies the count of items after applying `before` and `after` filters. + """ + filteredCount: Int! + + """ + A list of nodes. + """ + nodes: [IssueTimelineItems] + + """ + Identifies the count of items after applying `before`/`after` filters and `first`/`last`/`skip` slicing. + """ + pageCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the date and time when the timeline was last updated. + """ + updatedAt: DateTime! +} + +""" +An edge in a connection. +""" +type IssueTimelineItemsEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueTimelineItems +} + +""" +The possible item types found in a timeline. +""" +enum IssueTimelineItemsItemType { + """ + Represents a 'added_to_project' event on a given issue or pull request. + """ + ADDED_TO_PROJECT_EVENT + + """ + Represents an 'assigned' event on any assignable object. + """ + ASSIGNED_EVENT + + """ + Represents a 'closed' event on any `Closable`. + """ + CLOSED_EVENT + + """ + Represents a 'comment_deleted' event on a given issue or pull request. + """ + COMMENT_DELETED_EVENT + + """ + Represents a 'connected' event on a given issue or pull request. + """ + CONNECTED_EVENT + + """ + Represents a 'converted_note_to_issue' event on a given issue or pull request. + """ + CONVERTED_NOTE_TO_ISSUE_EVENT + + """ + Represents a 'converted_to_discussion' event on a given issue. + """ + CONVERTED_TO_DISCUSSION_EVENT + + """ + Represents a mention made by one issue or pull request to another. + """ + CROSS_REFERENCED_EVENT + + """ + Represents a 'demilestoned' event on a given issue or pull request. + """ + DEMILESTONED_EVENT + + """ + Represents a 'disconnected' event on a given issue or pull request. + """ + DISCONNECTED_EVENT + + """ + Represents a comment on an Issue. + """ + ISSUE_COMMENT + + """ + Represents a 'issue_type_added' event on a given issue. + """ + ISSUE_TYPE_ADDED_EVENT + + """ + Represents a 'issue_type_changed' event on a given issue. + """ + ISSUE_TYPE_CHANGED_EVENT + + """ + Represents a 'issue_type_removed' event on a given issue. + """ + ISSUE_TYPE_REMOVED_EVENT + + """ + Represents a 'labeled' event on a given issue or pull request. + """ + LABELED_EVENT + + """ + Represents a 'locked' event on a given issue or pull request. + """ + LOCKED_EVENT + + """ + Represents a 'marked_as_duplicate' event on a given issue or pull request. + """ + MARKED_AS_DUPLICATE_EVENT + + """ + Represents a 'mentioned' event on a given issue or pull request. + """ + MENTIONED_EVENT + + """ + Represents a 'milestoned' event on a given issue or pull request. + """ + MILESTONED_EVENT + + """ + Represents a 'moved_columns_in_project' event on a given issue or pull request. + """ + MOVED_COLUMNS_IN_PROJECT_EVENT + + """ + Represents a 'parent_issue_added' event on a given issue. + """ + PARENT_ISSUE_ADDED_EVENT + + """ + Represents a 'parent_issue_removed' event on a given issue. + """ + PARENT_ISSUE_REMOVED_EVENT + + """ + Represents a 'pinned' event on a given issue or pull request. + """ + PINNED_EVENT + + """ + Represents a 'referenced' event on a given `ReferencedSubject`. + """ + REFERENCED_EVENT + + """ + Represents a 'removed_from_project' event on a given issue or pull request. + """ + REMOVED_FROM_PROJECT_EVENT + + """ + Represents a 'renamed' event on a given issue or pull request + """ + RENAMED_TITLE_EVENT + + """ + Represents a 'reopened' event on any `Closable`. + """ + REOPENED_EVENT + + """ + Represents a 'subscribed' event on a given `Subscribable`. + """ + SUBSCRIBED_EVENT + + """ + Represents a 'sub_issue_added' event on a given issue. + """ + SUB_ISSUE_ADDED_EVENT + + """ + Represents a 'sub_issue_removed' event on a given issue. + """ + SUB_ISSUE_REMOVED_EVENT + + """ + Represents a 'transferred' event on a given issue or pull request. + """ + TRANSFERRED_EVENT + + """ + Represents an 'unassigned' event on any assignable object. + """ + UNASSIGNED_EVENT + + """ + Represents an 'unlabeled' event on a given issue or pull request. + """ + UNLABELED_EVENT + + """ + Represents an 'unlocked' event on a given issue or pull request. + """ + UNLOCKED_EVENT + + """ + Represents an 'unmarked_as_duplicate' event on a given issue or pull request. + """ + UNMARKED_AS_DUPLICATE_EVENT + + """ + Represents an 'unpinned' event on a given issue or pull request. + """ + UNPINNED_EVENT + + """ + Represents an 'unsubscribed' event on a given `Subscribable`. + """ + UNSUBSCRIBED_EVENT + + """ + Represents a 'user_blocked' event on a given user. + """ + USER_BLOCKED_EVENT +} + +""" +Represents the type of Issue. +""" +type IssueType implements Node { + """ + The issue type's color. + """ + color: IssueTypeColor! + + """ + The issue type's description. + """ + description: String + + """ + The Node ID of the IssueType object + """ + id: ID! + + """ + The issue type's enabled state. + """ + isEnabled: Boolean! + + """ + Whether the issue type is publicly visible. + """ + isPrivate: Boolean! + @deprecated( + reason: "Private issue types are being deprecated and can no longer be created. Removal on 2025-04-01 UTC." + ) + + """ + The issues with this issue type in the given repository. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + Target repository to load the issues from. + """ + repositoryId: ID! + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + The issue type's name. + """ + name: String! +} + +""" +Represents a 'issue_type_added' event on a given issue. +""" +type IssueTypeAddedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the IssueTypeAddedEvent object + """ + id: ID! + + """ + The issue type added. + """ + issueType: IssueType +} + +""" +Represents a 'issue_type_changed' event on a given issue. +""" +type IssueTypeChangedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the IssueTypeChangedEvent object + """ + id: ID! + + """ + The issue type added. + """ + issueType: IssueType + + """ + The issue type removed. + """ + prevIssueType: IssueType +} + +""" +The possible color for an issue type +""" +enum IssueTypeColor { + """ + blue + """ + BLUE + + """ + gray + """ + GRAY + + """ + green + """ + GREEN + + """ + orange + """ + ORANGE + + """ + pink + """ + PINK + + """ + purple + """ + PURPLE + + """ + red + """ + RED + + """ + yellow + """ + YELLOW +} + +""" +The connection type for IssueType. +""" +type IssueTypeConnection { + """ + A list of edges. + """ + edges: [IssueTypeEdge] + + """ + A list of nodes. + """ + nodes: [IssueType] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type IssueTypeEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueType +} + +""" +Ordering options for issue types connections +""" +input IssueTypeOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order issue types by. + """ + field: IssueTypeOrderField! +} + +""" +Properties by which issue type connections can be ordered. +""" +enum IssueTypeOrderField { + """ + Order issue types by creation time + """ + CREATED_AT + + """ + Order issue types by name + """ + NAME +} + +""" +Represents a 'issue_type_removed' event on a given issue. +""" +type IssueTypeRemovedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the IssueTypeRemovedEvent object + """ + id: ID! + + """ + The issue type removed. + """ + issueType: IssueType +} + +""" +Represents a user signing up for a GitHub account. +""" +type JoinedGitHubContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +A label for categorizing Issues, Pull Requests, Milestones, or Discussions with a given Repository. +""" +type Label implements Node { + """ + Identifies the label color. + """ + color: String! + + """ + Identifies the date and time when the label was created. + """ + createdAt: DateTime + + """ + A brief description of this label. + """ + description: String + + """ + The Node ID of the Label object + """ + id: ID! + + """ + Indicates whether or not this is a default label. + """ + isDefault: Boolean! + + """ + A list of issues associated with this label. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Identifies the label name. + """ + name: String! + + """ + A list of pull requests associated with this label. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + The repository associated with this label. + """ + repository: Repository! + + """ + The HTTP path for this label. + """ + resourcePath: URI! + + """ + Identifies the date and time when the label was last updated. + """ + updatedAt: DateTime + + """ + The HTTP URL for this label. + """ + url: URI! +} + +""" +The connection type for Label. +""" +type LabelConnection { + """ + A list of edges. + """ + edges: [LabelEdge] + + """ + A list of nodes. + """ + nodes: [Label] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type LabelEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Label +} + +""" +Ways in which lists of labels can be ordered upon return. +""" +input LabelOrder { + """ + The direction in which to order labels by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order labels by. + """ + field: LabelOrderField! +} + +""" +Properties by which label connections can be ordered. +""" +enum LabelOrderField { + """ + Order labels by creation time + """ + CREATED_AT + + """ + Order labels by name + """ + NAME +} + +""" +An object that can have labels assigned to it. +""" +interface Labelable { + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + Indicates if the viewer can edit labels for this object. + """ + viewerCanLabel: Boolean! +} + +""" +Represents a 'labeled' event on a given issue or pull request. +""" +type LabeledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the LabeledEvent object + """ + id: ID! + + """ + Identifies the label associated with the 'labeled' event. + """ + label: Label! + + """ + Identifies the `Labelable` associated with the event. + """ + labelable: Labelable! +} + +""" +Represents a given language found in repositories. +""" +type Language implements Node { + """ + The color defined for the current language. + """ + color: String + + """ + The Node ID of the Language object + """ + id: ID! + + """ + The name of the current language. + """ + name: String! +} + +""" +A list of languages associated with the parent. +""" +type LanguageConnection { + """ + A list of edges. + """ + edges: [LanguageEdge] + + """ + A list of nodes. + """ + nodes: [Language] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + The total size in bytes of files written in that language. + """ + totalSize: Int! +} + +""" +Represents the language of a repository. +""" +type LanguageEdge { + cursor: String! + node: Language! + + """ + The number of bytes of code written in the language. + """ + size: Int! +} + +""" +Ordering options for language connections. +""" +input LanguageOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order languages by. + """ + field: LanguageOrderField! +} + +""" +Properties by which language connections can be ordered. +""" +enum LanguageOrderField { + """ + Order languages by the size of all files containing the language + """ + SIZE +} + +""" +A repository's open source license +""" +type License implements Node { + """ + The full text of the license + """ + body: String! + + """ + The conditions set by the license + """ + conditions: [LicenseRule]! + + """ + A human-readable description of the license + """ + description: String + + """ + Whether the license should be featured + """ + featured: Boolean! + + """ + Whether the license should be displayed in license pickers + """ + hidden: Boolean! + + """ + The Node ID of the License object + """ + id: ID! + + """ + Instructions on how to implement the license + """ + implementation: String + + """ + The lowercased SPDX ID of the license + """ + key: String! + + """ + The limitations set by the license + """ + limitations: [LicenseRule]! + + """ + The license full name specified by + """ + name: String! + + """ + Customary short name if applicable (e.g, GPLv3) + """ + nickname: String + + """ + The permissions set by the license + """ + permissions: [LicenseRule]! + + """ + Whether the license is a pseudo-license placeholder (e.g., other, no-license) + """ + pseudoLicense: Boolean! + + """ + Short identifier specified by + """ + spdxId: String + + """ + URL to the license on + """ + url: URI +} + +""" +Describes a License's conditions, permissions, and limitations +""" +type LicenseRule { + """ + A description of the rule + """ + description: String! + + """ + The machine-readable rule key + """ + key: String! + + """ + The human-readable rule label + """ + label: String! +} + +""" +Autogenerated input type of LinkProjectV2ToRepository +""" +input LinkProjectV2ToRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the project to link to the repository. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The ID of the repository to link to the project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of LinkProjectV2ToRepository. +""" +type LinkProjectV2ToRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository the project is linked to. + """ + repository: Repository +} + +""" +Autogenerated input type of LinkProjectV2ToTeam +""" +input LinkProjectV2ToTeamInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the project to link to the team. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The ID of the team to link to the project. + """ + teamId: ID! @possibleTypes(concreteTypes: ["Team"]) +} + +""" +Autogenerated return type of LinkProjectV2ToTeam. +""" +type LinkProjectV2ToTeamPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The team the project is linked to + """ + team: Team +} + +""" +Autogenerated input type of LinkRepositoryToProject +""" +input LinkRepositoryToProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to link to a Repository + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The ID of the Repository to link to a Project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of LinkRepositoryToProject. +""" +type LinkRepositoryToProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The linked Project. + """ + project: Project + + """ + The linked Repository. + """ + repository: Repository +} + +""" +A branch linked to an issue. +""" +type LinkedBranch implements Node { + """ + The Node ID of the LinkedBranch object + """ + id: ID! + + """ + The branch's ref. + """ + ref: Ref +} + +""" +A list of branches linked to an issue. +""" +type LinkedBranchConnection { + """ + A list of edges. + """ + edges: [LinkedBranchEdge] + + """ + A list of nodes. + """ + nodes: [LinkedBranch] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type LinkedBranchEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: LinkedBranch +} + +""" +Autogenerated input type of LockLockable +""" +input LockLockableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A reason for why the item will be locked. + """ + lockReason: LockReason + + """ + ID of the item to be locked. + """ + lockableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Lockable") +} + +""" +Autogenerated return type of LockLockable. +""" +type LockLockablePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was locked. + """ + lockedRecord: Lockable +} + +""" +The possible reasons that an issue or pull request was locked. +""" +enum LockReason { + """ + The issue or pull request was locked because the conversation was off-topic. + """ + OFF_TOPIC + + """ + The issue or pull request was locked because the conversation was resolved. + """ + RESOLVED + + """ + The issue or pull request was locked because the conversation was spam. + """ + SPAM + + """ + The issue or pull request was locked because the conversation was too heated. + """ + TOO_HEATED +} + +""" +An object that can be locked. +""" +interface Lockable { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + `true` if the object is locked + """ + locked: Boolean! +} + +""" +Represents a 'locked' event on a given issue or pull request. +""" +type LockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the LockedEvent object + """ + id: ID! + + """ + Reason that the conversation was locked (optional). + """ + lockReason: LockReason + + """ + Object that was locked. + """ + lockable: Lockable! +} + +""" +A placeholder user for attribution of imported data on GitHub. +""" +type Mannequin implements Actor & Node & UniformResourceLocatable { + """ + A URL pointing to the GitHub App's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The user that has claimed the data attributed to this mannequin. + """ + claimant: User + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The mannequin's email on the source instance. + """ + email: String + + """ + The Node ID of the Mannequin object + """ + id: ID! + + """ + The username of the actor. + """ + login: String! + + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The URL to this resource. + """ + url: URI! +} + +""" +A list of mannequins. +""" +type MannequinConnection { + """ + A list of edges. + """ + edges: [MannequinEdge] + + """ + A list of nodes. + """ + nodes: [Mannequin] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a mannequin. +""" +type MannequinEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Mannequin +} + +""" +Ordering options for mannequins. +""" +input MannequinOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order mannequins by. + """ + field: MannequinOrderField! +} + +""" +Properties by which mannequins can be ordered. +""" +enum MannequinOrderField { + """ + Order mannequins why when they were created. + """ + CREATED_AT + + """ + Order mannequins alphabetically by their source login. + """ + LOGIN +} + +""" +Autogenerated input type of MarkDiscussionCommentAsAnswer +""" +input MarkDiscussionCommentAsAnswerInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion comment to mark as an answer. + """ + id: ID! @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of MarkDiscussionCommentAsAnswer. +""" +type MarkDiscussionCommentAsAnswerPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that includes the chosen comment. + """ + discussion: Discussion +} + +""" +Autogenerated input type of MarkFileAsViewed +""" +input MarkFileAsViewedInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The path of the file to mark as viewed + """ + path: String! + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MarkFileAsViewed. +""" +type MarkFileAsViewedPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of MarkProjectV2AsTemplate +""" +input MarkProjectV2AsTemplateInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to mark as a template. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of MarkProjectV2AsTemplate. +""" +type MarkProjectV2AsTemplatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of MarkPullRequestReadyForReview +""" +input MarkPullRequestReadyForReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be marked as ready for review. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MarkPullRequestReadyForReview. +""" +type MarkPullRequestReadyForReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that is ready for review. + """ + pullRequest: PullRequest +} + +""" +Represents a 'marked_as_duplicate' event on a given issue or pull request. +""" +type MarkedAsDuplicateEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + The authoritative issue or pull request which has been duplicated by another. + """ + canonical: IssueOrPullRequest + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The issue or pull request which has been marked as a duplicate of another. + """ + duplicate: IssueOrPullRequest + + """ + The Node ID of the MarkedAsDuplicateEvent object + """ + id: ID! + + """ + Canonical and duplicate belong to different repositories. + """ + isCrossRepository: Boolean! +} + +""" +A public description of a Marketplace category. +""" +type MarketplaceCategory implements Node { + """ + The category's description. + """ + description: String + + """ + The technical description of how apps listed in this category work with GitHub. + """ + howItWorks: String + + """ + The Node ID of the MarketplaceCategory object + """ + id: ID! + + """ + The category's name. + """ + name: String! + + """ + How many Marketplace listings have this as their primary category. + """ + primaryListingCount: Int! + + """ + The HTTP path for this Marketplace category. + """ + resourcePath: URI! + + """ + How many Marketplace listings have this as their secondary category. + """ + secondaryListingCount: Int! + + """ + The short name of the category used in its URL. + """ + slug: String! + + """ + The HTTP URL for this Marketplace category. + """ + url: URI! +} + +""" +A listing in the GitHub integration marketplace. +""" +type MarketplaceListing implements Node { + """ + The GitHub App this listing represents. + """ + app: App + + """ + URL to the listing owner's company site. + """ + companyUrl: URI + + """ + The HTTP path for configuring access to the listing's integration or OAuth app + """ + configurationResourcePath: URI! + + """ + The HTTP URL for configuring access to the listing's integration or OAuth app + """ + configurationUrl: URI! + + """ + URL to the listing's documentation. + """ + documentationUrl: URI + + """ + The listing's detailed description. + """ + extendedDescription: String + + """ + The listing's detailed description rendered to HTML. + """ + extendedDescriptionHTML: HTML! + + """ + The listing's introductory description. + """ + fullDescription: String! + + """ + The listing's introductory description rendered to HTML. + """ + fullDescriptionHTML: HTML! + + """ + Does this listing have any plans with a free trial? + """ + hasPublishedFreeTrialPlans: Boolean! + + """ + Does this listing have a terms of service link? + """ + hasTermsOfService: Boolean! + + """ + Whether the creator of the app is a verified org + """ + hasVerifiedOwner: Boolean! + + """ + A technical description of how this app works with GitHub. + """ + howItWorks: String + + """ + The listing's technical description rendered to HTML. + """ + howItWorksHTML: HTML! + + """ + The Node ID of the MarketplaceListing object + """ + id: ID! + + """ + URL to install the product to the viewer's account or organization. + """ + installationUrl: URI + + """ + Whether this listing's app has been installed for the current viewer + """ + installedForViewer: Boolean! + + """ + Whether this listing has been removed from the Marketplace. + """ + isArchived: Boolean! + + """ + Whether this listing is still an editable draft that has not been submitted + for review and is not publicly visible in the Marketplace. + """ + isDraft: Boolean! + + """ + Whether the product this listing represents is available as part of a paid plan. + """ + isPaid: Boolean! + + """ + Whether this listing has been approved for display in the Marketplace. + """ + isPublic: Boolean! + + """ + Whether this listing has been rejected by GitHub for display in the Marketplace. + """ + isRejected: Boolean! + + """ + Whether this listing has been approved for unverified display in the Marketplace. + """ + isUnverified: Boolean! + + """ + Whether this draft listing has been submitted for review for approval to be unverified in the Marketplace. + """ + isUnverifiedPending: Boolean! + + """ + Whether this draft listing has been submitted for review from GitHub for approval to be verified in the Marketplace. + """ + isVerificationPendingFromDraft: Boolean! + + """ + Whether this unverified listing has been submitted for review from GitHub for approval to be verified in the Marketplace. + """ + isVerificationPendingFromUnverified: Boolean! + + """ + Whether this listing has been approved for verified display in the Marketplace. + """ + isVerified: Boolean! + + """ + The hex color code, without the leading '#', for the logo background. + """ + logoBackgroundColor: String! + + """ + URL for the listing's logo image. + """ + logoUrl( + """ + The size in pixels of the resulting square image. + """ + size: Int = 400 + ): URI + + """ + The listing's full name. + """ + name: String! + + """ + The listing's very short description without a trailing period or ampersands. + """ + normalizedShortDescription: String! + + """ + URL to the listing's detailed pricing. + """ + pricingUrl: URI + + """ + The category that best describes the listing. + """ + primaryCategory: MarketplaceCategory! + + """ + URL to the listing's privacy policy, may return an empty string for listings that do not require a privacy policy URL. + """ + privacyPolicyUrl: URI! + + """ + The HTTP path for the Marketplace listing. + """ + resourcePath: URI! + + """ + The URLs for the listing's screenshots. + """ + screenshotUrls: [String]! + + """ + An alternate category that describes the listing. + """ + secondaryCategory: MarketplaceCategory + + """ + The listing's very short description. + """ + shortDescription: String! + + """ + The short name of the listing used in its URL. + """ + slug: String! + + """ + URL to the listing's status page. + """ + statusUrl: URI + + """ + An email address for support for this listing's app. + """ + supportEmail: String + + """ + Either a URL or an email address for support for this listing's app, may + return an empty string for listings that do not require a support URL. + """ + supportUrl: URI! + + """ + URL to the listing's terms of service. + """ + termsOfServiceUrl: URI + + """ + The HTTP URL for the Marketplace listing. + """ + url: URI! + + """ + Can the current viewer add plans for this Marketplace listing. + """ + viewerCanAddPlans: Boolean! + + """ + Can the current viewer approve this Marketplace listing. + """ + viewerCanApprove: Boolean! + + """ + Can the current viewer delist this Marketplace listing. + """ + viewerCanDelist: Boolean! + + """ + Can the current viewer edit this Marketplace listing. + """ + viewerCanEdit: Boolean! + + """ + Can the current viewer edit the primary and secondary category of this + Marketplace listing. + """ + viewerCanEditCategories: Boolean! + + """ + Can the current viewer edit the plans for this Marketplace listing. + """ + viewerCanEditPlans: Boolean! + + """ + Can the current viewer return this Marketplace listing to draft state + so it becomes editable again. + """ + viewerCanRedraft: Boolean! + + """ + Can the current viewer reject this Marketplace listing by returning it to + an editable draft state or rejecting it entirely. + """ + viewerCanReject: Boolean! + + """ + Can the current viewer request this listing be reviewed for display in + the Marketplace as verified. + """ + viewerCanRequestApproval: Boolean! + + """ + Indicates whether the current user has an active subscription to this Marketplace listing. + """ + viewerHasPurchased: Boolean! + + """ + Indicates if the current user has purchased a subscription to this Marketplace listing + for all of the organizations the user owns. + """ + viewerHasPurchasedForAllOrganizations: Boolean! + + """ + Does the current viewer role allow them to administer this Marketplace listing. + """ + viewerIsListingAdmin: Boolean! +} + +""" +Look up Marketplace Listings +""" +type MarketplaceListingConnection { + """ + A list of edges. + """ + edges: [MarketplaceListingEdge] + + """ + A list of nodes. + """ + nodes: [MarketplaceListing] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MarketplaceListingEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MarketplaceListing +} + +""" +Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph. +""" +type MaxFilePathLengthParameters { + """ + The maximum amount of characters allowed in file paths. + """ + maxFilePathLength: Int! +} + +""" +Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph. +""" +input MaxFilePathLengthParametersInput { + """ + The maximum amount of characters allowed in file paths. + """ + maxFilePathLength: Int! +} + +""" +Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph. +""" +type MaxFileSizeParameters { + """ + The maximum file size allowed in megabytes. This limit does not apply to Git Large File Storage (Git LFS). + """ + maxFileSize: Int! +} + +""" +Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph. +""" +input MaxFileSizeParametersInput { + """ + The maximum file size allowed in megabytes. This limit does not apply to Git Large File Storage (Git LFS). + """ + maxFileSize: Int! +} + +""" +Represents a member feature request notification +""" +type MemberFeatureRequestNotification implements Node { + """ + Represents member feature request body containing entity name and the number of feature requests + """ + body: String! + + """ + The Node ID of the MemberFeatureRequestNotification object + """ + id: ID! + + """ + Represents member feature request notification title + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Entities that have members who can set status messages. +""" +interface MemberStatusable { + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! +} + +""" +Audit log entry for a members_can_delete_repos.clear event. +""" +type MembersCanDeleteReposClearAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + + """ + The Node ID of the MembersCanDeleteReposClearAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a members_can_delete_repos.disable event. +""" +type MembersCanDeleteReposDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + + """ + The Node ID of the MembersCanDeleteReposDisableAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a members_can_delete_repos.enable event. +""" +type MembersCanDeleteReposEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + + """ + The Node ID of the MembersCanDeleteReposEnableAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Represents a 'mentioned' event on a given issue or pull request. +""" +type MentionedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the MentionedEvent object + """ + id: ID! +} + +""" +Autogenerated input type of MergeBranch +""" +input MergeBranchInput { + """ + The email address to associate with this commit. + """ + authorEmail: String + + """ + The name of the base branch that the provided head will be merged into. + """ + base: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Message to use for the merge commit. If omitted, a default will be used. + """ + commitMessage: String + + """ + The head to merge into the base branch. This can be a branch name or a commit GitObjectID. + """ + head: String! + + """ + The Node ID of the Repository containing the base branch that will be modified. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of MergeBranch. +""" +type MergeBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The resulting merge Commit. + """ + mergeCommit: Commit +} + +""" +The possible default commit messages for merges. +""" +enum MergeCommitMessage { + """ + Default to a blank commit message. + """ + BLANK + + """ + Default to the pull request's body. + """ + PR_BODY + + """ + Default to the pull request's title. + """ + PR_TITLE +} + +""" +The possible default commit titles for merges. +""" +enum MergeCommitTitle { + """ + Default to the classic title for a merge message (e.g., Merge pull request #123 from branch-name). + """ + MERGE_MESSAGE + + """ + Default to the pull request's title. + """ + PR_TITLE +} + +""" +Autogenerated input type of MergePullRequest +""" +input MergePullRequestInput { + """ + The email address to associate with this merge. + """ + authorEmail: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Commit body to use for the merge commit; if omitted, a default message will be used + """ + commitBody: String + + """ + Commit headline to use for the merge commit; if omitted, a default message will be used. + """ + commitHeadline: String + + """ + OID that the pull request head ref must match to allow merge; if omitted, no check is performed. + """ + expectedHeadOid: GitObjectID + + """ + The merge method to use. If omitted, defaults to 'MERGE' + """ + mergeMethod: PullRequestMergeMethod = MERGE + + """ + ID of the pull request to be merged. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MergePullRequest. +""" +type MergePullRequestPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was merged. + """ + pullRequest: PullRequest +} + +""" +The queue of pull request entries to be merged into a protected branch in a repository. +""" +type MergeQueue implements Node { + """ + The configuration for this merge queue + """ + configuration: MergeQueueConfiguration + + """ + The entries in the queue + """ + entries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): MergeQueueEntryConnection + + """ + The Node ID of the MergeQueue object + """ + id: ID! + + """ + The estimated time in seconds until a newly added entry would be merged + """ + nextEntryEstimatedTimeToMerge: Int + + """ + The repository this merge queue belongs to + """ + repository: Repository + + """ + The HTTP path for this merge queue + """ + resourcePath: URI! + + """ + The HTTP URL for this merge queue + """ + url: URI! +} + +""" +Configuration for a MergeQueue +""" +type MergeQueueConfiguration { + """ + The amount of time in minutes to wait for a check response before considering it a failure. + """ + checkResponseTimeout: Int + + """ + The maximum number of entries to build at once. + """ + maximumEntriesToBuild: Int + + """ + The maximum number of entries to merge at once. + """ + maximumEntriesToMerge: Int + + """ + The merge method to use for this queue. + """ + mergeMethod: PullRequestMergeMethod + + """ + The strategy to use when merging entries. + """ + mergingStrategy: MergeQueueMergingStrategy + + """ + The minimum number of entries required to merge at once. + """ + minimumEntriesToMerge: Int + + """ + The amount of time in minutes to wait before ignoring the minumum number of + entries in the queue requirement and merging a collection of entries + """ + minimumEntriesToMergeWaitTime: Int +} + +""" +Entries in a MergeQueue +""" +type MergeQueueEntry implements Node { + """ + The base commit for this entry + """ + baseCommit: Commit + + """ + The date and time this entry was added to the merge queue + """ + enqueuedAt: DateTime! + + """ + The actor that enqueued this entry + """ + enqueuer: Actor! + + """ + The estimated time in seconds until this entry will be merged + """ + estimatedTimeToMerge: Int + + """ + The head commit for this entry + """ + headCommit: Commit + + """ + The Node ID of the MergeQueueEntry object + """ + id: ID! + + """ + Whether this pull request should jump the queue + """ + jump: Boolean! + + """ + The merge queue that this entry belongs to + """ + mergeQueue: MergeQueue + + """ + The position of this entry in the queue + """ + position: Int! + + """ + The pull request that will be added to a merge group + """ + pullRequest: PullRequest + + """ + Does this pull request need to be deployed on its own + """ + solo: Boolean! + + """ + The state of this entry in the queue + """ + state: MergeQueueEntryState! +} + +""" +The connection type for MergeQueueEntry. +""" +type MergeQueueEntryConnection { + """ + A list of edges. + """ + edges: [MergeQueueEntryEdge] + + """ + A list of nodes. + """ + nodes: [MergeQueueEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MergeQueueEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MergeQueueEntry +} + +""" +The possible states for a merge queue entry. +""" +enum MergeQueueEntryState { + """ + The entry is currently waiting for checks to pass. + """ + AWAITING_CHECKS + + """ + The entry is currently locked. + """ + LOCKED + + """ + The entry is currently mergeable. + """ + MERGEABLE + + """ + The entry is currently queued. + """ + QUEUED + + """ + The entry is currently unmergeable. + """ + UNMERGEABLE +} + +""" +When set to ALLGREEN, the merge commit created by merge queue for each PR in the +group must pass all required checks to merge. When set to HEADGREEN, only the +commit at the head of the merge group, i.e. the commit containing changes from +all of the PRs in the group, must pass its required checks to merge. +""" +enum MergeQueueGroupingStrategy { + """ + The merge commit created by merge queue for each PR in the group must pass all required checks to merge + """ + ALLGREEN + + """ + Only the commit at the head of the merge group must pass its required checks to merge. + """ + HEADGREEN +} + +""" +Method to use when merging changes from queued pull requests. +""" +enum MergeQueueMergeMethod { + """ + Merge commit + """ + MERGE + + """ + Rebase and merge + """ + REBASE + + """ + Squash and merge + """ + SQUASH +} + +""" +The possible merging strategies for a merge queue. +""" +enum MergeQueueMergingStrategy { + """ + Entries only allowed to merge if they are passing. + """ + ALLGREEN + + """ + Failing Entires are allowed to merge if they are with a passing entry. + """ + HEADGREEN +} + +""" +Merges must be performed via a merge queue. +""" +type MergeQueueParameters { + """ + Maximum time for a required status check to report a conclusion. After this + much time has elapsed, checks that have not reported a conclusion will be + assumed to have failed + """ + checkResponseTimeoutMinutes: Int! + + """ + When set to ALLGREEN, the merge commit created by merge queue for each PR in + the group must pass all required checks to merge. When set to HEADGREEN, only + the commit at the head of the merge group, i.e. the commit containing changes + from all of the PRs in the group, must pass its required checks to merge. + """ + groupingStrategy: MergeQueueGroupingStrategy! + + """ + Limit the number of queued pull requests requesting checks and workflow runs at the same time. + """ + maxEntriesToBuild: Int! + + """ + The maximum number of PRs that will be merged together in a group. + """ + maxEntriesToMerge: Int! + + """ + Method to use when merging changes from queued pull requests. + """ + mergeMethod: MergeQueueMergeMethod! + + """ + The minimum number of PRs that will be merged together in a group. + """ + minEntriesToMerge: Int! + + """ + The time merge queue should wait after the first PR is added to the queue for + the minimum group size to be met. After this time has elapsed, the minimum + group size will be ignored and a smaller group will be merged. + """ + minEntriesToMergeWaitMinutes: Int! +} + +""" +Merges must be performed via a merge queue. +""" +input MergeQueueParametersInput { + """ + Maximum time for a required status check to report a conclusion. After this + much time has elapsed, checks that have not reported a conclusion will be + assumed to have failed + """ + checkResponseTimeoutMinutes: Int! + + """ + When set to ALLGREEN, the merge commit created by merge queue for each PR in + the group must pass all required checks to merge. When set to HEADGREEN, only + the commit at the head of the merge group, i.e. the commit containing changes + from all of the PRs in the group, must pass its required checks to merge. + """ + groupingStrategy: MergeQueueGroupingStrategy! + + """ + Limit the number of queued pull requests requesting checks and workflow runs at the same time. + """ + maxEntriesToBuild: Int! + + """ + The maximum number of PRs that will be merged together in a group. + """ + maxEntriesToMerge: Int! + + """ + Method to use when merging changes from queued pull requests. + """ + mergeMethod: MergeQueueMergeMethod! + + """ + The minimum number of PRs that will be merged together in a group. + """ + minEntriesToMerge: Int! + + """ + The time merge queue should wait after the first PR is added to the queue for + the minimum group size to be met. After this time has elapsed, the minimum + group size will be ignored and a smaller group will be merged. + """ + minEntriesToMergeWaitMinutes: Int! +} + +""" +Detailed status information about a pull request merge. +""" +enum MergeStateStatus { + """ + The head ref is out of date. + """ + BEHIND + + """ + The merge is blocked. + """ + BLOCKED + + """ + Mergeable and passing commit status. + """ + CLEAN + + """ + The merge commit cannot be cleanly created. + """ + DIRTY + + """ + The merge is blocked due to the pull request being a draft. + """ + DRAFT + @deprecated( + reason: "DRAFT state will be removed from this enum and `isDraft` should be used instead Use PullRequest.isDraft instead. Removal on 2021-01-01 UTC." + ) + + """ + Mergeable with passing commit status and pre-receive hooks. + """ + HAS_HOOKS + + """ + The state cannot currently be determined. + """ + UNKNOWN + + """ + Mergeable with non-passing commit status. + """ + UNSTABLE +} + +""" +Whether or not a PullRequest can be merged. +""" +enum MergeableState { + """ + The pull request cannot be merged due to merge conflicts. + """ + CONFLICTING + + """ + The pull request can be merged. + """ + MERGEABLE + + """ + The mergeability of the pull request is still being calculated. + """ + UNKNOWN +} + +""" +Represents a 'merged' event on a given pull request. +""" +type MergedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the commit associated with the `merge` event. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the MergedEvent object + """ + id: ID! + + """ + Identifies the Ref associated with the `merge` event. + """ + mergeRef: Ref + + """ + Identifies the name of the Ref associated with the `merge` event. + """ + mergeRefName: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this merged event. + """ + resourcePath: URI! + + """ + The HTTP URL for this merged event. + """ + url: URI! +} + +""" +Represents a GitHub Enterprise Importer (GEI) migration. +""" +interface Migration { + """ + The migration flag to continue on error. + """ + continueOnError: Boolean! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: String + + """ + The reason the migration failed. + """ + failureReason: String + + """ + The Node ID of the Migration object + """ + id: ID! + + """ + The URL for the migration log (expires 1 day after migration completes). + """ + migrationLogUrl: URI + + """ + The migration source. + """ + migrationSource: MigrationSource! + + """ + The target repository name. + """ + repositoryName: String! + + """ + The migration source URL, for example `https://github.com` or `https://monalisa.ghe.com`. + """ + sourceUrl: URI! + + """ + The migration state. + """ + state: MigrationState! + + """ + The number of warnings encountered for this migration. To review the warnings, + check the [Migration Log](https://docs.github.com/migrations/using-github-enterprise-importer/completing-your-migration-with-github-enterprise-importer/accessing-your-migration-logs-for-github-enterprise-importer). + """ + warningsCount: Int! +} + +""" +A GitHub Enterprise Importer (GEI) migration source. +""" +type MigrationSource implements Node { + """ + The Node ID of the MigrationSource object + """ + id: ID! + + """ + The migration source name. + """ + name: String! + + """ + The migration source type. + """ + type: MigrationSourceType! + + """ + The migration source URL, for example `https://github.com` or `https://monalisa.ghe.com`. + """ + url: URI! +} + +""" +Represents the different GitHub Enterprise Importer (GEI) migration sources. +""" +enum MigrationSourceType { + """ + An Azure DevOps migration source. + """ + AZURE_DEVOPS + + """ + A Bitbucket Server migration source. + """ + BITBUCKET_SERVER + + """ + A GitHub Migration API source. + """ + GITHUB_ARCHIVE +} + +""" +The GitHub Enterprise Importer (GEI) migration state. +""" +enum MigrationState { + """ + The migration has failed. + """ + FAILED + + """ + The migration has invalid credentials. + """ + FAILED_VALIDATION + + """ + The migration is in progress. + """ + IN_PROGRESS + + """ + The migration has not started. + """ + NOT_STARTED + + """ + The migration needs to have its credentials validated. + """ + PENDING_VALIDATION + + """ + The migration has been queued. + """ + QUEUED + + """ + The migration has succeeded. + """ + SUCCEEDED +} + +""" +Represents a Milestone object on a given repository. +""" +type Milestone implements Closable & Node & UniformResourceLocatable { + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + Identifies the number of closed issues associated with the milestone. + """ + closedIssueCount: Int! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who created the milestone. + """ + creator: Actor + + """ + Identifies the description of the milestone. + """ + description: String + + """ + The HTML rendered description of the milestone using GitHub Flavored Markdown. + """ + descriptionHTML: String + + """ + Identifies the due date of the milestone. + """ + dueOn: DateTime + + """ + The Node ID of the Milestone object + """ + id: ID! + + """ + A list of issues associated with the milestone. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Identifies the number of the milestone. + """ + number: Int! + + """ + Identifies the number of open issues associated with the milestone. + """ + openIssueCount: Int! + + """ + Identifies the percentage complete for the milestone + """ + progressPercentage: Float! + + """ + A list of pull requests associated with the milestone. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + The repository associated with this milestone. + """ + repository: Repository! + + """ + The HTTP path for this milestone + """ + resourcePath: URI! + + """ + Identifies the state of the milestone. + """ + state: MilestoneState! + + """ + Identifies the title of the milestone. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this milestone + """ + url: URI! + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! +} + +""" +The connection type for Milestone. +""" +type MilestoneConnection { + """ + A list of edges. + """ + edges: [MilestoneEdge] + + """ + A list of nodes. + """ + nodes: [Milestone] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MilestoneEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Milestone +} + +""" +Types that can be inside a Milestone. +""" +union MilestoneItem = Issue | PullRequest + +""" +Ordering options for milestone connections. +""" +input MilestoneOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order milestones by. + """ + field: MilestoneOrderField! +} + +""" +Properties by which milestone connections can be ordered. +""" +enum MilestoneOrderField { + """ + Order milestones by when they were created. + """ + CREATED_AT + + """ + Order milestones by when they are due. + """ + DUE_DATE + + """ + Order milestones by their number. + """ + NUMBER + + """ + Order milestones by when they were last updated. + """ + UPDATED_AT +} + +""" +The possible states of a milestone. +""" +enum MilestoneState { + """ + A milestone that has been closed. + """ + CLOSED + + """ + A milestone that is still open. + """ + OPEN +} + +""" +Represents a 'milestoned' event on a given issue or pull request. +""" +type MilestonedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the MilestonedEvent object + """ + id: ID! + + """ + Identifies the milestone title associated with the 'milestoned' event. + """ + milestoneTitle: String! + + """ + Object referenced by event. + """ + subject: MilestoneItem! +} + +""" +Entities that can be minimized. +""" +interface Minimizable { + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! +} + +""" +Autogenerated input type of MinimizeComment +""" +input MinimizeCommentInput { + """ + The classification of comment + """ + classifier: ReportedContentClassifiers! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! + @possibleTypes( + concreteTypes: [ + "CommitComment" + "DiscussionComment" + "GistComment" + "IssueComment" + "PullRequestReview" + "PullRequestReviewComment" + ] + abstractType: "Minimizable" + ) +} + +""" +Autogenerated return type of MinimizeComment. +""" +type MinimizeCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The comment that was minimized. + """ + minimizedComment: Minimizable +} + +""" +Autogenerated input type of MoveProjectCard +""" +input MoveProjectCardInput { + """ + Place the new card after the card with this id. Pass null to place it at the top. + """ + afterCardId: ID @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + The id of the card to move. + """ + cardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to move it into. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of MoveProjectCard. +""" +type MoveProjectCardPayload { + """ + The new edge of the moved card. + """ + cardEdge: ProjectCardEdge + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of MoveProjectColumn +""" +input MoveProjectColumnInput { + """ + Place the new column after the column with this id. Pass null to place it at the front. + """ + afterColumnId: ID @possibleTypes(concreteTypes: ["ProjectColumn"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to move. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of MoveProjectColumn. +""" +type MoveProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new edge of the moved column. + """ + columnEdge: ProjectColumnEdge +} + +""" +Represents a 'moved_columns_in_project' event on a given issue or pull request. +""" +type MovedColumnsInProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The Node ID of the MovedColumnsInProjectEvent object + """ + id: ID! + + """ + Column name the issue or pull request was moved from. + """ + previousProjectColumnName: String! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Project referenced by event. + """ + project: Project + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Column name the issue or pull request was moved to. + """ + projectColumnName: String! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) +} + +""" +The root query for implementing GraphQL mutations. +""" +type Mutation { + """ + Clear all of a customer's queued migrations + """ + abortQueuedMigrations( + """ + Parameters for AbortQueuedMigrations + """ + input: AbortQueuedMigrationsInput! + ): AbortQueuedMigrationsPayload + + """ + Abort a repository migration queued or in progress. + """ + abortRepositoryMigration( + """ + Parameters for AbortRepositoryMigration + """ + input: AbortRepositoryMigrationInput! + ): AbortRepositoryMigrationPayload + + """ + Accepts a pending invitation for a user to become an administrator of an enterprise. + """ + acceptEnterpriseAdministratorInvitation( + """ + Parameters for AcceptEnterpriseAdministratorInvitation + """ + input: AcceptEnterpriseAdministratorInvitationInput! + ): AcceptEnterpriseAdministratorInvitationPayload + + """ + Accepts a pending invitation for a user to become an unaffiliated member of an enterprise. + """ + acceptEnterpriseMemberInvitation( + """ + Parameters for AcceptEnterpriseMemberInvitation + """ + input: AcceptEnterpriseMemberInvitationInput! + ): AcceptEnterpriseMemberInvitationPayload + + """ + Applies a suggested topic to the repository. + """ + acceptTopicSuggestion( + """ + Parameters for AcceptTopicSuggestion + """ + input: AcceptTopicSuggestionInput! + ): AcceptTopicSuggestionPayload + + """ + Access user namespace repository for a temporary duration. + """ + accessUserNamespaceRepository( + """ + Parameters for AccessUserNamespaceRepository + """ + input: AccessUserNamespaceRepositoryInput! + ): AccessUserNamespaceRepositoryPayload + + """ + Adds assignees to an assignable object. + """ + addAssigneesToAssignable( + """ + Parameters for AddAssigneesToAssignable + """ + input: AddAssigneesToAssignableInput! + ): AddAssigneesToAssignablePayload + + """ + Adds a comment to an Issue or Pull Request. + """ + addComment( + """ + Parameters for AddComment + """ + input: AddCommentInput! + ): AddCommentPayload + + """ + Adds a comment to a Discussion, possibly as a reply to another comment. + """ + addDiscussionComment( + """ + Parameters for AddDiscussionComment + """ + input: AddDiscussionCommentInput! + ): AddDiscussionCommentPayload + + """ + Vote for an option in a discussion poll. + """ + addDiscussionPollVote( + """ + Parameters for AddDiscussionPollVote + """ + input: AddDiscussionPollVoteInput! + ): AddDiscussionPollVotePayload + + """ + Adds enterprise members to an organization within the enterprise. + """ + addEnterpriseOrganizationMember( + """ + Parameters for AddEnterpriseOrganizationMember + """ + input: AddEnterpriseOrganizationMemberInput! + ): AddEnterpriseOrganizationMemberPayload + + """ + Adds a support entitlement to an enterprise member. + """ + addEnterpriseSupportEntitlement( + """ + Parameters for AddEnterpriseSupportEntitlement + """ + input: AddEnterpriseSupportEntitlementInput! + ): AddEnterpriseSupportEntitlementPayload + + """ + Adds labels to a labelable object. + """ + addLabelsToLabelable( + """ + Parameters for AddLabelsToLabelable + """ + input: AddLabelsToLabelableInput! + ): AddLabelsToLabelablePayload + + """ + Adds a card to a ProjectColumn. Either `contentId` or `note` must be provided but **not** both. + """ + addProjectCard( + """ + Parameters for AddProjectCard + """ + input: AddProjectCardInput! + ): AddProjectCardPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Adds a column to a Project. + """ + addProjectColumn( + """ + Parameters for AddProjectColumn + """ + input: AddProjectColumnInput! + ): AddProjectColumnPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Creates a new draft issue and add it to a Project. + """ + addProjectV2DraftIssue( + """ + Parameters for AddProjectV2DraftIssue + """ + input: AddProjectV2DraftIssueInput! + ): AddProjectV2DraftIssuePayload + + """ + Links an existing content instance to a Project. + """ + addProjectV2ItemById( + """ + Parameters for AddProjectV2ItemById + """ + input: AddProjectV2ItemByIdInput! + ): AddProjectV2ItemByIdPayload + + """ + Adds a review to a Pull Request. + """ + addPullRequestReview( + """ + Parameters for AddPullRequestReview + """ + input: AddPullRequestReviewInput! + ): AddPullRequestReviewPayload + + """ + Adds a comment to a review. + """ + addPullRequestReviewComment( + """ + Parameters for AddPullRequestReviewComment + """ + input: AddPullRequestReviewCommentInput! + ): AddPullRequestReviewCommentPayload + + """ + Adds a new thread to a pending Pull Request Review. + """ + addPullRequestReviewThread( + """ + Parameters for AddPullRequestReviewThread + """ + input: AddPullRequestReviewThreadInput! + ): AddPullRequestReviewThreadPayload + + """ + Adds a reply to an existing Pull Request Review Thread. + """ + addPullRequestReviewThreadReply( + """ + Parameters for AddPullRequestReviewThreadReply + """ + input: AddPullRequestReviewThreadReplyInput! + ): AddPullRequestReviewThreadReplyPayload + + """ + Adds a reaction to a subject. + """ + addReaction( + """ + Parameters for AddReaction + """ + input: AddReactionInput! + ): AddReactionPayload + + """ + Adds a star to a Starrable. + """ + addStar( + """ + Parameters for AddStar + """ + input: AddStarInput! + ): AddStarPayload + + """ + Adds a sub-issue to a given issue + """ + addSubIssue( + """ + Parameters for AddSubIssue + """ + input: AddSubIssueInput! + ): AddSubIssuePayload + + """ + Add an upvote to a discussion or discussion comment. + """ + addUpvote( + """ + Parameters for AddUpvote + """ + input: AddUpvoteInput! + ): AddUpvotePayload + + """ + Adds a verifiable domain to an owning account. + """ + addVerifiableDomain( + """ + Parameters for AddVerifiableDomain + """ + input: AddVerifiableDomainInput! + ): AddVerifiableDomainPayload + + """ + Approve all pending deployments under one or more environments + """ + approveDeployments( + """ + Parameters for ApproveDeployments + """ + input: ApproveDeploymentsInput! + ): ApproveDeploymentsPayload + + """ + Approve a verifiable domain for notification delivery. + """ + approveVerifiableDomain( + """ + Parameters for ApproveVerifiableDomain + """ + input: ApproveVerifiableDomainInput! + ): ApproveVerifiableDomainPayload + + """ + Archives a ProjectV2Item + """ + archiveProjectV2Item( + """ + Parameters for ArchiveProjectV2Item + """ + input: ArchiveProjectV2ItemInput! + ): ArchiveProjectV2ItemPayload + + """ + Marks a repository as archived. + """ + archiveRepository( + """ + Parameters for ArchiveRepository + """ + input: ArchiveRepositoryInput! + ): ArchiveRepositoryPayload + + """ + Cancels a pending invitation for an administrator to join an enterprise. + """ + cancelEnterpriseAdminInvitation( + """ + Parameters for CancelEnterpriseAdminInvitation + """ + input: CancelEnterpriseAdminInvitationInput! + ): CancelEnterpriseAdminInvitationPayload + + """ + Cancels a pending invitation for an unaffiliated member to join an enterprise. + """ + cancelEnterpriseMemberInvitation( + """ + Parameters for CancelEnterpriseMemberInvitation + """ + input: CancelEnterpriseMemberInvitationInput! + ): CancelEnterpriseMemberInvitationPayload + + """ + Cancel an active sponsorship. + """ + cancelSponsorship( + """ + Parameters for CancelSponsorship + """ + input: CancelSponsorshipInput! + ): CancelSponsorshipPayload + + """ + Update your status on GitHub. + """ + changeUserStatus( + """ + Parameters for ChangeUserStatus + """ + input: ChangeUserStatusInput! + ): ChangeUserStatusPayload + + """ + Clears all labels from a labelable object. + """ + clearLabelsFromLabelable( + """ + Parameters for ClearLabelsFromLabelable + """ + input: ClearLabelsFromLabelableInput! + ): ClearLabelsFromLabelablePayload + + """ + This mutation clears the value of a field for an item in a Project. Currently + only text, number, date, assignees, labels, single-select, iteration and + milestone fields are supported. + """ + clearProjectV2ItemFieldValue( + """ + Parameters for ClearProjectV2ItemFieldValue + """ + input: ClearProjectV2ItemFieldValueInput! + ): ClearProjectV2ItemFieldValuePayload + + """ + Creates a new project by cloning configuration from an existing project. + """ + cloneProject( + """ + Parameters for CloneProject + """ + input: CloneProjectInput! + ): CloneProjectPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Create a new repository with the same files and directory structure as a template repository. + """ + cloneTemplateRepository( + """ + Parameters for CloneTemplateRepository + """ + input: CloneTemplateRepositoryInput! + ): CloneTemplateRepositoryPayload + + """ + Close a discussion. + """ + closeDiscussion( + """ + Parameters for CloseDiscussion + """ + input: CloseDiscussionInput! + ): CloseDiscussionPayload + + """ + Close an issue. + """ + closeIssue( + """ + Parameters for CloseIssue + """ + input: CloseIssueInput! + ): CloseIssuePayload + + """ + Close a pull request. + """ + closePullRequest( + """ + Parameters for ClosePullRequest + """ + input: ClosePullRequestInput! + ): ClosePullRequestPayload + + """ + Convert a project note card to one associated with a newly created issue. + """ + convertProjectCardNoteToIssue( + """ + Parameters for ConvertProjectCardNoteToIssue + """ + input: ConvertProjectCardNoteToIssueInput! + ): ConvertProjectCardNoteToIssuePayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Converts a projectV2 draft issue item to an issue. + """ + convertProjectV2DraftIssueItemToIssue( + """ + Parameters for ConvertProjectV2DraftIssueItemToIssue + """ + input: ConvertProjectV2DraftIssueItemToIssueInput! + ): ConvertProjectV2DraftIssueItemToIssuePayload + + """ + Converts a pull request to draft + """ + convertPullRequestToDraft( + """ + Parameters for ConvertPullRequestToDraft + """ + input: ConvertPullRequestToDraftInput! + ): ConvertPullRequestToDraftPayload + + """ + Copy a project. + """ + copyProjectV2( + """ + Parameters for CopyProjectV2 + """ + input: CopyProjectV2Input! + ): CopyProjectV2Payload + + """ + Invites a user to claim reattributable data + """ + createAttributionInvitation( + """ + Parameters for CreateAttributionInvitation + """ + input: CreateAttributionInvitationInput! + ): CreateAttributionInvitationPayload + + """ + Create a new branch protection rule + """ + createBranchProtectionRule( + """ + Parameters for CreateBranchProtectionRule + """ + input: CreateBranchProtectionRuleInput! + ): CreateBranchProtectionRulePayload + + """ + Create a check run. + """ + createCheckRun( + """ + Parameters for CreateCheckRun + """ + input: CreateCheckRunInput! + ): CreateCheckRunPayload + + """ + Create a check suite + """ + createCheckSuite( + """ + Parameters for CreateCheckSuite + """ + input: CreateCheckSuiteInput! + ): CreateCheckSuitePayload + + """ + Appends a commit to the given branch as the authenticated user. + + This mutation creates a commit whose parent is the HEAD of the provided + branch and also updates that branch to point to the new commit. + It can be thought of as similar to `git commit`. + + ### Locating a Branch + + Commits are appended to a `branch` of type `Ref`. + This must refer to a git branch (i.e. the fully qualified path must + begin with `refs/heads/`, although including this prefix is optional. + + Callers may specify the `branch` to commit to either by its global node + ID or by passing both of `repositoryNameWithOwner` and `refName`. For + more details see the documentation for `CommittableBranch`. + + ### Describing Changes + + `fileChanges` are specified as a `FilesChanges` object describing + `FileAdditions` and `FileDeletions`. + + Please see the documentation for `FileChanges` for more information on + how to use this argument to describe any set of file changes. + + ### Authorship + + Similar to the web commit interface, this mutation does not support + specifying the author or committer of the commit and will not add + support for this in the future. + + A commit created by a successful execution of this mutation will be + authored by the owner of the credential which authenticates the API + request. The committer will be identical to that of commits authored + using the web interface. + + If you need full control over author and committer information, please + use the Git Database REST API instead. + + ### Commit Signing + + Commits made using this mutation are automatically signed by GitHub if + supported and will be marked as verified in the user interface. + """ + createCommitOnBranch( + """ + Parameters for CreateCommitOnBranch + """ + input: CreateCommitOnBranchInput! + ): CreateCommitOnBranchPayload + + """ + Creates a new deployment event. + """ + createDeployment( + """ + Parameters for CreateDeployment + """ + input: CreateDeploymentInput! + ): CreateDeploymentPayload + + """ + Create a deployment status. + """ + createDeploymentStatus( + """ + Parameters for CreateDeploymentStatus + """ + input: CreateDeploymentStatusInput! + ): CreateDeploymentStatusPayload + + """ + Create a discussion. + """ + createDiscussion( + """ + Parameters for CreateDiscussion + """ + input: CreateDiscussionInput! + ): CreateDiscussionPayload + + """ + Creates an organization as part of an enterprise account. A personal access + token used to create an organization is implicitly permitted to update the + organization it created, if the organization is part of an enterprise that has + SAML enabled or uses Enterprise Managed Users. If the organization is not part + of such an enterprise, and instead has SAML enabled for it individually, the + token will then require SAML authorization to continue working against that organization. + """ + createEnterpriseOrganization( + """ + Parameters for CreateEnterpriseOrganization + """ + input: CreateEnterpriseOrganizationInput! + ): CreateEnterpriseOrganizationPayload + + """ + Creates an environment or simply returns it if already exists. + """ + createEnvironment( + """ + Parameters for CreateEnvironment + """ + input: CreateEnvironmentInput! + ): CreateEnvironmentPayload + + """ + Creates a new IP allow list entry. + """ + createIpAllowListEntry( + """ + Parameters for CreateIpAllowListEntry + """ + input: CreateIpAllowListEntryInput! + ): CreateIpAllowListEntryPayload + + """ + Creates a new issue. + """ + createIssue( + """ + Parameters for CreateIssue + """ + input: CreateIssueInput! + ): CreateIssuePayload + + """ + Creates a new issue type + """ + createIssueType( + """ + Parameters for CreateIssueType + """ + input: CreateIssueTypeInput! + ): CreateIssueTypePayload + + """ + Creates a new label. + """ + createLabel( + """ + Parameters for CreateLabel + """ + input: CreateLabelInput! + ): CreateLabelPayload + + """ + Create a branch linked to an issue. + """ + createLinkedBranch( + """ + Parameters for CreateLinkedBranch + """ + input: CreateLinkedBranchInput! + ): CreateLinkedBranchPayload + + """ + Creates a GitHub Enterprise Importer (GEI) migration source. + """ + createMigrationSource( + """ + Parameters for CreateMigrationSource + """ + input: CreateMigrationSourceInput! + ): CreateMigrationSourcePayload + + """ + Creates a new project. + """ + createProject( + """ + Parameters for CreateProject + """ + input: CreateProjectInput! + ): CreateProjectPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Creates a new project. + """ + createProjectV2( + """ + Parameters for CreateProjectV2 + """ + input: CreateProjectV2Input! + ): CreateProjectV2Payload + + """ + Create a new project field. + """ + createProjectV2Field( + """ + Parameters for CreateProjectV2Field + """ + input: CreateProjectV2FieldInput! + ): CreateProjectV2FieldPayload + + """ + Creates a status update within a Project. + """ + createProjectV2StatusUpdate( + """ + Parameters for CreateProjectV2StatusUpdate + """ + input: CreateProjectV2StatusUpdateInput! + ): CreateProjectV2StatusUpdatePayload + + """ + Create a new pull request + """ + createPullRequest( + """ + Parameters for CreatePullRequest + """ + input: CreatePullRequestInput! + ): CreatePullRequestPayload + + """ + Create a new Git Ref. + """ + createRef( + """ + Parameters for CreateRef + """ + input: CreateRefInput! + ): CreateRefPayload + + """ + Create a new repository. + """ + createRepository( + """ + Parameters for CreateRepository + """ + input: CreateRepositoryInput! + ): CreateRepositoryPayload + + """ + Create a repository ruleset + """ + createRepositoryRuleset( + """ + Parameters for CreateRepositoryRuleset + """ + input: CreateRepositoryRulesetInput! + ): CreateRepositoryRulesetPayload + + """ + Create a GitHub Sponsors profile to allow others to sponsor you or your organization. + """ + createSponsorsListing( + """ + Parameters for CreateSponsorsListing + """ + input: CreateSponsorsListingInput! + ): CreateSponsorsListingPayload + + """ + Create a new payment tier for your GitHub Sponsors profile. + """ + createSponsorsTier( + """ + Parameters for CreateSponsorsTier + """ + input: CreateSponsorsTierInput! + ): CreateSponsorsTierPayload + + """ + Start a new sponsorship of a maintainer in GitHub Sponsors, or reactivate a past sponsorship. + """ + createSponsorship( + """ + Parameters for CreateSponsorship + """ + input: CreateSponsorshipInput! + ): CreateSponsorshipPayload + + """ + Make many sponsorships for different sponsorable users or organizations at + once. Can only sponsor those who have a public GitHub Sponsors profile. + """ + createSponsorships( + """ + Parameters for CreateSponsorships + """ + input: CreateSponsorshipsInput! + ): CreateSponsorshipsPayload + + """ + Creates a new team discussion. + """ + createTeamDiscussion( + """ + Parameters for CreateTeamDiscussion + """ + input: CreateTeamDiscussionInput! + ): CreateTeamDiscussionPayload + + """ + Creates a new team discussion comment. + """ + createTeamDiscussionComment( + """ + Parameters for CreateTeamDiscussionComment + """ + input: CreateTeamDiscussionCommentInput! + ): CreateTeamDiscussionCommentPayload + + """ + Creates a new user list. + """ + createUserList( + """ + Parameters for CreateUserList + """ + input: CreateUserListInput! + ): CreateUserListPayload + + """ + Rejects a suggested topic for the repository. + """ + declineTopicSuggestion( + """ + Parameters for DeclineTopicSuggestion + """ + input: DeclineTopicSuggestionInput! + ): DeclineTopicSuggestionPayload + + """ + Delete a branch protection rule + """ + deleteBranchProtectionRule( + """ + Parameters for DeleteBranchProtectionRule + """ + input: DeleteBranchProtectionRuleInput! + ): DeleteBranchProtectionRulePayload + + """ + Deletes a deployment. + """ + deleteDeployment( + """ + Parameters for DeleteDeployment + """ + input: DeleteDeploymentInput! + ): DeleteDeploymentPayload + + """ + Delete a discussion and all of its replies. + """ + deleteDiscussion( + """ + Parameters for DeleteDiscussion + """ + input: DeleteDiscussionInput! + ): DeleteDiscussionPayload + + """ + Delete a discussion comment. If it has replies, wipe it instead. + """ + deleteDiscussionComment( + """ + Parameters for DeleteDiscussionComment + """ + input: DeleteDiscussionCommentInput! + ): DeleteDiscussionCommentPayload + + """ + Deletes an environment + """ + deleteEnvironment( + """ + Parameters for DeleteEnvironment + """ + input: DeleteEnvironmentInput! + ): DeleteEnvironmentPayload + + """ + Deletes an IP allow list entry. + """ + deleteIpAllowListEntry( + """ + Parameters for DeleteIpAllowListEntry + """ + input: DeleteIpAllowListEntryInput! + ): DeleteIpAllowListEntryPayload + + """ + Deletes an Issue object. + """ + deleteIssue( + """ + Parameters for DeleteIssue + """ + input: DeleteIssueInput! + ): DeleteIssuePayload + + """ + Deletes an IssueComment object. + """ + deleteIssueComment( + """ + Parameters for DeleteIssueComment + """ + input: DeleteIssueCommentInput! + ): DeleteIssueCommentPayload + + """ + Delete an issue type + """ + deleteIssueType( + """ + Parameters for DeleteIssueType + """ + input: DeleteIssueTypeInput! + ): DeleteIssueTypePayload + + """ + Deletes a label. + """ + deleteLabel( + """ + Parameters for DeleteLabel + """ + input: DeleteLabelInput! + ): DeleteLabelPayload + + """ + Unlink a branch from an issue. + """ + deleteLinkedBranch( + """ + Parameters for DeleteLinkedBranch + """ + input: DeleteLinkedBranchInput! + ): DeleteLinkedBranchPayload + + """ + Delete a package version. + """ + deletePackageVersion( + """ + Parameters for DeletePackageVersion + """ + input: DeletePackageVersionInput! + ): DeletePackageVersionPayload + + """ + Deletes a project. + """ + deleteProject( + """ + Parameters for DeleteProject + """ + input: DeleteProjectInput! + ): DeleteProjectPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Deletes a project card. + """ + deleteProjectCard( + """ + Parameters for DeleteProjectCard + """ + input: DeleteProjectCardInput! + ): DeleteProjectCardPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Deletes a project column. + """ + deleteProjectColumn( + """ + Parameters for DeleteProjectColumn + """ + input: DeleteProjectColumnInput! + ): DeleteProjectColumnPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Delete a project. + """ + deleteProjectV2( + """ + Parameters for DeleteProjectV2 + """ + input: DeleteProjectV2Input! + ): DeleteProjectV2Payload + + """ + Delete a project field. + """ + deleteProjectV2Field( + """ + Parameters for DeleteProjectV2Field + """ + input: DeleteProjectV2FieldInput! + ): DeleteProjectV2FieldPayload + + """ + Deletes an item from a Project. + """ + deleteProjectV2Item( + """ + Parameters for DeleteProjectV2Item + """ + input: DeleteProjectV2ItemInput! + ): DeleteProjectV2ItemPayload + + """ + Deletes a project status update. + """ + deleteProjectV2StatusUpdate( + """ + Parameters for DeleteProjectV2StatusUpdate + """ + input: DeleteProjectV2StatusUpdateInput! + ): DeleteProjectV2StatusUpdatePayload + + """ + Deletes a project workflow. + """ + deleteProjectV2Workflow( + """ + Parameters for DeleteProjectV2Workflow + """ + input: DeleteProjectV2WorkflowInput! + ): DeleteProjectV2WorkflowPayload + + """ + Deletes a pull request review. + """ + deletePullRequestReview( + """ + Parameters for DeletePullRequestReview + """ + input: DeletePullRequestReviewInput! + ): DeletePullRequestReviewPayload + + """ + Deletes a pull request review comment. + """ + deletePullRequestReviewComment( + """ + Parameters for DeletePullRequestReviewComment + """ + input: DeletePullRequestReviewCommentInput! + ): DeletePullRequestReviewCommentPayload + + """ + Delete a Git Ref. + """ + deleteRef( + """ + Parameters for DeleteRef + """ + input: DeleteRefInput! + ): DeleteRefPayload + + """ + Delete a repository ruleset + """ + deleteRepositoryRuleset( + """ + Parameters for DeleteRepositoryRuleset + """ + input: DeleteRepositoryRulesetInput! + ): DeleteRepositoryRulesetPayload + + """ + Deletes a team discussion. + """ + deleteTeamDiscussion( + """ + Parameters for DeleteTeamDiscussion + """ + input: DeleteTeamDiscussionInput! + ): DeleteTeamDiscussionPayload + + """ + Deletes a team discussion comment. + """ + deleteTeamDiscussionComment( + """ + Parameters for DeleteTeamDiscussionComment + """ + input: DeleteTeamDiscussionCommentInput! + ): DeleteTeamDiscussionCommentPayload + + """ + Deletes a user list. + """ + deleteUserList( + """ + Parameters for DeleteUserList + """ + input: DeleteUserListInput! + ): DeleteUserListPayload + + """ + Deletes a verifiable domain. + """ + deleteVerifiableDomain( + """ + Parameters for DeleteVerifiableDomain + """ + input: DeleteVerifiableDomainInput! + ): DeleteVerifiableDomainPayload + + """ + Remove a pull request from the merge queue. + """ + dequeuePullRequest( + """ + Parameters for DequeuePullRequest + """ + input: DequeuePullRequestInput! + ): DequeuePullRequestPayload + + """ + Disable auto merge on the given pull request + """ + disablePullRequestAutoMerge( + """ + Parameters for DisablePullRequestAutoMerge + """ + input: DisablePullRequestAutoMergeInput! + ): DisablePullRequestAutoMergePayload + + """ + Dismisses an approved or rejected pull request review. + """ + dismissPullRequestReview( + """ + Parameters for DismissPullRequestReview + """ + input: DismissPullRequestReviewInput! + ): DismissPullRequestReviewPayload + + """ + Dismisses the Dependabot alert. + """ + dismissRepositoryVulnerabilityAlert( + """ + Parameters for DismissRepositoryVulnerabilityAlert + """ + input: DismissRepositoryVulnerabilityAlertInput! + ): DismissRepositoryVulnerabilityAlertPayload + + """ + Enable the default auto-merge on a pull request. + """ + enablePullRequestAutoMerge( + """ + Parameters for EnablePullRequestAutoMerge + """ + input: EnablePullRequestAutoMergeInput! + ): EnablePullRequestAutoMergePayload + + """ + Add a pull request to the merge queue. + """ + enqueuePullRequest( + """ + Parameters for EnqueuePullRequest + """ + input: EnqueuePullRequestInput! + ): EnqueuePullRequestPayload + + """ + Follow an organization. + """ + followOrganization( + """ + Parameters for FollowOrganization + """ + input: FollowOrganizationInput! + ): FollowOrganizationPayload + + """ + Follow a user. + """ + followUser( + """ + Parameters for FollowUser + """ + input: FollowUserInput! + ): FollowUserPayload + + """ + Grant the migrator role to a user for all organizations under an enterprise account. + """ + grantEnterpriseOrganizationsMigratorRole( + """ + Parameters for GrantEnterpriseOrganizationsMigratorRole + """ + input: GrantEnterpriseOrganizationsMigratorRoleInput! + ): GrantEnterpriseOrganizationsMigratorRolePayload + + """ + Grant the migrator role to a user or a team. + """ + grantMigratorRole( + """ + Parameters for GrantMigratorRole + """ + input: GrantMigratorRoleInput! + ): GrantMigratorRolePayload + + """ + Creates a new project by importing columns and a list of issues/PRs. + """ + importProject( + """ + Parameters for ImportProject + """ + input: ImportProjectInput! + ): ImportProjectPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Invite someone to become an administrator of the enterprise. + """ + inviteEnterpriseAdmin( + """ + Parameters for InviteEnterpriseAdmin + """ + input: InviteEnterpriseAdminInput! + ): InviteEnterpriseAdminPayload + + """ + Invite someone to become an unaffiliated member of the enterprise. + """ + inviteEnterpriseMember( + """ + Parameters for InviteEnterpriseMember + """ + input: InviteEnterpriseMemberInput! + ): InviteEnterpriseMemberPayload + + """ + Links a project to a repository. + """ + linkProjectV2ToRepository( + """ + Parameters for LinkProjectV2ToRepository + """ + input: LinkProjectV2ToRepositoryInput! + ): LinkProjectV2ToRepositoryPayload + + """ + Links a project to a team. + """ + linkProjectV2ToTeam( + """ + Parameters for LinkProjectV2ToTeam + """ + input: LinkProjectV2ToTeamInput! + ): LinkProjectV2ToTeamPayload + + """ + Creates a repository link for a project. + """ + linkRepositoryToProject( + """ + Parameters for LinkRepositoryToProject + """ + input: LinkRepositoryToProjectInput! + ): LinkRepositoryToProjectPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Lock a lockable object + """ + lockLockable( + """ + Parameters for LockLockable + """ + input: LockLockableInput! + ): LockLockablePayload + + """ + Mark a discussion comment as the chosen answer for discussions in an answerable category. + """ + markDiscussionCommentAsAnswer( + """ + Parameters for MarkDiscussionCommentAsAnswer + """ + input: MarkDiscussionCommentAsAnswerInput! + ): MarkDiscussionCommentAsAnswerPayload + + """ + Mark a pull request file as viewed + """ + markFileAsViewed( + """ + Parameters for MarkFileAsViewed + """ + input: MarkFileAsViewedInput! + ): MarkFileAsViewedPayload + + """ + Mark a project as a template. Note that only projects which are owned by an Organization can be marked as a template. + """ + markProjectV2AsTemplate( + """ + Parameters for MarkProjectV2AsTemplate + """ + input: MarkProjectV2AsTemplateInput! + ): MarkProjectV2AsTemplatePayload + + """ + Marks a pull request ready for review. + """ + markPullRequestReadyForReview( + """ + Parameters for MarkPullRequestReadyForReview + """ + input: MarkPullRequestReadyForReviewInput! + ): MarkPullRequestReadyForReviewPayload + + """ + Merge a head into a branch. + """ + mergeBranch( + """ + Parameters for MergeBranch + """ + input: MergeBranchInput! + ): MergeBranchPayload + + """ + Merge a pull request. + """ + mergePullRequest( + """ + Parameters for MergePullRequest + """ + input: MergePullRequestInput! + ): MergePullRequestPayload + + """ + Minimizes a comment on an Issue, Commit, Pull Request, or Gist + """ + minimizeComment( + """ + Parameters for MinimizeComment + """ + input: MinimizeCommentInput! + ): MinimizeCommentPayload + + """ + Moves a project card to another place. + """ + moveProjectCard( + """ + Parameters for MoveProjectCard + """ + input: MoveProjectCardInput! + ): MoveProjectCardPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Moves a project column to another place. + """ + moveProjectColumn( + """ + Parameters for MoveProjectColumn + """ + input: MoveProjectColumnInput! + ): MoveProjectColumnPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Pin an environment to a repository + """ + pinEnvironment( + """ + Parameters for PinEnvironment + """ + input: PinEnvironmentInput! + ): PinEnvironmentPayload + + """ + Pin an issue to a repository + """ + pinIssue( + """ + Parameters for PinIssue + """ + input: PinIssueInput! + ): PinIssuePayload + + """ + Publish an existing sponsorship tier that is currently still a draft to a GitHub Sponsors profile. + """ + publishSponsorsTier( + """ + Parameters for PublishSponsorsTier + """ + input: PublishSponsorsTierInput! + ): PublishSponsorsTierPayload + + """ + Regenerates the identity provider recovery codes for an enterprise + """ + regenerateEnterpriseIdentityProviderRecoveryCodes( + """ + Parameters for RegenerateEnterpriseIdentityProviderRecoveryCodes + """ + input: RegenerateEnterpriseIdentityProviderRecoveryCodesInput! + ): RegenerateEnterpriseIdentityProviderRecoveryCodesPayload + + """ + Regenerates a verifiable domain's verification token. + """ + regenerateVerifiableDomainToken( + """ + Parameters for RegenerateVerifiableDomainToken + """ + input: RegenerateVerifiableDomainTokenInput! + ): RegenerateVerifiableDomainTokenPayload + + """ + Reject all pending deployments under one or more environments + """ + rejectDeployments( + """ + Parameters for RejectDeployments + """ + input: RejectDeploymentsInput! + ): RejectDeploymentsPayload + + """ + Removes assignees from an assignable object. + """ + removeAssigneesFromAssignable( + """ + Parameters for RemoveAssigneesFromAssignable + """ + input: RemoveAssigneesFromAssignableInput! + ): RemoveAssigneesFromAssignablePayload + + """ + Removes an administrator from the enterprise. + """ + removeEnterpriseAdmin( + """ + Parameters for RemoveEnterpriseAdmin + """ + input: RemoveEnterpriseAdminInput! + ): RemoveEnterpriseAdminPayload + + """ + Removes the identity provider from an enterprise. Owners of enterprises both + with and without Enterprise Managed Users may use this mutation. + """ + removeEnterpriseIdentityProvider( + """ + Parameters for RemoveEnterpriseIdentityProvider + """ + input: RemoveEnterpriseIdentityProviderInput! + ): RemoveEnterpriseIdentityProviderPayload + + """ + Removes a user from all organizations within the enterprise + """ + removeEnterpriseMember( + """ + Parameters for RemoveEnterpriseMember + """ + input: RemoveEnterpriseMemberInput! + ): RemoveEnterpriseMemberPayload + + """ + Removes an organization from the enterprise + """ + removeEnterpriseOrganization( + """ + Parameters for RemoveEnterpriseOrganization + """ + input: RemoveEnterpriseOrganizationInput! + ): RemoveEnterpriseOrganizationPayload + + """ + Removes a support entitlement from an enterprise member. + """ + removeEnterpriseSupportEntitlement( + """ + Parameters for RemoveEnterpriseSupportEntitlement + """ + input: RemoveEnterpriseSupportEntitlementInput! + ): RemoveEnterpriseSupportEntitlementPayload + + """ + Removes labels from a Labelable object. + """ + removeLabelsFromLabelable( + """ + Parameters for RemoveLabelsFromLabelable + """ + input: RemoveLabelsFromLabelableInput! + ): RemoveLabelsFromLabelablePayload + + """ + Removes outside collaborator from all repositories in an organization. + """ + removeOutsideCollaborator( + """ + Parameters for RemoveOutsideCollaborator + """ + input: RemoveOutsideCollaboratorInput! + ): RemoveOutsideCollaboratorPayload + + """ + Removes a reaction from a subject. + """ + removeReaction( + """ + Parameters for RemoveReaction + """ + input: RemoveReactionInput! + ): RemoveReactionPayload + + """ + Removes a star from a Starrable. + """ + removeStar( + """ + Parameters for RemoveStar + """ + input: RemoveStarInput! + ): RemoveStarPayload + + """ + Removes a sub-issue from a given issue + """ + removeSubIssue( + """ + Parameters for RemoveSubIssue + """ + input: RemoveSubIssueInput! + ): RemoveSubIssuePayload + + """ + Remove an upvote to a discussion or discussion comment. + """ + removeUpvote( + """ + Parameters for RemoveUpvote + """ + input: RemoveUpvoteInput! + ): RemoveUpvotePayload + + """ + Reopen a discussion. + """ + reopenDiscussion( + """ + Parameters for ReopenDiscussion + """ + input: ReopenDiscussionInput! + ): ReopenDiscussionPayload + + """ + Reopen a issue. + """ + reopenIssue( + """ + Parameters for ReopenIssue + """ + input: ReopenIssueInput! + ): ReopenIssuePayload + + """ + Reopen a pull request. + """ + reopenPullRequest( + """ + Parameters for ReopenPullRequest + """ + input: ReopenPullRequestInput! + ): ReopenPullRequestPayload + + """ + Reorder a pinned repository environment + """ + reorderEnvironment( + """ + Parameters for ReorderEnvironment + """ + input: ReorderEnvironmentInput! + ): ReorderEnvironmentPayload + + """ + Replaces all actors for assignable object. + """ + replaceActorsForAssignable( + """ + Parameters for ReplaceActorsForAssignable + """ + input: ReplaceActorsForAssignableInput! + ): ReplaceActorsForAssignablePayload + + """ + Reprioritizes a sub-issue to a different position in the parent list. + """ + reprioritizeSubIssue( + """ + Parameters for ReprioritizeSubIssue + """ + input: ReprioritizeSubIssueInput! + ): ReprioritizeSubIssuePayload + + """ + Set review requests on a pull request. + """ + requestReviews( + """ + Parameters for RequestReviews + """ + input: RequestReviewsInput! + ): RequestReviewsPayload + + """ + Rerequests an existing check suite. + """ + rerequestCheckSuite( + """ + Parameters for RerequestCheckSuite + """ + input: RerequestCheckSuiteInput! + ): RerequestCheckSuitePayload + + """ + Marks a review thread as resolved. + """ + resolveReviewThread( + """ + Parameters for ResolveReviewThread + """ + input: ResolveReviewThreadInput! + ): ResolveReviewThreadPayload + + """ + Retire a published payment tier from your GitHub Sponsors profile so it cannot be used to start new sponsorships. + """ + retireSponsorsTier( + """ + Parameters for RetireSponsorsTier + """ + input: RetireSponsorsTierInput! + ): RetireSponsorsTierPayload + + """ + Create a pull request that reverts the changes from a merged pull request. + """ + revertPullRequest( + """ + Parameters for RevertPullRequest + """ + input: RevertPullRequestInput! + ): RevertPullRequestPayload + + """ + Revoke the migrator role to a user for all organizations under an enterprise account. + """ + revokeEnterpriseOrganizationsMigratorRole( + """ + Parameters for RevokeEnterpriseOrganizationsMigratorRole + """ + input: RevokeEnterpriseOrganizationsMigratorRoleInput! + ): RevokeEnterpriseOrganizationsMigratorRolePayload + + """ + Revoke the migrator role from a user or a team. + """ + revokeMigratorRole( + """ + Parameters for RevokeMigratorRole + """ + input: RevokeMigratorRoleInput! + ): RevokeMigratorRolePayload + + """ + Creates or updates the identity provider for an enterprise. + """ + setEnterpriseIdentityProvider( + """ + Parameters for SetEnterpriseIdentityProvider + """ + input: SetEnterpriseIdentityProviderInput! + ): SetEnterpriseIdentityProviderPayload + + """ + Set an organization level interaction limit for an organization's public repositories. + """ + setOrganizationInteractionLimit( + """ + Parameters for SetOrganizationInteractionLimit + """ + input: SetOrganizationInteractionLimitInput! + ): SetOrganizationInteractionLimitPayload + + """ + Sets an interaction limit setting for a repository. + """ + setRepositoryInteractionLimit( + """ + Parameters for SetRepositoryInteractionLimit + """ + input: SetRepositoryInteractionLimitInput! + ): SetRepositoryInteractionLimitPayload + + """ + Set a user level interaction limit for an user's public repositories. + """ + setUserInteractionLimit( + """ + Parameters for SetUserInteractionLimit + """ + input: SetUserInteractionLimitInput! + ): SetUserInteractionLimitPayload + + """ + Starts a GitHub Enterprise Importer organization migration. + """ + startOrganizationMigration( + """ + Parameters for StartOrganizationMigration + """ + input: StartOrganizationMigrationInput! + ): StartOrganizationMigrationPayload + + """ + Starts a GitHub Enterprise Importer (GEI) repository migration. + """ + startRepositoryMigration( + """ + Parameters for StartRepositoryMigration + """ + input: StartRepositoryMigrationInput! + ): StartRepositoryMigrationPayload + + """ + Submits a pending pull request review. + """ + submitPullRequestReview( + """ + Parameters for SubmitPullRequestReview + """ + input: SubmitPullRequestReviewInput! + ): SubmitPullRequestReviewPayload + + """ + Transfer an organization from one enterprise to another enterprise. + """ + transferEnterpriseOrganization( + """ + Parameters for TransferEnterpriseOrganization + """ + input: TransferEnterpriseOrganizationInput! + ): TransferEnterpriseOrganizationPayload + + """ + Transfer an issue to a different repository + """ + transferIssue( + """ + Parameters for TransferIssue + """ + input: TransferIssueInput! + ): TransferIssuePayload + + """ + Unarchives a ProjectV2Item + """ + unarchiveProjectV2Item( + """ + Parameters for UnarchiveProjectV2Item + """ + input: UnarchiveProjectV2ItemInput! + ): UnarchiveProjectV2ItemPayload + + """ + Unarchives a repository. + """ + unarchiveRepository( + """ + Parameters for UnarchiveRepository + """ + input: UnarchiveRepositoryInput! + ): UnarchiveRepositoryPayload + + """ + Unfollow an organization. + """ + unfollowOrganization( + """ + Parameters for UnfollowOrganization + """ + input: UnfollowOrganizationInput! + ): UnfollowOrganizationPayload + + """ + Unfollow a user. + """ + unfollowUser( + """ + Parameters for UnfollowUser + """ + input: UnfollowUserInput! + ): UnfollowUserPayload + + """ + Unlinks a project from a repository. + """ + unlinkProjectV2FromRepository( + """ + Parameters for UnlinkProjectV2FromRepository + """ + input: UnlinkProjectV2FromRepositoryInput! + ): UnlinkProjectV2FromRepositoryPayload + + """ + Unlinks a project to a team. + """ + unlinkProjectV2FromTeam( + """ + Parameters for UnlinkProjectV2FromTeam + """ + input: UnlinkProjectV2FromTeamInput! + ): UnlinkProjectV2FromTeamPayload + + """ + Deletes a repository link from a project. + """ + unlinkRepositoryFromProject( + """ + Parameters for UnlinkRepositoryFromProject + """ + input: UnlinkRepositoryFromProjectInput! + ): UnlinkRepositoryFromProjectPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Unlock a lockable object + """ + unlockLockable( + """ + Parameters for UnlockLockable + """ + input: UnlockLockableInput! + ): UnlockLockablePayload + + """ + Unmark a discussion comment as the chosen answer for discussions in an answerable category. + """ + unmarkDiscussionCommentAsAnswer( + """ + Parameters for UnmarkDiscussionCommentAsAnswer + """ + input: UnmarkDiscussionCommentAsAnswerInput! + ): UnmarkDiscussionCommentAsAnswerPayload + + """ + Unmark a pull request file as viewed + """ + unmarkFileAsViewed( + """ + Parameters for UnmarkFileAsViewed + """ + input: UnmarkFileAsViewedInput! + ): UnmarkFileAsViewedPayload + + """ + Unmark an issue as a duplicate of another issue. + """ + unmarkIssueAsDuplicate( + """ + Parameters for UnmarkIssueAsDuplicate + """ + input: UnmarkIssueAsDuplicateInput! + ): UnmarkIssueAsDuplicatePayload + + """ + Unmark a project as a template. + """ + unmarkProjectV2AsTemplate( + """ + Parameters for UnmarkProjectV2AsTemplate + """ + input: UnmarkProjectV2AsTemplateInput! + ): UnmarkProjectV2AsTemplatePayload + + """ + Unminimizes a comment on an Issue, Commit, Pull Request, or Gist + """ + unminimizeComment( + """ + Parameters for UnminimizeComment + """ + input: UnminimizeCommentInput! + ): UnminimizeCommentPayload + + """ + Unpin a pinned issue from a repository + """ + unpinIssue( + """ + Parameters for UnpinIssue + """ + input: UnpinIssueInput! + ): UnpinIssuePayload + + """ + Marks a review thread as unresolved. + """ + unresolveReviewThread( + """ + Parameters for UnresolveReviewThread + """ + input: UnresolveReviewThreadInput! + ): UnresolveReviewThreadPayload + + """ + Update a branch protection rule + """ + updateBranchProtectionRule( + """ + Parameters for UpdateBranchProtectionRule + """ + input: UpdateBranchProtectionRuleInput! + ): UpdateBranchProtectionRulePayload + + """ + Update a check run + """ + updateCheckRun( + """ + Parameters for UpdateCheckRun + """ + input: UpdateCheckRunInput! + ): UpdateCheckRunPayload + + """ + Modifies the settings of an existing check suite + """ + updateCheckSuitePreferences( + """ + Parameters for UpdateCheckSuitePreferences + """ + input: UpdateCheckSuitePreferencesInput! + ): UpdateCheckSuitePreferencesPayload + + """ + Update a discussion + """ + updateDiscussion( + """ + Parameters for UpdateDiscussion + """ + input: UpdateDiscussionInput! + ): UpdateDiscussionPayload + + """ + Update the contents of a comment on a Discussion + """ + updateDiscussionComment( + """ + Parameters for UpdateDiscussionComment + """ + input: UpdateDiscussionCommentInput! + ): UpdateDiscussionCommentPayload + + """ + Updates the role of an enterprise administrator. + """ + updateEnterpriseAdministratorRole( + """ + Parameters for UpdateEnterpriseAdministratorRole + """ + input: UpdateEnterpriseAdministratorRoleInput! + ): UpdateEnterpriseAdministratorRolePayload + + """ + Sets whether private repository forks are enabled for an enterprise. + """ + updateEnterpriseAllowPrivateRepositoryForkingSetting( + """ + Parameters for UpdateEnterpriseAllowPrivateRepositoryForkingSetting + """ + input: UpdateEnterpriseAllowPrivateRepositoryForkingSettingInput! + ): UpdateEnterpriseAllowPrivateRepositoryForkingSettingPayload + + """ + Sets the base repository permission for organizations in an enterprise. + """ + updateEnterpriseDefaultRepositoryPermissionSetting( + """ + Parameters for UpdateEnterpriseDefaultRepositoryPermissionSetting + """ + input: UpdateEnterpriseDefaultRepositoryPermissionSettingInput! + ): UpdateEnterpriseDefaultRepositoryPermissionSettingPayload + + """ + Sets whether deploy keys are allowed to be created and used for an enterprise. + """ + updateEnterpriseDeployKeySetting( + """ + Parameters for UpdateEnterpriseDeployKeySetting + """ + input: UpdateEnterpriseDeployKeySettingInput! + ): UpdateEnterpriseDeployKeySettingPayload + + """ + Sets whether organization members with admin permissions on a repository can change repository visibility. + """ + updateEnterpriseMembersCanChangeRepositoryVisibilitySetting( + """ + Parameters for UpdateEnterpriseMembersCanChangeRepositoryVisibilitySetting + """ + input: UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingInput! + ): UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingPayload + + """ + Sets the members can create repositories setting for an enterprise. + """ + updateEnterpriseMembersCanCreateRepositoriesSetting( + """ + Parameters for UpdateEnterpriseMembersCanCreateRepositoriesSetting + """ + input: UpdateEnterpriseMembersCanCreateRepositoriesSettingInput! + ): UpdateEnterpriseMembersCanCreateRepositoriesSettingPayload + + """ + Sets the members can delete issues setting for an enterprise. + """ + updateEnterpriseMembersCanDeleteIssuesSetting( + """ + Parameters for UpdateEnterpriseMembersCanDeleteIssuesSetting + """ + input: UpdateEnterpriseMembersCanDeleteIssuesSettingInput! + ): UpdateEnterpriseMembersCanDeleteIssuesSettingPayload + + """ + Sets the members can delete repositories setting for an enterprise. + """ + updateEnterpriseMembersCanDeleteRepositoriesSetting( + """ + Parameters for UpdateEnterpriseMembersCanDeleteRepositoriesSetting + """ + input: UpdateEnterpriseMembersCanDeleteRepositoriesSettingInput! + ): UpdateEnterpriseMembersCanDeleteRepositoriesSettingPayload + + """ + Sets whether members can invite collaborators are enabled for an enterprise. + """ + updateEnterpriseMembersCanInviteCollaboratorsSetting( + """ + Parameters for UpdateEnterpriseMembersCanInviteCollaboratorsSetting + """ + input: UpdateEnterpriseMembersCanInviteCollaboratorsSettingInput! + ): UpdateEnterpriseMembersCanInviteCollaboratorsSettingPayload + + """ + Sets whether or not an organization owner can make purchases. + """ + updateEnterpriseMembersCanMakePurchasesSetting( + """ + Parameters for UpdateEnterpriseMembersCanMakePurchasesSetting + """ + input: UpdateEnterpriseMembersCanMakePurchasesSettingInput! + ): UpdateEnterpriseMembersCanMakePurchasesSettingPayload + + """ + Sets the members can update protected branches setting for an enterprise. + """ + updateEnterpriseMembersCanUpdateProtectedBranchesSetting( + """ + Parameters for UpdateEnterpriseMembersCanUpdateProtectedBranchesSetting + """ + input: UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingInput! + ): UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingPayload + + """ + Sets the members can view dependency insights for an enterprise. + """ + updateEnterpriseMembersCanViewDependencyInsightsSetting( + """ + Parameters for UpdateEnterpriseMembersCanViewDependencyInsightsSetting + """ + input: UpdateEnterpriseMembersCanViewDependencyInsightsSettingInput! + ): UpdateEnterpriseMembersCanViewDependencyInsightsSettingPayload + + """ + Sets whether organization projects are enabled for an enterprise. + """ + updateEnterpriseOrganizationProjectsSetting( + """ + Parameters for UpdateEnterpriseOrganizationProjectsSetting + """ + input: UpdateEnterpriseOrganizationProjectsSettingInput! + ): UpdateEnterpriseOrganizationProjectsSettingPayload + + """ + Updates the role of an enterprise owner with an organization. + """ + updateEnterpriseOwnerOrganizationRole( + """ + Parameters for UpdateEnterpriseOwnerOrganizationRole + """ + input: UpdateEnterpriseOwnerOrganizationRoleInput! + ): UpdateEnterpriseOwnerOrganizationRolePayload + + """ + Updates an enterprise's profile. + """ + updateEnterpriseProfile( + """ + Parameters for UpdateEnterpriseProfile + """ + input: UpdateEnterpriseProfileInput! + ): UpdateEnterpriseProfilePayload + + """ + Sets whether repository projects are enabled for a enterprise. + """ + updateEnterpriseRepositoryProjectsSetting( + """ + Parameters for UpdateEnterpriseRepositoryProjectsSetting + """ + input: UpdateEnterpriseRepositoryProjectsSettingInput! + ): UpdateEnterpriseRepositoryProjectsSettingPayload + + """ + Sets whether team discussions are enabled for an enterprise. + """ + updateEnterpriseTeamDiscussionsSetting( + """ + Parameters for UpdateEnterpriseTeamDiscussionsSetting + """ + input: UpdateEnterpriseTeamDiscussionsSettingInput! + ): UpdateEnterpriseTeamDiscussionsSettingPayload + + """ + Sets the two-factor authentication methods that users of an enterprise may not use. + """ + updateEnterpriseTwoFactorAuthenticationDisallowedMethodsSetting( + """ + Parameters for UpdateEnterpriseTwoFactorAuthenticationDisallowedMethodsSetting + """ + input: UpdateEnterpriseTwoFactorAuthenticationDisallowedMethodsSettingInput! + ): UpdateEnterpriseTwoFactorAuthenticationDisallowedMethodsSettingPayload + + """ + Sets whether two factor authentication is required for all users in an enterprise. + """ + updateEnterpriseTwoFactorAuthenticationRequiredSetting( + """ + Parameters for UpdateEnterpriseTwoFactorAuthenticationRequiredSetting + """ + input: UpdateEnterpriseTwoFactorAuthenticationRequiredSettingInput! + ): UpdateEnterpriseTwoFactorAuthenticationRequiredSettingPayload + + """ + Updates an environment. + """ + updateEnvironment( + """ + Parameters for UpdateEnvironment + """ + input: UpdateEnvironmentInput! + ): UpdateEnvironmentPayload + + """ + Sets whether an IP allow list is enabled on an owner. + """ + updateIpAllowListEnabledSetting( + """ + Parameters for UpdateIpAllowListEnabledSetting + """ + input: UpdateIpAllowListEnabledSettingInput! + ): UpdateIpAllowListEnabledSettingPayload + + """ + Updates an IP allow list entry. + """ + updateIpAllowListEntry( + """ + Parameters for UpdateIpAllowListEntry + """ + input: UpdateIpAllowListEntryInput! + ): UpdateIpAllowListEntryPayload + + """ + Sets whether IP allow list configuration for installed GitHub Apps is enabled on an owner. + """ + updateIpAllowListForInstalledAppsEnabledSetting( + """ + Parameters for UpdateIpAllowListForInstalledAppsEnabledSetting + """ + input: UpdateIpAllowListForInstalledAppsEnabledSettingInput! + ): UpdateIpAllowListForInstalledAppsEnabledSettingPayload + + """ + Updates an Issue. + """ + updateIssue( + """ + Parameters for UpdateIssue + """ + input: UpdateIssueInput! + ): UpdateIssuePayload + + """ + Updates an IssueComment object. + """ + updateIssueComment( + """ + Parameters for UpdateIssueComment + """ + input: UpdateIssueCommentInput! + ): UpdateIssueCommentPayload + + """ + Updates the issue type on an issue + """ + updateIssueIssueType( + """ + Parameters for UpdateIssueIssueType + """ + input: UpdateIssueIssueTypeInput! + ): UpdateIssueIssueTypePayload + + """ + Update an issue type + """ + updateIssueType( + """ + Parameters for UpdateIssueType + """ + input: UpdateIssueTypeInput! + ): UpdateIssueTypePayload + + """ + Updates an existing label. + """ + updateLabel( + """ + Parameters for UpdateLabel + """ + input: UpdateLabelInput! + ): UpdateLabelPayload + + """ + Update the setting to restrict notifications to only verified or approved domains available to an owner. + """ + updateNotificationRestrictionSetting( + """ + Parameters for UpdateNotificationRestrictionSetting + """ + input: UpdateNotificationRestrictionSettingInput! + ): UpdateNotificationRestrictionSettingPayload + + """ + Sets whether private repository forks are enabled for an organization. + """ + updateOrganizationAllowPrivateRepositoryForkingSetting( + """ + Parameters for UpdateOrganizationAllowPrivateRepositoryForkingSetting + """ + input: UpdateOrganizationAllowPrivateRepositoryForkingSettingInput! + ): UpdateOrganizationAllowPrivateRepositoryForkingSettingPayload + + """ + Sets whether contributors are required to sign off on web-based commits for repositories in an organization. + """ + updateOrganizationWebCommitSignoffSetting( + """ + Parameters for UpdateOrganizationWebCommitSignoffSetting + """ + input: UpdateOrganizationWebCommitSignoffSettingInput! + ): UpdateOrganizationWebCommitSignoffSettingPayload + + """ + Toggle the setting for your GitHub Sponsors profile that allows other GitHub + accounts to sponsor you on GitHub while paying for the sponsorship on Patreon. + Only applicable when you have a GitHub Sponsors profile and have connected + your GitHub account with Patreon. + """ + updatePatreonSponsorability( + """ + Parameters for UpdatePatreonSponsorability + """ + input: UpdatePatreonSponsorabilityInput! + ): UpdatePatreonSponsorabilityPayload + + """ + Updates an existing project. + """ + updateProject( + """ + Parameters for UpdateProject + """ + input: UpdateProjectInput! + ): UpdateProjectPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Updates an existing project card. + """ + updateProjectCard( + """ + Parameters for UpdateProjectCard + """ + input: UpdateProjectCardInput! + ): UpdateProjectCardPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Updates an existing project column. + """ + updateProjectColumn( + """ + Parameters for UpdateProjectColumn + """ + input: UpdateProjectColumnInput! + ): UpdateProjectColumnPayload + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Updates an existing project. + """ + updateProjectV2( + """ + Parameters for UpdateProjectV2 + """ + input: UpdateProjectV2Input! + ): UpdateProjectV2Payload + + """ + Update the collaborators on a team or a project + """ + updateProjectV2Collaborators( + """ + Parameters for UpdateProjectV2Collaborators + """ + input: UpdateProjectV2CollaboratorsInput! + ): UpdateProjectV2CollaboratorsPayload + + """ + Updates a draft issue within a Project. + """ + updateProjectV2DraftIssue( + """ + Parameters for UpdateProjectV2DraftIssue + """ + input: UpdateProjectV2DraftIssueInput! + ): UpdateProjectV2DraftIssuePayload + + """ + Update a project field. + """ + updateProjectV2Field( + """ + Parameters for UpdateProjectV2Field + """ + input: UpdateProjectV2FieldInput! + ): UpdateProjectV2FieldPayload + + """ + This mutation updates the value of a field for an item in a Project. Currently + only single-select, text, number, date, and iteration fields are supported. + """ + updateProjectV2ItemFieldValue( + """ + Parameters for UpdateProjectV2ItemFieldValue + """ + input: UpdateProjectV2ItemFieldValueInput! + ): UpdateProjectV2ItemFieldValuePayload + + """ + This mutation updates the position of the item in the project, where the position represents the priority of an item. + """ + updateProjectV2ItemPosition( + """ + Parameters for UpdateProjectV2ItemPosition + """ + input: UpdateProjectV2ItemPositionInput! + ): UpdateProjectV2ItemPositionPayload + + """ + Updates a status update within a Project. + """ + updateProjectV2StatusUpdate( + """ + Parameters for UpdateProjectV2StatusUpdate + """ + input: UpdateProjectV2StatusUpdateInput! + ): UpdateProjectV2StatusUpdatePayload + + """ + Update a pull request + """ + updatePullRequest( + """ + Parameters for UpdatePullRequest + """ + input: UpdatePullRequestInput! + ): UpdatePullRequestPayload + + """ + Merge or Rebase HEAD from upstream branch into pull request branch + """ + updatePullRequestBranch( + """ + Parameters for UpdatePullRequestBranch + """ + input: UpdatePullRequestBranchInput! + ): UpdatePullRequestBranchPayload + + """ + Updates the body of a pull request review. + """ + updatePullRequestReview( + """ + Parameters for UpdatePullRequestReview + """ + input: UpdatePullRequestReviewInput! + ): UpdatePullRequestReviewPayload + + """ + Updates a pull request review comment. + """ + updatePullRequestReviewComment( + """ + Parameters for UpdatePullRequestReviewComment + """ + input: UpdatePullRequestReviewCommentInput! + ): UpdatePullRequestReviewCommentPayload + + """ + Update a Git Ref. + """ + updateRef( + """ + Parameters for UpdateRef + """ + input: UpdateRefInput! + ): UpdateRefPayload + + """ + Creates, updates and/or deletes multiple refs in a repository. + + This mutation takes a list of `RefUpdate`s and performs these updates + on the repository. All updates are performed atomically, meaning that + if one of them is rejected, no other ref will be modified. + + `RefUpdate.beforeOid` specifies that the given reference needs to point + to the given value before performing any updates. A value of + `0000000000000000000000000000000000000000` can be used to verify that + the references should not exist. + + `RefUpdate.afterOid` specifies the value that the given reference + will point to after performing all updates. A value of + `0000000000000000000000000000000000000000` can be used to delete a + reference. + + If `RefUpdate.force` is set to `true`, a non-fast-forward updates + for the given reference will be allowed. + """ + updateRefs( + """ + Parameters for UpdateRefs + """ + input: UpdateRefsInput! + ): UpdateRefsPayload + + """ + Update information about a repository. + """ + updateRepository( + """ + Parameters for UpdateRepository + """ + input: UpdateRepositoryInput! + ): UpdateRepositoryPayload + + """ + Update a repository ruleset + """ + updateRepositoryRuleset( + """ + Parameters for UpdateRepositoryRuleset + """ + input: UpdateRepositoryRulesetInput! + ): UpdateRepositoryRulesetPayload + + """ + Sets whether contributors are required to sign off on web-based commits for a repository. + """ + updateRepositoryWebCommitSignoffSetting( + """ + Parameters for UpdateRepositoryWebCommitSignoffSetting + """ + input: UpdateRepositoryWebCommitSignoffSettingInput! + ): UpdateRepositoryWebCommitSignoffSettingPayload + + """ + Change visibility of your sponsorship and opt in or out of email updates from the maintainer. + """ + updateSponsorshipPreferences( + """ + Parameters for UpdateSponsorshipPreferences + """ + input: UpdateSponsorshipPreferencesInput! + ): UpdateSponsorshipPreferencesPayload + + """ + Updates the state for subscribable subjects. + """ + updateSubscription( + """ + Parameters for UpdateSubscription + """ + input: UpdateSubscriptionInput! + ): UpdateSubscriptionPayload + + """ + Updates a team discussion. + """ + updateTeamDiscussion( + """ + Parameters for UpdateTeamDiscussion + """ + input: UpdateTeamDiscussionInput! + ): UpdateTeamDiscussionPayload + + """ + Updates a discussion comment. + """ + updateTeamDiscussionComment( + """ + Parameters for UpdateTeamDiscussionComment + """ + input: UpdateTeamDiscussionCommentInput! + ): UpdateTeamDiscussionCommentPayload + + """ + Updates team review assignment. + """ + updateTeamReviewAssignment( + """ + Parameters for UpdateTeamReviewAssignment + """ + input: UpdateTeamReviewAssignmentInput! + ): UpdateTeamReviewAssignmentPayload + + """ + Update team repository. + """ + updateTeamsRepository( + """ + Parameters for UpdateTeamsRepository + """ + input: UpdateTeamsRepositoryInput! + ): UpdateTeamsRepositoryPayload + + """ + Replaces the repository's topics with the given topics. + """ + updateTopics( + """ + Parameters for UpdateTopics + """ + input: UpdateTopicsInput! + ): UpdateTopicsPayload + + """ + Updates an existing user list. + """ + updateUserList( + """ + Parameters for UpdateUserList + """ + input: UpdateUserListInput! + ): UpdateUserListPayload + + """ + Updates which of the viewer's lists an item belongs to + """ + updateUserListsForItem( + """ + Parameters for UpdateUserListsForItem + """ + input: UpdateUserListsForItemInput! + ): UpdateUserListsForItemPayload + + """ + Verify that a verifiable domain has the expected DNS record. + """ + verifyVerifiableDomain( + """ + Parameters for VerifyVerifiableDomain + """ + input: VerifyVerifiableDomainInput! + ): VerifyVerifiableDomainPayload +} + +""" +An object with an ID. +""" +interface Node { + """ + ID of the object. + """ + id: ID! +} + +""" +The possible values for the notification restriction setting. +""" +enum NotificationRestrictionSettingValue { + """ + The setting is disabled for the owner. + """ + DISABLED + + """ + The setting is enabled for the owner. + """ + ENABLED +} + +""" +An OIDC identity provider configured to provision identities for an enterprise. +Visible to enterprise owners or enterprise owners' personal access tokens +(classic) with read:enterprise or admin:enterprise scope. +""" +type OIDCProvider implements Node { + """ + The enterprise this identity provider belongs to. + """ + enterprise: Enterprise + + """ + ExternalIdentities provisioned by this identity provider. + """ + externalIdentities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter to external identities with the users login + """ + login: String + + """ + Filter to external identities with valid org membership only + """ + membersOnly: Boolean + + """ + Filter to external identities with the users userName/NameID attribute + """ + userName: String + ): ExternalIdentityConnection! + + """ + The Node ID of the OIDCProvider object + """ + id: ID! + + """ + The OIDC identity provider type + """ + providerType: OIDCProviderType! + + """ + The id of the tenant this provider is attached to + """ + tenantId: String! +} + +""" +The OIDC identity provider type +""" +enum OIDCProviderType { + """ + Azure Active Directory + """ + AAD +} + +""" +Metadata for an audit entry with action oauth_application.* +""" +interface OauthApplicationAuditEntryData { + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI +} + +""" +Audit log entry for a oauth_application.create event. +""" +type OauthApplicationCreateAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The application URL of the OAuth application. + """ + applicationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The callback URL of the OAuth application. + """ + callbackUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OauthApplicationCreateAuditEntry object + """ + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The rate limit of the OAuth application. + """ + rateLimit: Int + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The state of the OAuth application. + """ + state: OauthApplicationCreateAuditEntryState + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The state of an OAuth application when it was created. +""" +enum OauthApplicationCreateAuditEntryState { + """ + The OAuth application was active and allowed to have OAuth Accesses. + """ + ACTIVE + + """ + The OAuth application was in the process of being deleted. + """ + PENDING_DELETION + + """ + The OAuth application was suspended from generating OAuth Accesses due to abuse or security concerns. + """ + SUSPENDED +} + +""" +The corresponding operation type for the action +""" +enum OperationType { + """ + An existing resource was accessed + """ + ACCESS + + """ + A resource performed an authentication event + """ + AUTHENTICATION + + """ + A new resource was created + """ + CREATE + + """ + An existing resource was modified + """ + MODIFY + + """ + An existing resource was removed + """ + REMOVE + + """ + An existing resource was restored + """ + RESTORE + + """ + An existing resource was transferred between multiple resources + """ + TRANSFER +} + +""" +Possible directions in which to order a list of items when provided an `orderBy` argument. +""" +enum OrderDirection { + """ + Specifies an ascending order for a given `orderBy` argument. + """ + ASC + + """ + Specifies a descending order for a given `orderBy` argument. + """ + DESC +} + +""" +Audit log entry for a org.add_billing_manager +""" +type OrgAddBillingManagerAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgAddBillingManagerAuditEntry object + """ + id: ID! + + """ + The email address used to invite a billing manager for the organization. + """ + invitationEmail: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.add_member +""" +type OrgAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgAddMemberAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The permission level of the member added to the organization. + """ + permission: OrgAddMemberAuditEntryPermission + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The permissions available to members on an Organization. +""" +enum OrgAddMemberAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + Can read and clone repositories. + """ + READ +} + +""" +Audit log entry for a org.block_user +""" +type OrgBlockUserAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The blocked user. + """ + blockedUser: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the blocked user. + """ + blockedUserName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the blocked user. + """ + blockedUserResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the blocked user. + """ + blockedUserUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgBlockUserAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.config.disable_collaborators_only event. +""" +type OrgConfigDisableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgConfigDisableCollaboratorsOnlyAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.config.enable_collaborators_only event. +""" +type OrgConfigEnableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgConfigEnableCollaboratorsOnlyAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.create event. +""" +type OrgCreateAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The billing plan for the Organization. + """ + billingPlan: OrgCreateAuditEntryBillingPlan + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgCreateAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The billing plans available for organizations. +""" +enum OrgCreateAuditEntryBillingPlan { + """ + Team Plan + """ + BUSINESS + + """ + Enterprise Cloud Plan + """ + BUSINESS_PLUS + + """ + Free Plan + """ + FREE + + """ + Tiered Per Seat Plan + """ + TIERED_PER_SEAT + + """ + Legacy Unlimited Plan + """ + UNLIMITED +} + +""" +Audit log entry for a org.disable_oauth_app_restrictions event. +""" +type OrgDisableOauthAppRestrictionsAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgDisableOauthAppRestrictionsAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.disable_saml event. +""" +type OrgDisableSamlAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The SAML provider's digest algorithm URL. + """ + digestMethodUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgDisableSamlAuditEntry object + """ + id: ID! + + """ + The SAML provider's issuer URL. + """ + issuerUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The SAML provider's signature algorithm URL. + """ + signatureMethodUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The SAML provider's single sign-on URL. + """ + singleSignOnUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.disable_two_factor_requirement event. +""" +type OrgDisableTwoFactorRequirementAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgDisableTwoFactorRequirementAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.enable_oauth_app_restrictions event. +""" +type OrgEnableOauthAppRestrictionsAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgEnableOauthAppRestrictionsAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.enable_saml event. +""" +type OrgEnableSamlAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The SAML provider's digest algorithm URL. + """ + digestMethodUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgEnableSamlAuditEntry object + """ + id: ID! + + """ + The SAML provider's issuer URL. + """ + issuerUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The SAML provider's signature algorithm URL. + """ + signatureMethodUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The SAML provider's single sign-on URL. + """ + singleSignOnUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.enable_two_factor_requirement event. +""" +type OrgEnableTwoFactorRequirementAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgEnableTwoFactorRequirementAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Ordering options for an organization's enterprise owner connections. +""" +input OrgEnterpriseOwnerOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise owners by. + """ + field: OrgEnterpriseOwnerOrderField! +} + +""" +Properties by which enterprise owners can be ordered. +""" +enum OrgEnterpriseOwnerOrderField { + """ + Order enterprise owners by login. + """ + LOGIN +} + +""" +Audit log entry for a org.invite_member event. +""" +type OrgInviteMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The email address of the organization invitation. + """ + email: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgInviteMemberAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The organization invitation. + """ + organizationInvitation: OrganizationInvitation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.invite_to_business event. +""" +type OrgInviteToBusinessAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + + """ + The Node ID of the OrgInviteToBusinessAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.oauth_app_access_approved event. +""" +type OrgOauthAppAccessApprovedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgOauthAppAccessApprovedAuditEntry object + """ + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.oauth_app_access_blocked event. +""" +type OrgOauthAppAccessBlockedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgOauthAppAccessBlockedAuditEntry object + """ + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.oauth_app_access_denied event. +""" +type OrgOauthAppAccessDeniedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgOauthAppAccessDeniedAuditEntry object + """ + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.oauth_app_access_requested event. +""" +type OrgOauthAppAccessRequestedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgOauthAppAccessRequestedAuditEntry object + """ + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.oauth_app_access_unblocked event. +""" +type OrgOauthAppAccessUnblockedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgOauthAppAccessUnblockedAuditEntry object + """ + id: ID! + + """ + The name of the OAuth application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.remove_billing_manager event. +""" +type OrgRemoveBillingManagerAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgRemoveBillingManagerAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The reason for the billing manager being removed. + """ + reason: OrgRemoveBillingManagerAuditEntryReason + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The reason a billing manager was removed from an Organization. +""" +enum OrgRemoveBillingManagerAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + SAML SSO enforcement requires an external identity + """ + SAML_SSO_ENFORCEMENT_REQUIRES_EXTERNAL_IDENTITY + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE +} + +""" +Audit log entry for a org.remove_member event. +""" +type OrgRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgRemoveMemberAuditEntry object + """ + id: ID! + + """ + The types of membership the member has with the organization. + """ + membershipTypes: [OrgRemoveMemberAuditEntryMembershipType!] + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The reason for the member being removed. + """ + reason: OrgRemoveMemberAuditEntryReason + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The type of membership a user has with an Organization. +""" +enum OrgRemoveMemberAuditEntryMembershipType { + """ + Organization owners have full access and can change several settings, + including the names of repositories that belong to the Organization and Owners + team membership. In addition, organization owners can delete the organization + and all of its repositories. + """ + ADMIN + + """ + A billing manager is a user who manages the billing settings for the Organization, such as updating payment information. + """ + BILLING_MANAGER + + """ + A direct member is a user that is a member of the Organization. + """ + DIRECT_MEMBER + + """ + An outside collaborator is a person who isn't explicitly a member of the + Organization, but who has Read, Write, or Admin permissions to one or more + repositories in the organization. + """ + OUTSIDE_COLLABORATOR + + """ + A suspended member. + """ + SUSPENDED + + """ + An unaffiliated collaborator is a person who is not a member of the + Organization and does not have access to any repositories in the Organization. + """ + UNAFFILIATED +} + +""" +The reason a member was removed from an Organization. +""" +enum OrgRemoveMemberAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + SAML SSO enforcement requires an external identity + """ + SAML_SSO_ENFORCEMENT_REQUIRES_EXTERNAL_IDENTITY + + """ + User was removed from organization during account recovery + """ + TWO_FACTOR_ACCOUNT_RECOVERY + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE + + """ + User account has been deleted + """ + USER_ACCOUNT_DELETED +} + +""" +Audit log entry for a org.remove_outside_collaborator event. +""" +type OrgRemoveOutsideCollaboratorAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgRemoveOutsideCollaboratorAuditEntry object + """ + id: ID! + + """ + The types of membership the outside collaborator has with the organization. + """ + membershipTypes: [OrgRemoveOutsideCollaboratorAuditEntryMembershipType!] + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The reason for the outside collaborator being removed from the Organization. + """ + reason: OrgRemoveOutsideCollaboratorAuditEntryReason + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The type of membership a user has with an Organization. +""" +enum OrgRemoveOutsideCollaboratorAuditEntryMembershipType { + """ + A billing manager is a user who manages the billing settings for the Organization, such as updating payment information. + """ + BILLING_MANAGER + + """ + An outside collaborator is a person who isn't explicitly a member of the + Organization, but who has Read, Write, or Admin permissions to one or more + repositories in the organization. + """ + OUTSIDE_COLLABORATOR + + """ + An unaffiliated collaborator is a person who is not a member of the + Organization and does not have access to any repositories in the organization. + """ + UNAFFILIATED +} + +""" +The reason an outside collaborator was removed from an Organization. +""" +enum OrgRemoveOutsideCollaboratorAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE +} + +""" +Audit log entry for a org.restore_member event. +""" +type OrgRestoreMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgRestoreMemberAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The number of custom email routings for the restored member. + """ + restoredCustomEmailRoutingsCount: Int + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The number of issue assignments for the restored member. + """ + restoredIssueAssignmentsCount: Int + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + Restored organization membership objects. + """ + restoredMemberships: [OrgRestoreMemberAuditEntryMembership!] + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The number of restored memberships. + """ + restoredMembershipsCount: Int + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The number of repositories of the restored member. + """ + restoredRepositoriesCount: Int + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The number of starred repositories for the restored member. + """ + restoredRepositoryStarsCount: Int + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The number of watched repositories for the restored member. + """ + restoredRepositoryWatchesCount: Int + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Types of memberships that can be restored for an Organization member. +""" +union OrgRestoreMemberAuditEntryMembership = + OrgRestoreMemberMembershipOrganizationAuditEntryData + | OrgRestoreMemberMembershipRepositoryAuditEntryData + | OrgRestoreMemberMembershipTeamAuditEntryData + +""" +Metadata for an organization membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipOrganizationAuditEntryData implements OrganizationAuditEntryData { + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Metadata for a repository membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipRepositoryAuditEntryData implements RepositoryAuditEntryData { + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI +} + +""" +Metadata for a team membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipTeamAuditEntryData implements TeamAuditEntryData { + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI +} + +""" +Audit log entry for a org.unblock_user +""" +type OrgUnblockUserAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user being unblocked by the organization. + """ + blockedUser: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the blocked user. + """ + blockedUserName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the blocked user. + """ + blockedUserResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the blocked user. + """ + blockedUserUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgUnblockUserAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a org.update_default_repository_permission +""" +type OrgUpdateDefaultRepositoryPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgUpdateDefaultRepositoryPermissionAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The new base repository permission level for the organization. + """ + permission: OrgUpdateDefaultRepositoryPermissionAuditEntryPermission + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The former base repository permission level for the organization. + """ + permissionWas: OrgUpdateDefaultRepositoryPermissionAuditEntryPermission + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The default permission a repository can have in an Organization. +""" +enum OrgUpdateDefaultRepositoryPermissionAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + No default permission value. + """ + NONE + + """ + Can read and clone repositories. + """ + READ + + """ + Can read, clone and push to repositories. + """ + WRITE +} + +""" +Audit log entry for a org.update_member event. +""" +type OrgUpdateMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgUpdateMemberAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The new member permission level for the organization. + """ + permission: OrgUpdateMemberAuditEntryPermission + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The former member permission level for the organization. + """ + permissionWas: OrgUpdateMemberAuditEntryPermission + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The permissions available to members on an Organization. +""" +enum OrgUpdateMemberAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + Can read and clone repositories. + """ + READ +} + +""" +Audit log entry for a org.update_member_repository_creation_permission event. +""" +type OrgUpdateMemberRepositoryCreationPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + Can members create repositories in the organization. + """ + canCreateRepositories: Boolean + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgUpdateMemberRepositoryCreationPermissionAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The permission for visibility level of repositories for this organization. + """ + visibility: OrgUpdateMemberRepositoryCreationPermissionAuditEntryVisibility + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The permissions available for repository creation on an Organization. +""" +enum OrgUpdateMemberRepositoryCreationPermissionAuditEntryVisibility { + """ + All organization members are restricted from creating any repositories. + """ + ALL + + """ + All organization members are restricted from creating internal repositories. + """ + INTERNAL + + """ + All organization members are allowed to create any repositories. + """ + NONE + + """ + All organization members are restricted from creating private repositories. + """ + PRIVATE + + """ + All organization members are restricted from creating private or internal repositories. + """ + PRIVATE_INTERNAL + + """ + All organization members are restricted from creating public repositories. + """ + PUBLIC + + """ + All organization members are restricted from creating public or internal repositories. + """ + PUBLIC_INTERNAL + + """ + All organization members are restricted from creating public or private repositories. + """ + PUBLIC_PRIVATE +} + +""" +Audit log entry for a org.update_member_repository_invitation_permission event. +""" +type OrgUpdateMemberRepositoryInvitationPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + Can outside collaborators be invited to repositories in the organization. + """ + canInviteOutsideCollaboratorsToRepositories: Boolean + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the OrgUpdateMemberRepositoryInvitationPermissionAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +An account on GitHub, with one or more owners, that has repositories, members and teams. +""" +type Organization implements Actor & MemberStatusable & Node & PackageOwner & ProfileOwner & ProjectOwner & ProjectV2Owner & ProjectV2Recent & RepositoryDiscussionAuthor & RepositoryDiscussionCommentAuthor & RepositoryOwner & Sponsorable & UniformResourceLocatable { + """ + The announcement banner set on this organization, if any. Only visible to members of the organization's enterprise. + """ + announcementBanner: AnnouncementBanner + + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + Identifies the date and time when the organization was archived. + """ + archivedAt: DateTime + + """ + Audit log entries of the organization + """ + auditLog( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned audit log entries. + """ + orderBy: AuditLogOrder = {field: CREATED_AT, direction: DESC} + + """ + The query string to filter audit entries + """ + query: String + ): OrganizationAuditEntryConnection! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A URL pointing to the organization's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The organization's public profile description. + """ + description: String + + """ + The organization's public profile description rendered to HTML. + """ + descriptionHTML: String + + """ + A list of domains owned by the organization. + """ + domains( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter by if the domain is approved. + """ + isApproved: Boolean = null + + """ + Filter by if the domain is verified. + """ + isVerified: Boolean = null + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for verifiable domains returned. + """ + orderBy: VerifiableDomainOrder = {field: DOMAIN, direction: ASC} + ): VerifiableDomainConnection + + """ + The organization's public email. + """ + email: String + + """ + A list of owners of the organization's enterprise account. + """ + enterpriseOwners( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for enterprise owners returned from the connection. + """ + orderBy: OrgEnterpriseOwnerOrder = {field: LOGIN, direction: ASC} + + """ + The organization role to filter by. + """ + organizationRole: RoleInOrganization + + """ + The search string to look for. + """ + query: String + ): OrganizationEnterpriseOwnerConnection! + + """ + The estimated next GitHub Sponsors payout for this user/organization in cents (USD). + """ + estimatedNextSponsorsPayoutInCents: Int! + + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + + """ + The Node ID of the Organization object + """ + id: ID! + + """ + The interaction ability settings for this organization. + """ + interactionAbility: RepositoryInteractionAbility + + """ + The setting value for whether the organization has an IP allow list enabled. + """ + ipAllowListEnabledSetting: IpAllowListEnabledSettingValue! + + """ + The IP addresses that are allowed to access resources owned by the organization. + """ + ipAllowListEntries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for IP allow list entries returned. + """ + orderBy: IpAllowListEntryOrder = {field: ALLOW_LIST_VALUE, direction: ASC} + ): IpAllowListEntryConnection! + + """ + The setting value for whether the organization has IP allow list configuration for installed GitHub Apps enabled. + """ + ipAllowListForInstalledAppsEnabledSetting: IpAllowListForInstalledAppsEnabledSettingValue! + + """ + Whether the given account is sponsoring this user/organization. + """ + isSponsoredBy( + """ + The target account's login. + """ + accountLogin: String! + ): Boolean! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + Whether the organization has verified its profile email and website. + """ + isVerified: Boolean! + + """ + A list of the organization's issue types + """ + issueTypes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue types returned from the connection. + """ + orderBy: IssueTypeOrder = {field: CREATED_AT, direction: ASC} + ): IssueTypeConnection + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + Calculate how much each sponsor has ever paid total to this maintainer via + GitHub Sponsors. Does not include sponsorships paid via Patreon. + """ + lifetimeReceivedSponsorshipValues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for results returned from the connection. + """ + orderBy: SponsorAndLifetimeValueOrder = {field: SPONSOR_LOGIN, direction: ASC} + ): SponsorAndLifetimeValueConnection! + + """ + The organization's public profile location. + """ + location: String + + """ + The organization's login name. + """ + login: String! + + """ + A list of all mannequins for this organization. + """ + mannequins( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter mannequins by login. + """ + login: String + + """ + Ordering options for mannequins returned from the connection. + """ + orderBy: MannequinOrder = {field: CREATED_AT, direction: ASC} + ): MannequinConnection! + + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! + + """ + Members can fork private repositories in this organization + """ + membersCanForkPrivateRepositories: Boolean! + + """ + A list of users who are members of this organization. + """ + membersWithRole( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationMemberConnection! + + """ + The estimated monthly GitHub Sponsors income for this user/organization in cents (USD). + """ + monthlyEstimatedSponsorsIncomeInCents: Int! + + """ + The organization's public profile name. + """ + name: String + + """ + The HTTP path creating a new team + """ + newTeamResourcePath: URI! + + """ + The HTTP URL creating a new team + """ + newTeamUrl: URI! + + """ + Indicates if email notification delivery for this organization is restricted to verified or approved domains. + """ + notificationDeliveryRestrictionEnabledSetting: NotificationRestrictionSettingValue! + + """ + The billing email for the organization. + """ + organizationBillingEmail: String + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + A list of users who have been invited to join this organization. + """ + pendingMembers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP path listing organization's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing organization's projects + """ + projectsUrl: URI! + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter projects based on user role. + """ + minPermissionLevel: ProjectV2PermissionLevel = READ + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the owner. + """ + query: String + ): ProjectV2Connection! + + """ + Recent projects that this user has modified in the context of the owner. + """ + recentProjects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they are archived and not maintained + """ + isArchived: Boolean + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy. Internal + repositories are considered private; consider using the visibility argument + if only internal repositories are needed. Cannot be combined with the + visibility argument. + """ + privacy: RepositoryPrivacy + + """ + If non-null, filters repositories according to visibility. Cannot be combined with the privacy argument. + """ + visibility: RepositoryVisibility + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Follow repository renames. If disabled, a repository referenced by its old name will return an error. + """ + followRenames: Boolean = true + + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + Discussion comments this user has authored. + """ + repositoryDiscussionComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter discussion comments to only those that were marked as the answer + """ + onlyAnswers: Boolean = false + + """ + Filter discussion comments to only those in a specific repository. + """ + repositoryId: ID + ): DiscussionCommentConnection! + + """ + Discussions this user has started. + """ + repositoryDiscussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter discussions to only those that have been answered or not. Defaults to + including both answered and unanswered discussions. + """ + answered: Boolean = null + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for discussions returned from the connection. + """ + orderBy: DiscussionOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter discussions to only those in a specific repository. + """ + repositoryId: ID + + """ + A list of states to filter the discussions by. + """ + states: [DiscussionState!] = [] + ): DiscussionConnection! + + """ + A list of all repository migrations for this organization. + """ + repositoryMigrations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repository migrations returned. + """ + orderBy: RepositoryMigrationOrder = {field: CREATED_AT, direction: ASC} + + """ + Filter repository migrations by repository name. + """ + repositoryName: String + + """ + Filter repository migrations by state. + """ + state: MigrationState + ): RepositoryMigrationConnection! + + """ + When true the organization requires all members, billing managers, and outside + collaborators to enable two-factor authentication. + """ + requiresTwoFactorAuthentication: Boolean + + """ + The HTTP path for this organization. + """ + resourcePath: URI! + + """ + Returns a single ruleset from the current organization by ID. + """ + ruleset( + """ + The ID of the ruleset to be returned. + """ + databaseId: Int! + + """ + Include rulesets configured at higher levels that apply to this organization. + """ + includeParents: Boolean = true + ): RepositoryRuleset + + """ + A list of rulesets for this organization. + """ + rulesets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Return rulesets configured at higher levels that apply to this organization + """ + includeParents: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Return rulesets that apply to the specified target + """ + targets: [RepositoryRulesetTarget!] = null + ): RepositoryRulesetConnection + + """ + The Organization's SAML identity provider. Visible to (1) organization owners, + (2) organization owners' personal access tokens (classic) with read:org or + admin:org scope, (3) GitHub App with an installation token with read or write + access to members. + """ + samlIdentityProvider: OrganizationIdentityProvider + + """ + List of users and organizations this entity is sponsoring. + """ + sponsoring( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the users and organizations returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + ): SponsorConnection! + + """ + List of sponsors for this user or organization. + """ + sponsors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsors returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + + """ + If given, will filter for sponsors at the given tier. Will only return + sponsors whose tier the viewer is permitted to see. + """ + tierId: ID + ): SponsorConnection! + + """ + Events involving this sponsorable, such as new sponsorships. + """ + sponsorsActivities( + """ + Filter activities to only the specified actions. + """ + actions: [SponsorsActivityAction!] = [] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to include those events where this sponsorable acted as the sponsor. + Defaults to only including events where this sponsorable was the recipient + of a sponsorship. + """ + includeAsSponsor: Boolean = false + + """ + Whether or not to include private activities in the result set. Defaults to including public and private activities. + """ + includePrivate: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for activity returned from the connection. + """ + orderBy: SponsorsActivityOrder = {field: TIMESTAMP, direction: DESC} + + """ + Filter activities returned to only those that occurred in the most recent + specified time period. Set to ALL to avoid filtering by when the activity + occurred. Will be ignored if `since` or `until` is given. + """ + period: SponsorsActivityPeriod = MONTH + + """ + Filter activities to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter activities to those that occurred before this time. + """ + until: DateTime + ): SponsorsActivityConnection! + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + The sponsorship from the viewer to this user/organization; that is, the sponsorship where you're the sponsor. + """ + sponsorshipForViewerAsSponsor( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the viewer's sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + The sponsorship from this user/organization to the viewer; that is, the sponsorship you're receiving. + """ + sponsorshipForViewerAsSponsorable( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + List of sponsorship updates sent from this sponsorable to sponsors. + """ + sponsorshipNewsletters( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorship updates returned from the connection. + """ + orderBy: SponsorshipNewsletterOrder = {field: CREATED_AT, direction: DESC} + ): SponsorshipNewsletterConnection! + + """ + The sponsorships where this user or organization is the maintainer receiving the funds. + """ + sponsorshipsAsMaintainer( + """ + Whether to include only sponsorships that are active right now, versus all + sponsorships this maintainer has ever received. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + The sponsorships where this user or organization is the funder. + """ + sponsorshipsAsSponsor( + """ + Whether to include only sponsorships that are active right now, versus all sponsorships this sponsor has ever made. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter sponsorships returned to those for the specified maintainers. That + is, the recipient of the sponsorship is a user or organization with one of + the given logins. + """ + maintainerLogins: [String!] + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + Find an organization's team by its slug. + """ + team( + """ + The name or slug of the team to find. + """ + slug: String! + ): Team + + """ + A list of teams in this organization. + """ + teams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If true, filters teams that are mapped to an LDAP Group (Enterprise only) + """ + ldapMapped: Boolean + + """ + If non-null, filters teams according to notification setting + """ + notificationSetting: TeamNotificationSetting + + """ + Ordering options for teams returned from the connection + """ + orderBy: TeamOrder + + """ + If non-null, filters teams according to privacy + """ + privacy: TeamPrivacy + + """ + If non-null, filters teams with query on team name and team slug + """ + query: String + + """ + If non-null, filters teams according to whether the viewer is an admin or member on team + """ + role: TeamRole + + """ + If true, restrict to only root teams + """ + rootTeamsOnly: Boolean = false + + """ + User logins to filter by + """ + userLogins: [String!] + ): TeamConnection! + + """ + The HTTP path listing organization's teams + """ + teamsResourcePath: URI! + + """ + The HTTP URL listing organization's teams + """ + teamsUrl: URI! + + """ + The amount in United States cents (e.g., 500 = $5.00 USD) that this entity has + spent on GitHub to fund sponsorships. Only returns a value when viewed by the + user themselves or by a user who can manage sponsorships for the requested organization. + """ + totalSponsorshipAmountAsSponsorInCents( + """ + Filter payments to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter payments to those made to the users or organizations with the specified usernames. + """ + sponsorableLogins: [String!] = [] + + """ + Filter payments to those that occurred before this time. + """ + until: DateTime + ): Int + + """ + The organization's Twitter username. + """ + twitterUsername: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this organization. + """ + url: URI! + + """ + Organization is adminable by the viewer. + """ + viewerCanAdminister: Boolean! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Viewer can create repositories on this organization + """ + viewerCanCreateRepositories: Boolean! + + """ + Viewer can create teams on this organization. + """ + viewerCanCreateTeams: Boolean! + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + Viewer is an active member of this organization. + """ + viewerIsAMember: Boolean! + + """ + Whether or not this Organization is followed by the viewer. + """ + viewerIsFollowing: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! + + """ + Whether contributors are required to sign off on web-based commits for repositories in this organization. + """ + webCommitSignoffRequired: Boolean! + + """ + The organization's public profile URL. + """ + websiteUrl: URI +} + +""" +An audit entry in an organization audit log. +""" +union OrganizationAuditEntry = + MembersCanDeleteReposClearAuditEntry + | MembersCanDeleteReposDisableAuditEntry + | MembersCanDeleteReposEnableAuditEntry + | OauthApplicationCreateAuditEntry + | OrgAddBillingManagerAuditEntry + | OrgAddMemberAuditEntry + | OrgBlockUserAuditEntry + | OrgConfigDisableCollaboratorsOnlyAuditEntry + | OrgConfigEnableCollaboratorsOnlyAuditEntry + | OrgCreateAuditEntry + | OrgDisableOauthAppRestrictionsAuditEntry + | OrgDisableSamlAuditEntry + | OrgDisableTwoFactorRequirementAuditEntry + | OrgEnableOauthAppRestrictionsAuditEntry + | OrgEnableSamlAuditEntry + | OrgEnableTwoFactorRequirementAuditEntry + | OrgInviteMemberAuditEntry + | OrgInviteToBusinessAuditEntry + | OrgOauthAppAccessApprovedAuditEntry + | OrgOauthAppAccessBlockedAuditEntry + | OrgOauthAppAccessDeniedAuditEntry + | OrgOauthAppAccessRequestedAuditEntry + | OrgOauthAppAccessUnblockedAuditEntry + | OrgRemoveBillingManagerAuditEntry + | OrgRemoveMemberAuditEntry + | OrgRemoveOutsideCollaboratorAuditEntry + | OrgRestoreMemberAuditEntry + | OrgUnblockUserAuditEntry + | OrgUpdateDefaultRepositoryPermissionAuditEntry + | OrgUpdateMemberAuditEntry + | OrgUpdateMemberRepositoryCreationPermissionAuditEntry + | OrgUpdateMemberRepositoryInvitationPermissionAuditEntry + | PrivateRepositoryForkingDisableAuditEntry + | PrivateRepositoryForkingEnableAuditEntry + | RepoAccessAuditEntry + | RepoAddMemberAuditEntry + | RepoAddTopicAuditEntry + | RepoArchivedAuditEntry + | RepoChangeMergeSettingAuditEntry + | RepoConfigDisableAnonymousGitAccessAuditEntry + | RepoConfigDisableCollaboratorsOnlyAuditEntry + | RepoConfigDisableContributorsOnlyAuditEntry + | RepoConfigDisableSockpuppetDisallowedAuditEntry + | RepoConfigEnableAnonymousGitAccessAuditEntry + | RepoConfigEnableCollaboratorsOnlyAuditEntry + | RepoConfigEnableContributorsOnlyAuditEntry + | RepoConfigEnableSockpuppetDisallowedAuditEntry + | RepoConfigLockAnonymousGitAccessAuditEntry + | RepoConfigUnlockAnonymousGitAccessAuditEntry + | RepoCreateAuditEntry + | RepoDestroyAuditEntry + | RepoRemoveMemberAuditEntry + | RepoRemoveTopicAuditEntry + | RepositoryVisibilityChangeDisableAuditEntry + | RepositoryVisibilityChangeEnableAuditEntry + | TeamAddMemberAuditEntry + | TeamAddRepositoryAuditEntry + | TeamChangeParentTeamAuditEntry + | TeamRemoveMemberAuditEntry + | TeamRemoveRepositoryAuditEntry + +""" +The connection type for OrganizationAuditEntry. +""" +type OrganizationAuditEntryConnection { + """ + A list of edges. + """ + edges: [OrganizationAuditEntryEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationAuditEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Metadata for an audit entry with action org.* +""" +interface OrganizationAuditEntryData { + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +An edge in a connection. +""" +type OrganizationAuditEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationAuditEntry +} + +""" +A list of organizations managed by an enterprise. +""" +type OrganizationConnection { + """ + A list of edges. + """ + edges: [OrganizationEdge] + + """ + A list of nodes. + """ + nodes: [Organization] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type OrganizationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Organization +} + +""" +The connection type for User. +""" +type OrganizationEnterpriseOwnerConnection { + """ + A list of edges. + """ + edges: [OrganizationEnterpriseOwnerEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An enterprise owner in the context of an organization that is part of the enterprise. +""" +type OrganizationEnterpriseOwnerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User + + """ + The role of the owner with respect to the organization. + """ + organizationRole: RoleInOrganization! +} + +""" +An Identity Provider configured to provision SAML and SCIM identities for +Organizations. Visible to (1) organization owners, (2) organization owners' +personal access tokens (classic) with read:org or admin:org scope, (3) GitHub +App with an installation token with read or write access to members. +""" +type OrganizationIdentityProvider implements Node { + """ + The digest algorithm used to sign SAML requests for the Identity Provider. + """ + digestMethod: URI + + """ + External Identities provisioned by this Identity Provider + """ + externalIdentities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter to external identities with the users login + """ + login: String + + """ + Filter to external identities with valid org membership only + """ + membersOnly: Boolean + + """ + Filter to external identities with the users userName/NameID attribute + """ + userName: String + ): ExternalIdentityConnection! + + """ + The Node ID of the OrganizationIdentityProvider object + """ + id: ID! + + """ + The x509 certificate used by the Identity Provider to sign assertions and responses. + """ + idpCertificate: X509Certificate + + """ + The Issuer Entity ID for the SAML Identity Provider + """ + issuer: String + + """ + Organization this Identity Provider belongs to + """ + organization: Organization + + """ + The signature algorithm used to sign SAML requests for the Identity Provider. + """ + signatureMethod: URI + + """ + The URL endpoint for the Identity Provider's SAML SSO. + """ + ssoUrl: URI +} + +""" +An Invitation for a user to an organization. +""" +type OrganizationInvitation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email address of the user invited to the organization. + """ + email: String + + """ + The Node ID of the OrganizationInvitation object + """ + id: ID! + + """ + The source of the invitation. + """ + invitationSource: OrganizationInvitationSource! + + """ + The type of invitation that was sent (e.g. email, user). + """ + invitationType: OrganizationInvitationType! + + """ + The user who was invited to the organization. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User! + @deprecated( + reason: "`inviter` will be removed. `inviter` will be replaced by `inviterActor`. Removal on 2024-07-01 UTC." + ) + + """ + The user who created the invitation. + """ + inviterActor: User + + """ + The organization the invite is for + """ + organization: Organization! + + """ + The user's pending role in the organization (e.g. member, owner). + """ + role: OrganizationInvitationRole! +} + +""" +The connection type for OrganizationInvitation. +""" +type OrganizationInvitationConnection { + """ + A list of edges. + """ + edges: [OrganizationInvitationEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type OrganizationInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationInvitation +} + +""" +The possible organization invitation roles. +""" +enum OrganizationInvitationRole { + """ + The user is invited to be an admin of the organization. + """ + ADMIN + + """ + The user is invited to be a billing manager of the organization. + """ + BILLING_MANAGER + + """ + The user is invited to be a direct member of the organization. + """ + DIRECT_MEMBER + + """ + The user's previous role will be reinstated. + """ + REINSTATE +} + +""" +The possible organization invitation sources. +""" +enum OrganizationInvitationSource { + """ + The invitation was created from the web interface or from API + """ + MEMBER + + """ + The invitation was created from SCIM + """ + SCIM + + """ + The invitation was sent before this feature was added + """ + UNKNOWN +} + +""" +The possible organization invitation types. +""" +enum OrganizationInvitationType { + """ + The invitation was to an email address. + """ + EMAIL + + """ + The invitation was to an existing user. + """ + USER +} + +""" +A list of users who belong to the organization. +""" +type OrganizationMemberConnection { + """ + A list of edges. + """ + edges: [OrganizationMemberEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user within an organization. +""" +type OrganizationMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + Whether the organization member has two factor enabled or not. Returns null if information is not available to viewer. + """ + hasTwoFactorEnabled: Boolean + + """ + The item at the end of the edge. + """ + node: User + + """ + The role this user has in the organization. + """ + role: OrganizationMemberRole +} + +""" +The possible roles within an organization for its members. +""" +enum OrganizationMemberRole { + """ + The user is an administrator of the organization. + """ + ADMIN + + """ + The user is a member of the organization. + """ + MEMBER +} + +""" +The possible values for the members can create repositories setting on an organization. +""" +enum OrganizationMembersCanCreateRepositoriesSettingValue { + """ + Members will be able to create public and private repositories. + """ + ALL + + """ + Members will not be able to create public or private repositories. + """ + DISABLED + + """ + Members will be able to create only internal repositories. + """ + INTERNAL + + """ + Members will be able to create only private repositories. + """ + PRIVATE +} + +""" +A GitHub Enterprise Importer (GEI) organization migration. +""" +type OrganizationMigration implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: String + + """ + The reason the organization migration failed. + """ + failureReason: String + + """ + The Node ID of the OrganizationMigration object + """ + id: ID! + + """ + The remaining amount of repos to be migrated. + """ + remainingRepositoriesCount: Int + + """ + The name of the source organization to be migrated. + """ + sourceOrgName: String! + + """ + The URL of the source organization to migrate. + """ + sourceOrgUrl: URI! + + """ + The migration state. + """ + state: OrganizationMigrationState! + + """ + The name of the target organization. + """ + targetOrgName: String! + + """ + The total amount of repositories to be migrated. + """ + totalRepositoriesCount: Int +} + +""" +The Octoshift Organization migration state. +""" +enum OrganizationMigrationState { + """ + The Octoshift migration has failed. + """ + FAILED + + """ + The Octoshift migration has invalid credentials. + """ + FAILED_VALIDATION + + """ + The Octoshift migration is in progress. + """ + IN_PROGRESS + + """ + The Octoshift migration has not started. + """ + NOT_STARTED + + """ + The Octoshift migration needs to have its credentials validated. + """ + PENDING_VALIDATION + + """ + The Octoshift migration is performing post repository migrations. + """ + POST_REPO_MIGRATION + + """ + The Octoshift migration is performing pre repository migrations. + """ + PRE_REPO_MIGRATION + + """ + The Octoshift migration has been queued. + """ + QUEUED + + """ + The Octoshift org migration is performing repository migrations. + """ + REPO_MIGRATION + + """ + The Octoshift migration has succeeded. + """ + SUCCEEDED +} + +""" +Used for argument of CreateProjectV2 mutation. +""" +union OrganizationOrUser = Organization | User + +""" +Ordering options for organization connections. +""" +input OrganizationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order organizations by. + """ + field: OrganizationOrderField! +} + +""" +Properties by which organization connections can be ordered. +""" +enum OrganizationOrderField { + """ + Order organizations by creation time + """ + CREATED_AT + + """ + Order organizations by login + """ + LOGIN +} + +""" +An organization teams hovercard context +""" +type OrganizationTeamsHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Teams in this organization the user is a member of that are relevant + """ + relevantTeams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + The path for the full team list for this user + """ + teamsResourcePath: URI! + + """ + The URL for the full team list for this user + """ + teamsUrl: URI! + + """ + The total number of teams the user is on in the organization + """ + totalTeamCount: Int! +} + +""" +An organization list hovercard context +""" +type OrganizationsHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Organizations this user is a member of that are relevant + """ + relevantOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the User's organizations. + """ + orderBy: OrganizationOrder = null + ): OrganizationConnection! + + """ + The total number of organizations this user is in + """ + totalOrganizationCount: Int! +} + +""" +Information for an uploaded package. +""" +type Package implements Node { + """ + The Node ID of the Package object + """ + id: ID! + + """ + Find the latest version for the package. + """ + latestVersion: PackageVersion + + """ + Identifies the name of the package. + """ + name: String! + + """ + Identifies the type of the package. + """ + packageType: PackageType! + + """ + The repository this package belongs to. + """ + repository: Repository + + """ + Statistics about package activity. + """ + statistics: PackageStatistics + + """ + Find package version by version string. + """ + version( + """ + The package version. + """ + version: String! + ): PackageVersion + + """ + list of versions for this package + """ + versions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering of the returned packages. + """ + orderBy: PackageVersionOrder = {field: CREATED_AT, direction: DESC} + ): PackageVersionConnection! +} + +""" +The connection type for Package. +""" +type PackageConnection { + """ + A list of edges. + """ + edges: [PackageEdge] + + """ + A list of nodes. + """ + nodes: [Package] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Package +} + +""" +A file in a package version. +""" +type PackageFile implements Node { + """ + The Node ID of the PackageFile object + """ + id: ID! + + """ + MD5 hash of the file. + """ + md5: String + + """ + Name of the file. + """ + name: String! + + """ + The package version this file belongs to. + """ + packageVersion: PackageVersion + + """ + SHA1 hash of the file. + """ + sha1: String + + """ + SHA256 hash of the file. + """ + sha256: String + + """ + Size of the file in bytes. + """ + size: Int + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + URL to download the asset. + """ + url: URI +} + +""" +The connection type for PackageFile. +""" +type PackageFileConnection { + """ + A list of edges. + """ + edges: [PackageFileEdge] + + """ + A list of nodes. + """ + nodes: [PackageFile] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageFileEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PackageFile +} + +""" +Ways in which lists of package files can be ordered upon return. +""" +input PackageFileOrder { + """ + The direction in which to order package files by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order package files by. + """ + field: PackageFileOrderField +} + +""" +Properties by which package file connections can be ordered. +""" +enum PackageFileOrderField { + """ + Order package files by creation time + """ + CREATED_AT +} + +""" +Ways in which lists of packages can be ordered upon return. +""" +input PackageOrder { + """ + The direction in which to order packages by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order packages by. + """ + field: PackageOrderField +} + +""" +Properties by which package connections can be ordered. +""" +enum PackageOrderField { + """ + Order packages by creation time + """ + CREATED_AT +} + +""" +Represents an owner of a package. +""" +interface PackageOwner { + """ + The Node ID of the PackageOwner object + """ + id: ID! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! +} + +""" +Represents a object that contains package activity statistics such as downloads. +""" +type PackageStatistics { + """ + Number of times the package was downloaded since it was created. + """ + downloadsTotalCount: Int! +} + +""" +A version tag contains the mapping between a tag name and a version. +""" +type PackageTag implements Node { + """ + The Node ID of the PackageTag object + """ + id: ID! + + """ + Identifies the tag name of the version. + """ + name: String! + + """ + Version that the tag is associated with. + """ + version: PackageVersion +} + +""" +The possible types of a package. +""" +enum PackageType { + """ + A debian package. + """ + DEBIAN + + """ + A docker image. + """ + DOCKER + @deprecated( + reason: "DOCKER will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2021-06-21 UTC." + ) + + """ + A maven package. + """ + MAVEN + @deprecated( + reason: "MAVEN will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2023-02-10 UTC." + ) + + """ + An npm package. + """ + NPM + @deprecated( + reason: "NPM will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2022-11-21 UTC." + ) + + """ + A nuget package. + """ + NUGET + @deprecated( + reason: "NUGET will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2022-11-21 UTC." + ) + + """ + A python package. + """ + PYPI + + """ + A rubygems package. + """ + RUBYGEMS + @deprecated( + reason: "RUBYGEMS will be removed from this enum as this type will be migrated to only be used by the Packages REST API. Removal on 2022-12-28 UTC." + ) +} + +""" +Information about a specific package version. +""" +type PackageVersion implements Node { + """ + List of files associated with this package version + """ + files( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering of the returned package files. + """ + orderBy: PackageFileOrder = {field: CREATED_AT, direction: ASC} + ): PackageFileConnection! + + """ + The Node ID of the PackageVersion object + """ + id: ID! + + """ + The package associated with this version. + """ + package: Package + + """ + The platform this version was built for. + """ + platform: String + + """ + Whether or not this version is a pre-release. + """ + preRelease: Boolean! + + """ + The README of this package version. + """ + readme: String + + """ + The release associated with this package version. + """ + release: Release + + """ + Statistics about package activity. + """ + statistics: PackageVersionStatistics + + """ + The package version summary. + """ + summary: String + + """ + The version string. + """ + version: String! +} + +""" +The connection type for PackageVersion. +""" +type PackageVersionConnection { + """ + A list of edges. + """ + edges: [PackageVersionEdge] + + """ + A list of nodes. + """ + nodes: [PackageVersion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageVersionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PackageVersion +} + +""" +Ways in which lists of package versions can be ordered upon return. +""" +input PackageVersionOrder { + """ + The direction in which to order package versions by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order package versions by. + """ + field: PackageVersionOrderField +} + +""" +Properties by which package version connections can be ordered. +""" +enum PackageVersionOrderField { + """ + Order package versions by creation time + """ + CREATED_AT +} + +""" +Represents a object that contains package version activity statistics such as downloads. +""" +type PackageVersionStatistics { + """ + Number of times the package was downloaded since it was created. + """ + downloadsTotalCount: Int! +} + +""" +Information about pagination in a connection. +""" +type PageInfo { + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String + + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String +} + +""" +Represents a 'parent_issue_added' event on a given issue. +""" +type ParentIssueAddedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ParentIssueAddedEvent object + """ + id: ID! + + """ + The parent issue added. + """ + parent: Issue +} + +""" +Represents a 'parent_issue_removed' event on a given issue. +""" +type ParentIssueRemovedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ParentIssueRemovedEvent object + """ + id: ID! + + """ + The parent issue removed. + """ + parent: Issue +} + +""" +The possible types of patch statuses. +""" +enum PatchStatus { + """ + The file was added. Git status 'A'. + """ + ADDED + + """ + The file's type was changed. Git status 'T'. + """ + CHANGED + + """ + The file was copied. Git status 'C'. + """ + COPIED + + """ + The file was deleted. Git status 'D'. + """ + DELETED + + """ + The file's contents were changed. Git status 'M'. + """ + MODIFIED + + """ + The file was renamed. Git status 'R'. + """ + RENAMED +} + +""" +Types that can grant permissions on a repository to a user +""" +union PermissionGranter = Organization | Repository | Team + +""" +A level of permission and source for a user's access to a repository. +""" +type PermissionSource { + """ + The organization the repository belongs to. + """ + organization: Organization! + + """ + The level of access this source has granted to the user. + """ + permission: DefaultRepositoryPermissionField! + + """ + The name of the role this source has granted to the user. + """ + roleName: String + + """ + The source of this permission. + """ + source: PermissionGranter! +} + +""" +Autogenerated input type of PinEnvironment +""" +input PinEnvironmentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the environment to modify + """ + environmentId: ID! @possibleTypes(concreteTypes: ["Environment"]) + + """ + The desired state of the environment. If true, environment will be pinned. If false, it will be unpinned. + """ + pinned: Boolean! +} + +""" +Autogenerated return type of PinEnvironment. +""" +type PinEnvironmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The environment that was pinned + """ + environment: Environment + + """ + The pinned environment if we pinned + """ + pinnedEnvironment: PinnedEnvironment +} + +""" +Autogenerated input type of PinIssue +""" +input PinIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to be pinned + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of PinIssue. +""" +type PinIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was pinned + """ + issue: Issue +} + +""" +Types that can be pinned to a profile page. +""" +union PinnableItem = Gist | Repository + +""" +The connection type for PinnableItem. +""" +type PinnableItemConnection { + """ + A list of edges. + """ + edges: [PinnableItemEdge] + + """ + A list of nodes. + """ + nodes: [PinnableItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnableItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnableItem +} + +""" +Represents items that can be pinned to a profile page or dashboard. +""" +enum PinnableItemType { + """ + A gist. + """ + GIST + + """ + An issue. + """ + ISSUE + + """ + An organization. + """ + ORGANIZATION + + """ + A project. + """ + PROJECT + + """ + A pull request. + """ + PULL_REQUEST + + """ + A repository. + """ + REPOSITORY + + """ + A team. + """ + TEAM + + """ + A user. + """ + USER +} + +""" +A Pinned Discussion is a discussion pinned to a repository's index page. +""" +type PinnedDiscussion implements Node & RepositoryNode { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The discussion that was pinned. + """ + discussion: Discussion! + + """ + Color stops of the chosen gradient + """ + gradientStopColors: [String!]! + + """ + The Node ID of the PinnedDiscussion object + """ + id: ID! + + """ + Background texture pattern + """ + pattern: PinnedDiscussionPattern! + + """ + The actor that pinned this discussion. + """ + pinnedBy: Actor! + + """ + Preconfigured background gradient option + """ + preconfiguredGradient: PinnedDiscussionGradient + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for PinnedDiscussion. +""" +type PinnedDiscussionConnection { + """ + A list of edges. + """ + edges: [PinnedDiscussionEdge] + + """ + A list of nodes. + """ + nodes: [PinnedDiscussion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnedDiscussionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnedDiscussion +} + +""" +Preconfigured gradients that may be used to style discussions pinned within a repository. +""" +enum PinnedDiscussionGradient { + """ + A gradient of blue to mint + """ + BLUE_MINT + + """ + A gradient of blue to purple + """ + BLUE_PURPLE + + """ + A gradient of pink to blue + """ + PINK_BLUE + + """ + A gradient of purple to coral + """ + PURPLE_CORAL + + """ + A gradient of red to orange + """ + RED_ORANGE +} + +""" +Preconfigured background patterns that may be used to style discussions pinned within a repository. +""" +enum PinnedDiscussionPattern { + """ + An upward-facing chevron pattern + """ + CHEVRON_UP + + """ + A hollow dot pattern + """ + DOT + + """ + A solid dot pattern + """ + DOT_FILL + + """ + A heart pattern + """ + HEART_FILL + + """ + A plus sign pattern + """ + PLUS + + """ + A lightning bolt pattern + """ + ZAP +} + +""" +Represents a pinned environment on a given repository +""" +type PinnedEnvironment implements Node { + """ + Identifies the date and time when the pinned environment was created + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Identifies the environment associated. + """ + environment: Environment! + + """ + The Node ID of the PinnedEnvironment object + """ + id: ID! + + """ + Identifies the position of the pinned environment. + """ + position: Int! + + """ + The repository that this environment was pinned to. + """ + repository: Repository! +} + +""" +The connection type for PinnedEnvironment. +""" +type PinnedEnvironmentConnection { + """ + A list of edges. + """ + edges: [PinnedEnvironmentEdge] + + """ + A list of nodes. + """ + nodes: [PinnedEnvironment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnedEnvironmentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnedEnvironment +} + +""" +Ordering options for pinned environments +""" +input PinnedEnvironmentOrder { + """ + The direction in which to order pinned environments by the specified field. + """ + direction: OrderDirection! + + """ + The field to order pinned environments by. + """ + field: PinnedEnvironmentOrderField! +} + +""" +Properties by which pinned environments connections can be ordered +""" +enum PinnedEnvironmentOrderField { + """ + Order pinned environments by position + """ + POSITION +} + +""" +Represents a 'pinned' event on a given issue or pull request. +""" +type PinnedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the PinnedEvent object + """ + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +A Pinned Issue is a issue pinned to a repository's index page. +""" +type PinnedIssue implements Node { + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The Node ID of the PinnedIssue object + """ + id: ID! + + """ + The issue that was pinned. + """ + issue: Issue! + + """ + The actor that pinned this issue. + """ + pinnedBy: Actor! + + """ + The repository that this issue was pinned to. + """ + repository: Repository! +} + +""" +The connection type for PinnedIssue. +""" +type PinnedIssueConnection { + """ + A list of edges. + """ + edges: [PinnedIssueEdge] + + """ + A list of nodes. + """ + nodes: [PinnedIssue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnedIssueEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnedIssue +} + +""" +An ISO-8601 encoded UTC date string with millisecond precision. +""" +scalar PreciseDateTime + +""" +Audit log entry for a private_repository_forking.disable event. +""" +type PrivateRepositoryForkingDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + + """ + The Node ID of the PrivateRepositoryForkingDisableAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a private_repository_forking.enable event. +""" +type PrivateRepositoryForkingEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + + """ + The Node ID of the PrivateRepositoryForkingEnableAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +A curatable list of repositories relating to a repository owner, which defaults +to showing the most popular repositories they own. +""" +type ProfileItemShowcase { + """ + Whether or not the owner has pinned any repositories or gists. + """ + hasPinnedItems: Boolean! + + """ + The repositories and gists in the showcase. If the profile owner has any + pinned items, those will be returned. Otherwise, the profile owner's popular + repositories will be returned. + """ + items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PinnableItemConnection! +} + +""" +Represents any entity on GitHub that has a profile page. +""" +interface ProfileOwner { + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + The public profile email. + """ + email: String + + """ + The Node ID of the ProfileOwner object + """ + id: ID! + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + The public profile location. + """ + location: String + + """ + The username used to login. + """ + login: String! + + """ + The public profile name. + """ + name: String + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + The public profile website URL. + """ + websiteUrl: URI +} + +""" +Projects manage issues, pull requests and notes within a project owner. +""" +type Project implements Closable & Node & Updatable { + """ + The project's description body. + """ + body: String + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The projects description body rendered to HTML. + """ + bodyHTML: HTML! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Indicates if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + List of columns in the project + """ + columns( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectColumnConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The actor who originally created the project. + """ + creator: Actor + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The Node ID of the Project object + """ + id: ID! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The project's name. + """ + name: String! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The project's number. + """ + number: Int! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The project's owner. Currently limited to repositories, organizations, and users. + """ + owner: ProjectOwner! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + List of pending cards in this project + """ + pendingCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Project progress details. + """ + progress: ProjectProgress! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP path for this project + """ + resourcePath: URI! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Whether the project is open or closed. + """ + state: ProjectState! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP URL for this project + """ + url: URI! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! +} + +""" +A card in a project. +""" +type ProjectCard implements Node { + """ + The project column this card is associated under. A card may only belong to one + project column at a time. The column field will be null if the card is created + in a pending state and has yet to be associated with a column. Once cards are + associated with a column, they will not become pending in the future. + """ + column: ProjectColumn + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The card content item + """ + content: ProjectCardItem + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The actor who created this card + """ + creator: Actor + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The Node ID of the ProjectCard object + """ + id: ID! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Whether the card is archived + """ + isArchived: Boolean! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The card note + """ + note: String + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The project that contains this card. + """ + project: Project! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP path for this card + """ + resourcePath: URI! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The state of ProjectCard + """ + state: ProjectCardState + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP URL for this card + """ + url: URI! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) +} + +""" +The possible archived states of a project card. +""" +enum ProjectCardArchivedState { + """ + A project card that is archived + """ + ARCHIVED + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + A project card that is not archived + """ + NOT_ARCHIVED + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) +} + +""" +The connection type for ProjectCard. +""" +type ProjectCardConnection { + """ + A list of edges. + """ + edges: [ProjectCardEdge] + + """ + A list of nodes. + """ + nodes: [ProjectCard] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectCardEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectCard +} + +""" +An issue or PR and its owning repository to be used in a project card. +""" +input ProjectCardImport { + """ + The issue or pull request number. + """ + number: Int! + + """ + Repository name with owner (owner/repository). + """ + repository: String! +} + +""" +Types that can be inside Project Cards. +""" +union ProjectCardItem = Issue | PullRequest + +""" +Various content states of a ProjectCard +""" +enum ProjectCardState { + """ + The card has content only. + """ + CONTENT_ONLY + + """ + The card has a note only. + """ + NOTE_ONLY + + """ + The card is redacted. + """ + REDACTED +} + +""" +A column inside a project. +""" +type ProjectColumn implements Node { + """ + List of cards in the column + """ + cards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The Node ID of the ProjectColumn object + """ + id: ID! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The project column's name. + """ + name: String! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The project that contains this column. + """ + project: Project! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The semantic purpose of the column + """ + purpose: ProjectColumnPurpose + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP path for this project column + """ + resourcePath: URI! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP URL for this project column + """ + url: URI! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) +} + +""" +The connection type for ProjectColumn. +""" +type ProjectColumnConnection { + """ + A list of edges. + """ + edges: [ProjectColumnEdge] + + """ + A list of nodes. + """ + nodes: [ProjectColumn] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectColumnEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectColumn +} + +""" +A project column and a list of its issues and PRs. +""" +input ProjectColumnImport { + """ + The name of the column. + """ + columnName: String! + + """ + A list of issues and pull requests in the column. + """ + issues: [ProjectCardImport!] + + """ + The position of the column, starting from 0. + """ + position: Int! +} + +""" +The semantic purpose of the column - todo, in progress, or done. +""" +enum ProjectColumnPurpose { + """ + The column contains cards which are complete + """ + DONE + + """ + The column contains cards which are currently being worked on + """ + IN_PROGRESS + + """ + The column contains cards still to be worked on + """ + TODO +} + +""" +A list of projects associated with the owner. +""" +type ProjectConnection { + """ + A list of edges. + """ + edges: [ProjectEdge] + + """ + A list of nodes. + """ + nodes: [Project] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Project +} + +""" +Ways in which lists of projects can be ordered upon return. +""" +input ProjectOrder { + """ + The direction in which to order projects by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order projects by. + """ + field: ProjectOrderField! +} + +""" +Properties by which project connections can be ordered. +""" +enum ProjectOrderField { + """ + Order projects by creation time + """ + CREATED_AT + + """ + Order projects by name + """ + NAME + + """ + Order projects by update time + """ + UPDATED_AT +} + +""" +Represents an owner of a Project. +""" +interface ProjectOwner { + """ + The Node ID of the ProjectOwner object + """ + id: ID! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP path listing owners projects + """ + projectsResourcePath: URI! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP URL listing owners projects + """ + projectsUrl: URI! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) +} + +""" +Project progress stats. +""" +type ProjectProgress { + """ + The number of done cards. + """ + doneCount: Int! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The percentage of done cards. + """ + donePercentage: Float! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Whether progress tracking is enabled and cards with purpose exist for this project + """ + enabled: Boolean! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The number of in-progress cards. + """ + inProgressCount: Int! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The percentage of in-progress cards. + """ + inProgressPercentage: Float! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The number of to do cards. + """ + todoCount: Int! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The percentage of to do cards. + """ + todoPercentage: Float! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) +} + +""" +State of the project; either 'open' or 'closed' +""" +enum ProjectState { + """ + The project is closed. + """ + CLOSED + + """ + The project is open. + """ + OPEN +} + +""" +GitHub-provided templates for Projects +""" +enum ProjectTemplate { + """ + Create a board with v2 triggers to automatically move cards across To do, In progress and Done columns. + """ + AUTOMATED_KANBAN_V2 + + """ + Create a board with triggers to automatically move cards across columns with review automation. + """ + AUTOMATED_REVIEWS_KANBAN + + """ + Create a board with columns for To do, In progress and Done. + """ + BASIC_KANBAN + + """ + Create a board to triage and prioritize bugs with To do, priority, and Done columns. + """ + BUG_TRIAGE +} + +""" +New projects that manage issues, pull requests and drafts using tables and boards. +""" +type ProjectV2 implements Closable & Node & Updatable { + """ + Returns true if the project is closed. + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who originally created the project. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "`databaseId` will be removed because it does not support 64-bit signed integer identifiers. Use `fullDatabaseId` instead. Removal on 2025-04-01 UTC." + ) + + """ + A field of the project + """ + field( + """ + The name of the field + """ + name: String! + ): ProjectV2FieldConfiguration + + """ + List of fields and their constraints in the project + """ + fields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 fields returned from the connection + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConfigurationConnection! + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The Node ID of the ProjectV2 object + """ + id: ID! + + """ + List of items in the project + """ + items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 items returned from the connection + """ + orderBy: ProjectV2ItemOrder = {field: POSITION, direction: ASC} + ): ProjectV2ItemConnection! + + """ + The project's number. + """ + number: Int! + + """ + The project's owner. Currently limited to organizations and users. + """ + owner: ProjectV2Owner! + + """ + Returns true if the project is public. + """ + public: Boolean! + + """ + The project's readme. + """ + readme: String + + """ + The repositories the project is linked to. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder = {field: CREATED_AT, direction: DESC} + ): RepositoryConnection! + + """ + The HTTP path for this project + """ + resourcePath: URI! + + """ + The project's short description. + """ + shortDescription: String + + """ + List of the status updates in the project. + """ + statusUpdates( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: ProjectV2StatusOrder = {field: CREATED_AT, direction: DESC} + ): ProjectV2StatusUpdateConnection! + + """ + The teams the project is linked to. + """ + teams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for teams returned from this connection. + """ + orderBy: TeamOrder = {field: NAME, direction: ASC} + ): TeamConnection! + + """ + Returns true if this project is a template. + """ + template: Boolean! + + """ + The project's name. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this project + """ + url: URI! + + """ + A view of the project + """ + view( + """ + The number of a view belonging to the project + """ + number: Int! + ): ProjectV2View + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + List of views in the project + """ + views( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 views returned from the connection + """ + orderBy: ProjectV2ViewOrder = {field: POSITION, direction: ASC} + ): ProjectV2ViewConnection! + + """ + A workflow of the project + """ + workflow( + """ + The number of a workflow belonging to the project + """ + number: Int! + ): ProjectV2Workflow + + """ + List of the workflows in the project + """ + workflows( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 workflows returned from the connection + """ + orderBy: ProjectV2WorkflowOrder = {field: NAME, direction: ASC} + ): ProjectV2WorkflowConnection! +} + +""" +Possible collaborators for a project. +""" +union ProjectV2Actor = Team | User + +""" +The connection type for ProjectV2Actor. +""" +type ProjectV2ActorConnection { + """ + A list of edges. + """ + edges: [ProjectV2ActorEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2Actor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2ActorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2Actor +} + +""" +A collaborator to update on a project. Only one of the userId or teamId should be provided. +""" +input ProjectV2Collaborator { + """ + The role to grant the collaborator + """ + role: ProjectV2Roles! + + """ + The ID of the team as a collaborator. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + The ID of the user as a collaborator. + """ + userId: ID @possibleTypes(concreteTypes: ["User"]) +} + +""" +The connection type for ProjectV2. +""" +type ProjectV2Connection { + """ + A list of edges. + """ + edges: [ProjectV2Edge] + + """ + A list of nodes. + """ + nodes: [ProjectV2] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The type of a project field. +""" +enum ProjectV2CustomFieldType { + """ + Date + """ + DATE + + """ + Iteration + """ + ITERATION + + """ + Number + """ + NUMBER + + """ + Single Select + """ + SINGLE_SELECT + + """ + Text + """ + TEXT +} + +""" +An edge in a connection. +""" +type ProjectV2Edge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2 +} + +""" +A field inside a project. +""" +type ProjectV2Field implements Node & ProjectV2FieldCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The field's type. + """ + dataType: ProjectV2FieldType! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the ProjectV2Field object + """ + id: ID! + + """ + The project field's name. + """ + name: String! + + """ + The project that contains this field. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Common fields across different project field types +""" +interface ProjectV2FieldCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The field's type. + """ + dataType: ProjectV2FieldType! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the ProjectV2FieldCommon object + """ + id: ID! + + """ + The project field's name. + """ + name: String! + + """ + The project that contains this field. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Configurations for project fields. +""" +union ProjectV2FieldConfiguration = ProjectV2Field | ProjectV2IterationField | ProjectV2SingleSelectField + +""" +The connection type for ProjectV2FieldConfiguration. +""" +type ProjectV2FieldConfigurationConnection { + """ + A list of edges. + """ + edges: [ProjectV2FieldConfigurationEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2FieldConfiguration] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2FieldConfigurationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2FieldConfiguration +} + +""" +The connection type for ProjectV2Field. +""" +type ProjectV2FieldConnection { + """ + A list of edges. + """ + edges: [ProjectV2FieldEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2Field] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2FieldEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2Field +} + +""" +Ordering options for project v2 field connections +""" +input ProjectV2FieldOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 fields by. + """ + field: ProjectV2FieldOrderField! +} + +""" +Properties by which project v2 field connections can be ordered. +""" +enum ProjectV2FieldOrderField { + """ + Order project v2 fields by creation time + """ + CREATED_AT + + """ + Order project v2 fields by name + """ + NAME + + """ + Order project v2 fields by position + """ + POSITION +} + +""" +The type of a project field. +""" +enum ProjectV2FieldType { + """ + Assignees + """ + ASSIGNEES + + """ + Date + """ + DATE + + """ + Issue type + """ + ISSUE_TYPE + + """ + Iteration + """ + ITERATION + + """ + Labels + """ + LABELS + + """ + Linked Pull Requests + """ + LINKED_PULL_REQUESTS + + """ + Milestone + """ + MILESTONE + + """ + Number + """ + NUMBER + + """ + Parent issue + """ + PARENT_ISSUE + + """ + Repository + """ + REPOSITORY + + """ + Reviewers + """ + REVIEWERS + + """ + Single Select + """ + SINGLE_SELECT + + """ + Sub-issues progress + """ + SUB_ISSUES_PROGRESS + + """ + Text + """ + TEXT + + """ + Title + """ + TITLE + + """ + Tracked by + """ + TRACKED_BY + + """ + Tracks + """ + TRACKS +} + +""" +The values that can be used to update a field of an item inside a Project. Only 1 value can be updated at a time. +""" +input ProjectV2FieldValue { + """ + The ISO 8601 date to set on the field. + """ + date: Date + + """ + The id of the iteration to set on the field. + """ + iterationId: String + + """ + The number to set on the field. + """ + number: Float + + """ + The id of the single select option to set on the field. + """ + singleSelectOptionId: String + + """ + The text to set on the field. + """ + text: String +} + +""" +Ways in which to filter lists of projects. +""" +input ProjectV2Filters { + """ + List project v2 filtered by the state given. + """ + state: ProjectV2State +} + +""" +An item within a Project. +""" +type ProjectV2Item implements Node { + """ + The content of the referenced draft issue, issue, or pull request + """ + content: ProjectV2ItemContent + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "`databaseId` will be removed because it does not support 64-bit signed integer identifiers. Use `fullDatabaseId` instead. Removal on 2025-04-01 UTC." + ) + + """ + The field value of the first project field which matches the 'name' argument that is set on the item. + """ + fieldValueByName( + """ + The name of the field to return the field value of + """ + name: String! + ): ProjectV2ItemFieldValue + + """ + The field values that are set on the item. + """ + fieldValues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for project v2 item field values returned from the connection + """ + orderBy: ProjectV2ItemFieldValueOrder = {field: POSITION, direction: ASC} + ): ProjectV2ItemFieldValueConnection! + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The Node ID of the ProjectV2Item object + """ + id: ID! + + """ + Whether the item is archived. + """ + isArchived: Boolean! + + """ + The project that contains this item. + """ + project: ProjectV2! + + """ + The type of the item. + """ + type: ProjectV2ItemType! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for ProjectV2Item. +""" +type ProjectV2ItemConnection { + """ + A list of edges. + """ + edges: [ProjectV2ItemEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2Item] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Types that can be inside Project Items. +""" +union ProjectV2ItemContent = DraftIssue | Issue | PullRequest + +""" +An edge in a connection. +""" +type ProjectV2ItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2Item +} + +""" +The value of a date field in a Project item. +""" +type ProjectV2ItemFieldDateValue implements Node & ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Date value for the field + """ + date: Date + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The Node ID of the ProjectV2ItemFieldDateValue object + """ + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of an iteration field in a Project item. +""" +type ProjectV2ItemFieldIterationValue implements Node & ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The duration of the iteration in days. + """ + duration: Int! + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The Node ID of the ProjectV2ItemFieldIterationValue object + """ + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + The ID of the iteration. + """ + iterationId: String! + + """ + The start date of the iteration. + """ + startDate: Date! + + """ + The title of the iteration. + """ + title: String! + + """ + The title of the iteration, with HTML. + """ + titleHTML: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of the labels field in a Project item. +""" +type ProjectV2ItemFieldLabelValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + Labels value of a field + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): LabelConnection +} + +""" +The value of a milestone field in a Project item. +""" +type ProjectV2ItemFieldMilestoneValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + Milestone value of a field + """ + milestone: Milestone +} + +""" +The value of a number field in a Project item. +""" +type ProjectV2ItemFieldNumberValue implements Node & ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The Node ID of the ProjectV2ItemFieldNumberValue object + """ + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + Number as a float(8) + """ + number: Float + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of a pull request field in a Project item. +""" +type ProjectV2ItemFieldPullRequestValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The pull requests for this field + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests. + """ + orderBy: PullRequestOrder = {field: CREATED_AT, direction: ASC} + ): PullRequestConnection +} + +""" +The value of a repository field in a Project item. +""" +type ProjectV2ItemFieldRepositoryValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The repository for this field. + """ + repository: Repository +} + +""" +The value of a reviewers field in a Project item. +""" +type ProjectV2ItemFieldReviewerValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The reviewers for this field. + """ + reviewers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RequestedReviewerConnection +} + +""" +The value of a single select field in a Project item. +""" +type ProjectV2ItemFieldSingleSelectValue implements Node & ProjectV2ItemFieldValueCommon { + """ + The color applied to the selected single-select option. + """ + color: ProjectV2SingleSelectFieldOptionColor! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + A plain-text description of the selected single-select option, such as what the option means. + """ + description: String + + """ + The description of the selected single-select option, including HTML tags. + """ + descriptionHTML: String + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The Node ID of the ProjectV2ItemFieldSingleSelectValue object + """ + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + The name of the selected single select option. + """ + name: String + + """ + The html name of the selected single select option. + """ + nameHTML: String + + """ + The id of the selected single select option. + """ + optionId: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of a text field in a Project item. +""" +type ProjectV2ItemFieldTextValue implements Node & ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The Node ID of the ProjectV2ItemFieldTextValue object + """ + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + Text value of a field + """ + text: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The value of a user field in a Project item. +""" +type ProjectV2ItemFieldUserValue { + """ + The field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The users for this field + """ + users( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection +} + +""" +Project field values +""" +union ProjectV2ItemFieldValue = + ProjectV2ItemFieldDateValue + | ProjectV2ItemFieldIterationValue + | ProjectV2ItemFieldLabelValue + | ProjectV2ItemFieldMilestoneValue + | ProjectV2ItemFieldNumberValue + | ProjectV2ItemFieldPullRequestValue + | ProjectV2ItemFieldRepositoryValue + | ProjectV2ItemFieldReviewerValue + | ProjectV2ItemFieldSingleSelectValue + | ProjectV2ItemFieldTextValue + | ProjectV2ItemFieldUserValue + +""" +Common fields across different project field value types +""" +interface ProjectV2ItemFieldValueCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the item. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The project field that contains this value. + """ + field: ProjectV2FieldConfiguration! + + """ + The Node ID of the ProjectV2ItemFieldValueCommon object + """ + id: ID! + + """ + The project item that contains this value. + """ + item: ProjectV2Item! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for ProjectV2ItemFieldValue. +""" +type ProjectV2ItemFieldValueConnection { + """ + A list of edges. + """ + edges: [ProjectV2ItemFieldValueEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2ItemFieldValue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2ItemFieldValueEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2ItemFieldValue +} + +""" +Ordering options for project v2 item field value connections +""" +input ProjectV2ItemFieldValueOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 item field values by. + """ + field: ProjectV2ItemFieldValueOrderField! +} + +""" +Properties by which project v2 item field value connections can be ordered. +""" +enum ProjectV2ItemFieldValueOrderField { + """ + Order project v2 item field values by the their position in the project + """ + POSITION +} + +""" +Ordering options for project v2 item connections +""" +input ProjectV2ItemOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 items by. + """ + field: ProjectV2ItemOrderField! +} + +""" +Properties by which project v2 item connections can be ordered. +""" +enum ProjectV2ItemOrderField { + """ + Order project v2 items by the their position in the project + """ + POSITION +} + +""" +The type of a project item. +""" +enum ProjectV2ItemType { + """ + Draft Issue + """ + DRAFT_ISSUE + + """ + Issue + """ + ISSUE + + """ + Pull Request + """ + PULL_REQUEST + + """ + Redacted Item + """ + REDACTED +} + +""" +Represents an iteration +""" +input ProjectV2Iteration { + """ + The duration of the iteration, in days. + """ + duration: Int! + + """ + The start date for the iteration. + """ + startDate: Date! + + """ + The title for the iteration. + """ + title: String! +} + +""" +An iteration field inside a project. +""" +type ProjectV2IterationField implements Node & ProjectV2FieldCommon { + """ + Iteration configuration settings + """ + configuration: ProjectV2IterationFieldConfiguration! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The field's type. + """ + dataType: ProjectV2FieldType! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the ProjectV2IterationField object + """ + id: ID! + + """ + The project field's name. + """ + name: String! + + """ + The project that contains this field. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Iteration field configuration for a project. +""" +type ProjectV2IterationFieldConfiguration { + """ + The iteration's completed iterations + """ + completedIterations: [ProjectV2IterationFieldIteration!]! + + """ + The iteration's duration in days + """ + duration: Int! + + """ + The iteration's iterations + """ + iterations: [ProjectV2IterationFieldIteration!]! + + """ + The iteration's start day of the week + """ + startDay: Int! +} + +""" +Represents an iteration field configuration. +""" +input ProjectV2IterationFieldConfigurationInput { + """ + The duration of each iteration, in days. + """ + duration: Int! + + """ + Zero or more iterations for the field. + """ + iterations: [ProjectV2Iteration!]! + + """ + The start date for the first iteration. + """ + startDate: Date! +} + +""" +Iteration field iteration settings for a project. +""" +type ProjectV2IterationFieldIteration { + """ + The iteration's duration in days + """ + duration: Int! + + """ + The iteration's ID. + """ + id: String! + + """ + The iteration's start date + """ + startDate: Date! + + """ + The iteration's title. + """ + title: String! + + """ + The iteration's html title. + """ + titleHTML: String! +} + +""" +Ways in which lists of projects can be ordered upon return. +""" +input ProjectV2Order { + """ + The direction in which to order projects by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order projects by. + """ + field: ProjectV2OrderField! +} + +""" +Properties by which projects can be ordered. +""" +enum ProjectV2OrderField { + """ + The project's date and time of creation + """ + CREATED_AT + + """ + The project's number + """ + NUMBER + + """ + The project's title + """ + TITLE + + """ + The project's date and time of update + """ + UPDATED_AT +} + +""" +Represents an owner of a project. +""" +interface ProjectV2Owner { + """ + The Node ID of the ProjectV2Owner object + """ + id: ID! + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter projects based on user role. + """ + minPermissionLevel: ProjectV2PermissionLevel = READ + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the owner. + """ + query: String + ): ProjectV2Connection! +} + +""" +The possible roles of a collaborator on a project. +""" +enum ProjectV2PermissionLevel { + """ + The collaborator can view, edit, and maange the settings of the project + """ + ADMIN + + """ + The collaborator can view the project + """ + READ + + """ + The collaborator can view and edit the project + """ + WRITE +} + +""" +Recent projects for the owner. +""" +interface ProjectV2Recent { + """ + Recent projects that this user has modified in the context of the owner. + """ + recentProjects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! +} + +""" +The possible roles of a collaborator on a project. +""" +enum ProjectV2Roles { + """ + The collaborator can view, edit, and maange the settings of the project + """ + ADMIN + + """ + The collaborator has no direct access to the project + """ + NONE + + """ + The collaborator can view the project + """ + READER + + """ + The collaborator can view and edit the project + """ + WRITER +} + +""" +A single select field inside a project. +""" +type ProjectV2SingleSelectField implements Node & ProjectV2FieldCommon { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The field's type. + """ + dataType: ProjectV2FieldType! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the ProjectV2SingleSelectField object + """ + id: ID! + + """ + The project field's name. + """ + name: String! + + """ + Options for the single select field + """ + options( + """ + Filter returned options to only those matching these names, case insensitive. + """ + names: [String!] + ): [ProjectV2SingleSelectFieldOption!]! + + """ + The project that contains this field. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +Single select field option for a configuration for a project. +""" +type ProjectV2SingleSelectFieldOption { + """ + The option's display color. + """ + color: ProjectV2SingleSelectFieldOptionColor! + + """ + The option's plain-text description. + """ + description: String! + + """ + The option's description, possibly containing HTML. + """ + descriptionHTML: String! + + """ + The option's ID. + """ + id: String! + + """ + The option's name. + """ + name: String! + + """ + The option's html name. + """ + nameHTML: String! +} + +""" +The display color of a single-select field option. +""" +enum ProjectV2SingleSelectFieldOptionColor { + """ + BLUE + """ + BLUE + + """ + GRAY + """ + GRAY + + """ + GREEN + """ + GREEN + + """ + ORANGE + """ + ORANGE + + """ + PINK + """ + PINK + + """ + PURPLE + """ + PURPLE + + """ + RED + """ + RED + + """ + YELLOW + """ + YELLOW +} + +""" +Represents a single select field option +""" +input ProjectV2SingleSelectFieldOptionInput { + """ + The display color of the option + """ + color: ProjectV2SingleSelectFieldOptionColor! + + """ + The description text of the option + """ + description: String! + + """ + The name of the option + """ + name: String! +} + +""" +Represents a sort by field and direction. +""" +type ProjectV2SortBy { + """ + The direction of the sorting. Possible values are ASC and DESC. + """ + direction: OrderDirection! + + """ + The field by which items are sorted. + """ + field: ProjectV2Field! +} + +""" +The connection type for ProjectV2SortBy. +""" +type ProjectV2SortByConnection { + """ + A list of edges. + """ + edges: [ProjectV2SortByEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2SortBy] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2SortByEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2SortBy +} + +""" +Represents a sort by field and direction. +""" +type ProjectV2SortByField { + """ + The direction of the sorting. Possible values are ASC and DESC. + """ + direction: OrderDirection! + + """ + The field by which items are sorted. + """ + field: ProjectV2FieldConfiguration! +} + +""" +The connection type for ProjectV2SortByField. +""" +type ProjectV2SortByFieldConnection { + """ + A list of edges. + """ + edges: [ProjectV2SortByFieldEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2SortByField] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2SortByFieldEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2SortByField +} + +""" +The possible states of a project v2. +""" +enum ProjectV2State { + """ + A project v2 that has been closed + """ + CLOSED + + """ + A project v2 that is still open + """ + OPEN +} + +""" +Ways in which project v2 status updates can be ordered. +""" +input ProjectV2StatusOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field by which to order nodes. + """ + field: ProjectV2StatusUpdateOrderField! +} + +""" +A status update within a project. +""" +type ProjectV2StatusUpdate implements Node { + """ + The body of the status update. + """ + body: String + + """ + The body of the status update rendered to HTML. + """ + bodyHTML: HTML + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created the status update. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "`databaseId` will be removed because it does not support 64-bit signed integer identifiers. Use `fullDatabaseId` instead. Removal on 2025-04-01 UTC." + ) + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The Node ID of the ProjectV2StatusUpdate object + """ + id: ID! + + """ + The project that contains this status update. + """ + project: ProjectV2! + + """ + The start date of the status update. + """ + startDate: Date + + """ + The status of the status update. + """ + status: ProjectV2StatusUpdateStatus + + """ + The target date of the status update. + """ + targetDate: Date + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for ProjectV2StatusUpdate. +""" +type ProjectV2StatusUpdateConnection { + """ + A list of edges. + """ + edges: [ProjectV2StatusUpdateEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2StatusUpdate] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2StatusUpdateEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2StatusUpdate +} + +""" +Properties by which project v2 status updates can be ordered. +""" +enum ProjectV2StatusUpdateOrderField { + """ + Allows chronological ordering of project v2 status updates. + """ + CREATED_AT +} + +""" +The possible statuses of a project v2. +""" +enum ProjectV2StatusUpdateStatus { + """ + A project v2 that is at risk and encountering some challenges. + """ + AT_RISK + + """ + A project v2 that is complete. + """ + COMPLETE + + """ + A project v2 that is inactive. + """ + INACTIVE + + """ + A project v2 that is off track and needs attention. + """ + OFF_TRACK + + """ + A project v2 that is on track with no risks. + """ + ON_TRACK +} + +""" +A view within a ProjectV2. +""" +type ProjectV2View implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "`databaseId` will be removed because it does not support 64-bit signed integer identifiers. Use `fullDatabaseId` instead. Removal on 2025-04-01 UTC." + ) + + """ + The view's visible fields. + """ + fields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConfigurationConnection + + """ + The project view's filter. + """ + filter: String + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The view's group-by field. + """ + groupBy( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConnection + @deprecated( + reason: "The `ProjectV2View#order_by` API is deprecated in favour of the more capable `ProjectV2View#group_by_field` API. Check out the `ProjectV2View#group_by_fields` API as an example for the more capable alternative. Removal on 2023-04-01 UTC." + ) + + """ + The view's group-by field. + """ + groupByFields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConfigurationConnection + + """ + The Node ID of the ProjectV2View object + """ + id: ID! + + """ + The project view's layout. + """ + layout: ProjectV2ViewLayout! + + """ + The project view's name. + """ + name: String! + + """ + The project view's number. + """ + number: Int! + + """ + The project that contains this view. + """ + project: ProjectV2! + + """ + The view's sort-by config. + """ + sortBy( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2SortByConnection + @deprecated( + reason: "The `ProjectV2View#sort_by` API is deprecated in favour of the more capable `ProjectV2View#sort_by_fields` API. Check out the `ProjectV2View#sort_by_fields` API as an example for the more capable alternative. Removal on 2023-04-01 UTC." + ) + + """ + The view's sort-by config. + """ + sortByFields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2SortByFieldConnection + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The view's vertical-group-by field. + """ + verticalGroupBy( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConnection + @deprecated( + reason: "The `ProjectV2View#vertical_group_by` API is deprecated in favour of the more capable `ProjectV2View#vertical_group_by_fields` API. Check out the `ProjectV2View#vertical_group_by_fields` API as an example for the more capable alternative. Removal on 2023-04-01 UTC." + ) + + """ + The view's vertical-group-by field. + """ + verticalGroupByFields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConfigurationConnection + + """ + The view's visible fields. + """ + visibleFields( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the project v2 fields returned from the connection. + """ + orderBy: ProjectV2FieldOrder = {field: POSITION, direction: ASC} + ): ProjectV2FieldConnection + @deprecated( + reason: "The `ProjectV2View#visibleFields` API is deprecated in favour of the more capable `ProjectV2View#fields` API. Check out the `ProjectV2View#fields` API as an example for the more capable alternative. Removal on 2023-01-01 UTC." + ) +} + +""" +The connection type for ProjectV2View. +""" +type ProjectV2ViewConnection { + """ + A list of edges. + """ + edges: [ProjectV2ViewEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2View] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2ViewEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2View +} + +""" +The layout of a project v2 view. +""" +enum ProjectV2ViewLayout { + """ + Board layout + """ + BOARD_LAYOUT + + """ + Roadmap layout + """ + ROADMAP_LAYOUT + + """ + Table layout + """ + TABLE_LAYOUT +} + +""" +Ordering options for project v2 view connections +""" +input ProjectV2ViewOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 views by. + """ + field: ProjectV2ViewOrderField! +} + +""" +Properties by which project v2 view connections can be ordered. +""" +enum ProjectV2ViewOrderField { + """ + Order project v2 views by creation time + """ + CREATED_AT + + """ + Order project v2 views by name + """ + NAME + + """ + Order project v2 views by position + """ + POSITION +} + +""" +A workflow inside a project. +""" +type ProjectV2Workflow implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "`databaseId` will be removed because it does not support 64-bit signed integer identifiers. Use `fullDatabaseId` instead. Removal on 2025-04-01 UTC." + ) + + """ + Whether the workflow is enabled. + """ + enabled: Boolean! + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The Node ID of the ProjectV2Workflow object + """ + id: ID! + + """ + The name of the workflow. + """ + name: String! + + """ + The number of the workflow. + """ + number: Int! + + """ + The project that contains this workflow. + """ + project: ProjectV2! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for ProjectV2Workflow. +""" +type ProjectV2WorkflowConnection { + """ + A list of edges. + """ + edges: [ProjectV2WorkflowEdge] + + """ + A list of nodes. + """ + nodes: [ProjectV2Workflow] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectV2WorkflowEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectV2Workflow +} + +""" +Ordering options for project v2 workflows connections +""" +input ProjectV2WorkflowOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order the project v2 workflows by. + """ + field: ProjectV2WorkflowsOrderField! +} + +""" +Properties by which project workflows can be ordered. +""" +enum ProjectV2WorkflowsOrderField { + """ + The date and time of the workflow creation + """ + CREATED_AT + + """ + The name of the workflow + """ + NAME + + """ + The number of the workflow + """ + NUMBER + + """ + The date and time of the workflow update + """ + UPDATED_AT +} + +""" +A property that must match +""" +type PropertyTargetDefinition { + """ + The name of the property + """ + name: String! + + """ + The values to match for + """ + propertyValues: [String!]! + + """ + The source of the property. Choose 'custom' or 'system'. Defaults to 'custom' if not specified + """ + source: String +} + +""" +A property that must match +""" +input PropertyTargetDefinitionInput { + """ + The name of the property + """ + name: String! + + """ + The values to match for + """ + propertyValues: [String!]! + + """ + The source of the property. Choose 'custom' or 'system'. Defaults to 'custom' if not specified + """ + source: String +} + +""" +A user's public key. +""" +type PublicKey implements Node { + """ + The last time this authorization was used to perform an action. Values will be null for keys not owned by the user. + """ + accessedAt: DateTime + + """ + Identifies the date and time when the key was created. Keys created before + March 5th, 2014 have inaccurate values. Values will be null for keys not owned by the user. + """ + createdAt: DateTime + + """ + The fingerprint for this PublicKey. + """ + fingerprint: String! + + """ + The Node ID of the PublicKey object + """ + id: ID! + + """ + Whether this PublicKey is read-only or not. Values will be null for keys not owned by the user. + """ + isReadOnly: Boolean + + """ + The public key string. + """ + key: String! + + """ + Identifies the date and time when the key was updated. Keys created before + March 5th, 2014 may have inaccurate values. Values will be null for keys not + owned by the user. + """ + updatedAt: DateTime +} + +""" +The connection type for PublicKey. +""" +type PublicKeyConnection { + """ + A list of edges. + """ + edges: [PublicKeyEdge] + + """ + A list of nodes. + """ + nodes: [PublicKey] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PublicKeyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PublicKey +} + +""" +Autogenerated input type of PublishSponsorsTier +""" +input PublishSponsorsTierInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the draft tier to publish. + """ + tierId: ID! @possibleTypes(concreteTypes: ["SponsorsTier"]) +} + +""" +Autogenerated return type of PublishSponsorsTier. +""" +type PublishSponsorsTierPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The tier that was published. + """ + sponsorsTier: SponsorsTier +} + +""" +A repository pull request. +""" +type PullRequest implements Assignable & Closable & Comment & Labelable & Lockable & Node & ProjectV2Owner & Reactable & RepositoryNode & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + The number of additions in this pull request. + """ + additions: Int! + + """ + A list of actors assigned to this object. + """ + assignedActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): AssigneeConnection! + + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Returns the auto-merge request object if one exists for this pull request. + """ + autoMergeRequest: AutoMergeRequest + + """ + Identifies the base Ref associated with the pull request. + """ + baseRef: Ref + + """ + Identifies the name of the base Ref associated with the pull request, even if the ref has been deleted. + """ + baseRefName: String! + + """ + Identifies the oid of the base ref associated with the pull request, even if the ref has been deleted. + """ + baseRefOid: GitObjectID! + + """ + The repository associated with this pull request's base Ref. + """ + baseRepository: Repository + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Whether or not the pull request is rebaseable. + """ + canBeRebased: Boolean! + + """ + The number of changed files in this pull request. + """ + changedFiles: Int! + + """ + The HTTP path for the checks of this pull request. + """ + checksResourcePath: URI! + + """ + The HTTP URL for the checks of this pull request. + """ + checksUrl: URI! + + """ + `true` if the pull request is closed + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + List of issues that may be closed by this pull request + """ + closingIssuesReferences( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection + """ + orderBy: IssueOrder + + """ + Return only manually linked Issues + """ + userLinkedOnly: Boolean = false + ): IssueConnection + + """ + A list of comments associated with the pull request. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + A list of commits present in this pull request's head branch not present in the base branch. + """ + commits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestCommitConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "`databaseId` will be removed because it does not support 64-bit signed integer identifiers. Use `fullDatabaseId` instead. Removal on 2024-07-01 UTC." + ) + + """ + The number of deletions in this pull request. + """ + deletions: Int! + + """ + The actor who edited this pull request's body. + """ + editor: Actor + + """ + Lists the files changed within this pull request. + """ + files( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestChangedFileConnection + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + Identifies the head Ref associated with the pull request. + """ + headRef: Ref + + """ + Identifies the name of the head Ref associated with the pull request, even if the ref has been deleted. + """ + headRefName: String! + + """ + Identifies the oid of the head ref associated with the pull request, even if the ref has been deleted. + """ + headRefOid: GitObjectID! + + """ + The repository associated with this pull request's head Ref. + """ + headRepository: Repository + + """ + The owner of the repository associated with this pull request's head Ref. + """ + headRepositoryOwner: RepositoryOwner + + """ + The hovercard information for this issue + """ + hovercard( + """ + Whether or not to include notification contexts + """ + includeNotificationContexts: Boolean = true + ): Hovercard! + + """ + The Node ID of the PullRequest object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The head and base repositories are different. + """ + isCrossRepository: Boolean! + + """ + Identifies if the pull request is a draft. + """ + isDraft: Boolean! + + """ + Indicates whether the pull request is in a merge queue + """ + isInMergeQueue: Boolean! + + """ + Indicates whether the pull request's base ref has a merge queue enabled. + """ + isMergeQueueEnabled: Boolean! + + """ + Is this pull request read by the viewer + """ + isReadByViewer: Boolean + + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + A list of latest reviews per user associated with the pull request. + """ + latestOpinionatedReviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Only return reviews from user who have write access to the repository + """ + writersOnly: Boolean = false + ): PullRequestReviewConnection + + """ + A list of latest reviews per user associated with the pull request that are not also pending review. + """ + latestReviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewConnection + + """ + `true` if the pull request is locked + """ + locked: Boolean! + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean! + + """ + The commit that was created when this pull request was merged. + """ + mergeCommit: Commit + + """ + The merge queue for the pull request's base branch + """ + mergeQueue: MergeQueue + + """ + The merge queue entry of the pull request in the base branch's merge queue + """ + mergeQueueEntry: MergeQueueEntry + + """ + Detailed information about the current pull request merge state status. + """ + mergeStateStatus: MergeStateStatus! + + """ + Whether or not the pull request can be merged based on the existence of merge conflicts. + """ + mergeable: MergeableState! + + """ + Whether or not the pull request was merged. + """ + merged: Boolean! + + """ + The date and time that the pull request was merged. + """ + mergedAt: DateTime + + """ + The actor who merged the pull request. + """ + mergedBy: Actor + + """ + Identifies the milestone associated with the pull request. + """ + milestone: Milestone + + """ + Identifies the pull request number. + """ + number: Int! + + """ + A list of Users that are participating in the Pull Request conversation. + """ + participants( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The permalink to the pull request. + """ + permalink: URI! + + """ + The commit that GitHub automatically generated to test if this pull request + could be merged. This field will not return a value if the pull request is + merged, or if the test merge commit is still being generated. See the + `mergeable` field for more details on the mergeability of the pull request. + """ + potentialMergeCommit: Commit + + """ + List of project cards associated with this pull request. + """ + projectCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + List of project items associated with this pull request. + """ + projectItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Include archived items. + """ + includeArchived: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ItemConnection! + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter projects based on user role. + """ + minPermissionLevel: ProjectV2PermissionLevel = READ + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the owner. + """ + query: String + ): ProjectV2Connection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this pull request. + """ + resourcePath: URI! + + """ + The HTTP path for reverting this pull request. + """ + revertResourcePath: URI! + + """ + The HTTP URL for reverting this pull request. + """ + revertUrl: URI! + + """ + The current status of this pull request with respect to code review. + """ + reviewDecision: PullRequestReviewDecision + + """ + A list of review requests associated with the pull request. + """ + reviewRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReviewRequestConnection + + """ + The list of all review threads for this pull request. + """ + reviewThreads( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewThreadConnection! + + """ + A list of reviews associated with the pull request. + """ + reviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter by author of the review. + """ + author: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + A list of states to filter the reviews. + """ + states: [PullRequestReviewState!] + ): PullRequestReviewConnection + + """ + Identifies the state of the pull request. + """ + state: PullRequestState! + + """ + Check and Status rollup information for the PR's head ref. + """ + statusCheckRollup: StatusCheckRollup + + """ + A list of suggested actors to assign to this object + """ + suggestedActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If provided, searches users by login or profile name + """ + query: String + ): AssigneeConnection! + + """ + A list of reviewer suggestions based on commit history and past review comments. + """ + suggestedReviewers: [SuggestedReviewer]! + + """ + A list of events, comments, commits, etc. associated with the pull request. + """ + timeline( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows filtering timeline events by a `since` timestamp. + """ + since: DateTime + ): PullRequestTimelineConnection! + @deprecated(reason: "`timeline` will be removed Use PullRequest.timelineItems instead. Removal on 2020-10-01 UTC.") + + """ + A list of events, comments, commits, etc. associated with the pull request. + """ + timelineItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter timeline items by type. + """ + itemTypes: [PullRequestTimelineItemsItemType!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter timeline items by a `since` timestamp. + """ + since: DateTime + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): PullRequestTimelineItemsConnection! + + """ + Identifies the pull request title. + """ + title: String! + + """ + Identifies the pull request title rendered to HTML. + """ + titleHTML: HTML! + + """ + Returns a count of how many comments this pull request has received. + """ + totalCommentsCount: Int + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this pull request. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Whether or not the viewer can apply suggestion. + """ + viewerCanApplySuggestion: Boolean! + + """ + Indicates if the object can be closed by the viewer. + """ + viewerCanClose: Boolean! + + """ + Check if the viewer can restore the deleted head ref. + """ + viewerCanDeleteHeadRef: Boolean! + + """ + Whether or not the viewer can disable auto-merge + """ + viewerCanDisableAutoMerge: Boolean! + + """ + Can the viewer edit files within this pull request. + """ + viewerCanEditFiles: Boolean! + + """ + Whether or not the viewer can enable auto-merge + """ + viewerCanEnableAutoMerge: Boolean! + + """ + Indicates if the viewer can edit labels for this object. + """ + viewerCanLabel: Boolean! + + """ + Indicates whether the viewer can bypass branch protections and merge the pull request immediately + """ + viewerCanMergeAsAdmin: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Indicates if the object can be reopened by the viewer. + """ + viewerCanReopen: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Whether or not the viewer can update the head ref of this PR, by merging or rebasing the base ref. + If the head ref is up to date or unable to be updated by this user, this will return false. + """ + viewerCanUpdateBranch: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + The latest review given from the viewer. + """ + viewerLatestReview: PullRequestReview + + """ + The person who has requested the viewer for review on this pull request. + """ + viewerLatestReviewRequest: ReviewRequest + + """ + The merge body text for the viewer and method. + """ + viewerMergeBodyText( + """ + The merge method for the message. + """ + mergeType: PullRequestMergeMethod + ): String! + + """ + The merge headline text for the viewer and method. + """ + viewerMergeHeadlineText( + """ + The merge method for the message. + """ + mergeType: PullRequestMergeMethod + ): String! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +Array of allowed merge methods. Allowed values include `merge`, `squash`, and `rebase`. At least one option must be enabled. +""" +enum PullRequestAllowedMergeMethods { + """ + Add all commits from the head branch to the base branch with a merge commit. + """ + MERGE + + """ + Add all commits from the head branch onto the base branch individually. + """ + REBASE + + """ + Combine all commits from the head branch into a single commit in the base branch. + """ + SQUASH +} + +""" +The possible methods for updating a pull request's head branch with the base branch. +""" +enum PullRequestBranchUpdateMethod { + """ + Update branch via merge + """ + MERGE + + """ + Update branch via rebase + """ + REBASE +} + +""" +A file changed in a pull request. +""" +type PullRequestChangedFile { + """ + The number of additions to the file. + """ + additions: Int! + + """ + How the file was changed in this PullRequest + """ + changeType: PatchStatus! + + """ + The number of deletions to the file. + """ + deletions: Int! + + """ + The path of the file. + """ + path: String! + + """ + The state of the file for the viewer. + """ + viewerViewedState: FileViewedState! +} + +""" +The connection type for PullRequestChangedFile. +""" +type PullRequestChangedFileConnection { + """ + A list of edges. + """ + edges: [PullRequestChangedFileEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestChangedFile] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestChangedFileEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestChangedFile +} + +""" +Represents a Git commit part of a pull request. +""" +type PullRequestCommit implements Node & UniformResourceLocatable { + """ + The Git commit object + """ + commit: Commit! + + """ + The Node ID of the PullRequestCommit object + """ + id: ID! + + """ + The pull request this commit belongs to + """ + pullRequest: PullRequest! + + """ + The HTTP path for this pull request commit + """ + resourcePath: URI! + + """ + The HTTP URL for this pull request commit + """ + url: URI! +} + +""" +Represents a commit comment thread part of a pull request. +""" +type PullRequestCommitCommentThread implements Node & RepositoryNode { + """ + The comments that exist in this thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The commit the comments were made on. + """ + commit: Commit! + + """ + The Node ID of the PullRequestCommitCommentThread object + """ + id: ID! + + """ + The file the comments were made on. + """ + path: String + + """ + The position in the diff for the commit that the comment was made on. + """ + position: Int + + """ + The pull request this commit comment thread belongs to + """ + pullRequest: PullRequest! + + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +The connection type for PullRequestCommit. +""" +type PullRequestCommitConnection { + """ + A list of edges. + """ + edges: [PullRequestCommitEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestCommit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestCommitEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestCommit +} + +""" +The connection type for PullRequest. +""" +type PullRequestConnection { + """ + A list of edges. + """ + edges: [PullRequestEdge] + + """ + A list of nodes. + """ + nodes: [PullRequest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates pull requests opened by a user within one repository. +""" +type PullRequestContributionsByRepository { + """ + The pull request contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestContributionConnection! + + """ + The repository in which the pull requests were opened. + """ + repository: Repository! +} + +""" +An edge in a connection. +""" +type PullRequestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequest +} + +""" +Represents available types of methods to use when merging a pull request. +""" +enum PullRequestMergeMethod { + """ + Add all commits from the head branch to the base branch with a merge commit. + """ + MERGE + + """ + Add all commits from the head branch onto the base branch individually. + """ + REBASE + + """ + Combine all commits from the head branch into a single commit in the base branch. + """ + SQUASH +} + +""" +Ways in which lists of issues can be ordered upon return. +""" +input PullRequestOrder { + """ + The direction in which to order pull requests by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order pull requests by. + """ + field: PullRequestOrderField! +} + +""" +Properties by which pull_requests connections can be ordered. +""" +enum PullRequestOrderField { + """ + Order pull_requests by creation time + """ + CREATED_AT + + """ + Order pull_requests by update time + """ + UPDATED_AT +} + +""" +Require all commits be made to a non-target branch and submitted via a pull request before they can be merged. +""" +type PullRequestParameters { + """ + Array of allowed merge methods. Allowed values include `merge`, `squash`, and + `rebase`. At least one option must be enabled. + """ + allowedMergeMethods: [PullRequestAllowedMergeMethods!] + + """ + Automatically request review from Copilot for new pull requests, if the author has access to Copilot code review. + """ + automaticCopilotCodeReviewEnabled: Boolean! + + """ + New, reviewable commits pushed will dismiss previous pull request review approvals. + """ + dismissStaleReviewsOnPush: Boolean! + + """ + Require an approving review in pull requests that modify files that have a designated code owner. + """ + requireCodeOwnerReview: Boolean! + + """ + Whether the most recent reviewable push must be approved by someone other than the person who pushed it. + """ + requireLastPushApproval: Boolean! + + """ + The number of approving reviews that are required before a pull request can be merged. + """ + requiredApprovingReviewCount: Int! + + """ + All conversations on code must be resolved before a pull request can be merged. + """ + requiredReviewThreadResolution: Boolean! +} + +""" +Require all commits be made to a non-target branch and submitted via a pull request before they can be merged. +""" +input PullRequestParametersInput { + """ + Array of allowed merge methods. Allowed values include `merge`, `squash`, and + `rebase`. At least one option must be enabled. + """ + allowedMergeMethods: [PullRequestAllowedMergeMethods!] + + """ + Automatically request review from Copilot for new pull requests, if the author has access to Copilot code review. + """ + automaticCopilotCodeReviewEnabled: Boolean + + """ + New, reviewable commits pushed will dismiss previous pull request review approvals. + """ + dismissStaleReviewsOnPush: Boolean! + + """ + Require an approving review in pull requests that modify files that have a designated code owner. + """ + requireCodeOwnerReview: Boolean! + + """ + Whether the most recent reviewable push must be approved by someone other than the person who pushed it. + """ + requireLastPushApproval: Boolean! + + """ + The number of approving reviews that are required before a pull request can be merged. + """ + requiredApprovingReviewCount: Int! + + """ + All conversations on code must be resolved before a pull request can be merged. + """ + requiredReviewThreadResolution: Boolean! +} + +""" +A review object for a given pull request. +""" +type PullRequestReview implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Indicates whether the author of this review has push access to the repository. + """ + authorCanPushToRepository: Boolean! + + """ + Identifies the pull request review body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body of this review rendered as plain text. + """ + bodyText: String! + + """ + A list of review comments for the current pull request review. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewCommentConnection! + + """ + Identifies the commit associated with this pull request review. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "`databaseId` will be removed because it does not support 64-bit signed integer identifiers. Use `fullDatabaseId` instead. Removal on 2024-07-01 UTC." + ) + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The Node ID of the PullRequestReview object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + A list of teams that this review was made on behalf of. + """ + onBehalfOf( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the pull request associated with this pull request review. + """ + pullRequest: PullRequest! + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this PullRequestReview. + """ + resourcePath: URI! + + """ + Identifies the current state of the pull request review. + """ + state: PullRequestReviewState! + + """ + Identifies when the Pull Request Review was submitted + """ + submittedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this PullRequestReview. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +A review comment associated with a given repository pull request. +""" +type PullRequestReviewComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The comment body of this review comment. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The comment body of this review comment rendered as plain text. + """ + bodyText: String! + + """ + Identifies the commit associated with the comment. + """ + commit: Commit + + """ + Identifies when the comment was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "`databaseId` will be removed because it does not support 64-bit signed integer identifiers. Use `fullDatabaseId` instead. Removal on 2024-07-01 UTC." + ) + + """ + The diff hunk to which the comment applies. + """ + diffHunk: String! + + """ + Identifies when the comment was created in a draft state. + """ + draftedAt: DateTime! + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + Identifies the primary key from the database as a BigInt. + """ + fullDatabaseId: BigInt + + """ + The Node ID of the PullRequestReviewComment object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + The end line number on the file to which the comment applies + """ + line: Int + + """ + Returns why the comment was minimized. One of `abuse`, `off-topic`, + `outdated`, `resolved`, `duplicate` and `spam`. Note that the case and + formatting of these values differs from the inputs to the `MinimizeComment` mutation. + """ + minimizedReason: String + + """ + Identifies the original commit associated with the comment. + """ + originalCommit: Commit + + """ + The end line number on the file to which the comment applied when it was first created + """ + originalLine: Int + + """ + The original line index in the diff to which the comment applies. + """ + originalPosition: Int! + @deprecated(reason: "We are phasing out diff-relative positioning for PR comments Removal on 2023-10-01 UTC.") + + """ + The start line number on the file to which the comment applied when it was first created + """ + originalStartLine: Int + + """ + Identifies when the comment body is outdated + """ + outdated: Boolean! + + """ + The path to which the comment applies. + """ + path: String! + + """ + The line index in the diff to which the comment applies. + """ + position: Int + @deprecated( + reason: "We are phasing out diff-relative positioning for PR comments Use the `line` and `startLine` fields instead, which are file line numbers instead of diff line numbers Removal on 2023-10-01 UTC." + ) + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + The pull request associated with this review comment. + """ + pullRequest: PullRequest! + + """ + The pull request review associated with this review comment. + """ + pullRequestReview: PullRequestReview + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The comment this is a reply to. + """ + replyTo: PullRequestReviewComment + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this review comment. + """ + resourcePath: URI! + + """ + The start line number on the file to which the comment applies + """ + startLine: Int + + """ + Identifies the state of the comment. + """ + state: PullRequestReviewCommentState! + + """ + The level at which the comments in the corresponding thread are targeted, can be a diff line or a file + """ + subjectType: PullRequestReviewThreadSubjectType! + + """ + Identifies when the comment was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this review comment. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for PullRequestReviewComment. +""" +type PullRequestReviewCommentConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewCommentEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReviewComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestReviewCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReviewComment +} + +""" +The possible states of a pull request review comment. +""" +enum PullRequestReviewCommentState { + """ + A comment that is part of a pending review + """ + PENDING + + """ + A comment that is part of a submitted review + """ + SUBMITTED +} + +""" +The connection type for PullRequestReview. +""" +type PullRequestReviewConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReview] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates pull request reviews made by a user within one repository. +""" +type PullRequestReviewContributionsByRepository { + """ + The pull request review contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestReviewContributionConnection! + + """ + The repository in which the pull request reviews were made. + """ + repository: Repository! +} + +""" +The review status of a pull request. +""" +enum PullRequestReviewDecision { + """ + The pull request has received an approving review. + """ + APPROVED + + """ + Changes have been requested on the pull request. + """ + CHANGES_REQUESTED + + """ + A review is required before the pull request can be merged. + """ + REVIEW_REQUIRED +} + +""" +An edge in a connection. +""" +type PullRequestReviewEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReview +} + +""" +The possible events to perform on a pull request review. +""" +enum PullRequestReviewEvent { + """ + Submit feedback and approve merging these changes. + """ + APPROVE + + """ + Submit general feedback without explicit approval. + """ + COMMENT + + """ + Dismiss review so it now longer effects merging. + """ + DISMISS + + """ + Submit feedback that must be addressed before merging. + """ + REQUEST_CHANGES +} + +""" +The possible states of a pull request review. +""" +enum PullRequestReviewState { + """ + A review allowing the pull request to merge. + """ + APPROVED + + """ + A review blocking the pull request from merging. + """ + CHANGES_REQUESTED + + """ + An informational review. + """ + COMMENTED + + """ + A review that has been dismissed. + """ + DISMISSED + + """ + A review that has not yet been submitted. + """ + PENDING +} + +""" +A threaded list of comments for a given pull request. +""" +type PullRequestReviewThread implements Node { + """ + A list of pull request comments associated with the thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): PullRequestReviewCommentConnection! + + """ + The side of the diff on which this thread was placed. + """ + diffSide: DiffSide! + + """ + The Node ID of the PullRequestReviewThread object + """ + id: ID! + + """ + Whether or not the thread has been collapsed (resolved) + """ + isCollapsed: Boolean! + + """ + Indicates whether this thread was outdated by newer changes. + """ + isOutdated: Boolean! + + """ + Whether this thread has been resolved + """ + isResolved: Boolean! + + """ + The line in the file to which this thread refers + """ + line: Int + + """ + The original line in the file to which this thread refers. + """ + originalLine: Int + + """ + The original start line in the file to which this thread refers (multi-line only). + """ + originalStartLine: Int + + """ + Identifies the file path of this thread. + """ + path: String! + + """ + Identifies the pull request associated with this thread. + """ + pullRequest: PullRequest! + + """ + Identifies the repository associated with this thread. + """ + repository: Repository! + + """ + The user who resolved this thread + """ + resolvedBy: User + + """ + The side of the diff that the first line of the thread starts on (multi-line only) + """ + startDiffSide: DiffSide + + """ + The start line in the file to which this thread refers (multi-line only) + """ + startLine: Int + + """ + The level at which the comments in the corresponding thread are targeted, can be a diff line or a file + """ + subjectType: PullRequestReviewThreadSubjectType! + + """ + Indicates whether the current viewer can reply to this thread. + """ + viewerCanReply: Boolean! + + """ + Whether or not the viewer can resolve this thread + """ + viewerCanResolve: Boolean! + + """ + Whether or not the viewer can unresolve this thread + """ + viewerCanUnresolve: Boolean! +} + +""" +Review comment threads for a pull request review. +""" +type PullRequestReviewThreadConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewThreadEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReviewThread] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestReviewThreadEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReviewThread +} + +""" +The possible subject types of a pull request review comment. +""" +enum PullRequestReviewThreadSubjectType { + """ + A comment that has been made against the file of a pull request + """ + FILE + + """ + A comment that has been made against the line of a pull request + """ + LINE +} + +""" +Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. +""" +type PullRequestRevisionMarker { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The last commit the viewer has seen. + """ + lastSeenCommit: Commit! + + """ + The pull request to which the marker belongs. + """ + pullRequest: PullRequest! +} + +""" +The possible states of a pull request. +""" +enum PullRequestState { + """ + A pull request that has been closed without being merged. + """ + CLOSED + + """ + A pull request that has been closed by being merged. + """ + MERGED + + """ + A pull request that is still open. + """ + OPEN +} + +""" +A repository pull request template. +""" +type PullRequestTemplate { + """ + The body of the template + """ + body: String + + """ + The filename of the template + """ + filename: String + + """ + The repository the template belongs to + """ + repository: Repository! +} + +""" +A threaded list of comments for a given pull request. +""" +type PullRequestThread implements Node { + """ + A list of pull request comments associated with the thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): PullRequestReviewCommentConnection! + + """ + The side of the diff on which this thread was placed. + """ + diffSide: DiffSide! + + """ + The Node ID of the PullRequestThread object + """ + id: ID! + + """ + Whether or not the thread has been collapsed (resolved) + """ + isCollapsed: Boolean! + + """ + Indicates whether this thread was outdated by newer changes. + """ + isOutdated: Boolean! + + """ + Whether this thread has been resolved + """ + isResolved: Boolean! + + """ + The line in the file to which this thread refers + """ + line: Int + + """ + Identifies the file path of this thread. + """ + path: String! + + """ + Identifies the pull request associated with this thread. + """ + pullRequest: PullRequest! + + """ + Identifies the repository associated with this thread. + """ + repository: Repository! + + """ + The user who resolved this thread + """ + resolvedBy: User + + """ + The side of the diff that the first line of the thread starts on (multi-line only) + """ + startDiffSide: DiffSide + + """ + The line of the first file diff in the thread. + """ + startLine: Int + + """ + The level at which the comments in the corresponding thread are targeted, can be a diff line or a file + """ + subjectType: PullRequestReviewThreadSubjectType! + + """ + Indicates whether the current viewer can reply to this thread. + """ + viewerCanReply: Boolean! + + """ + Whether or not the viewer can resolve this thread + """ + viewerCanResolve: Boolean! + + """ + Whether or not the viewer can unresolve this thread + """ + viewerCanUnresolve: Boolean! +} + +""" +The connection type for PullRequestTimelineItem. +""" +type PullRequestTimelineConnection { + """ + A list of edges. + """ + edges: [PullRequestTimelineItemEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestTimelineItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An item in a pull request timeline +""" +union PullRequestTimelineItem = + AssignedEvent + | BaseRefDeletedEvent + | BaseRefForcePushedEvent + | ClosedEvent + | Commit + | CommitCommentThread + | CrossReferencedEvent + | DemilestonedEvent + | DeployedEvent + | DeploymentEnvironmentChangedEvent + | HeadRefDeletedEvent + | HeadRefForcePushedEvent + | HeadRefRestoredEvent + | IssueComment + | LabeledEvent + | LockedEvent + | MergedEvent + | MilestonedEvent + | PullRequestReview + | PullRequestReviewComment + | PullRequestReviewThread + | ReferencedEvent + | RenamedTitleEvent + | ReopenedEvent + | ReviewDismissedEvent + | ReviewRequestRemovedEvent + | ReviewRequestedEvent + | SubscribedEvent + | UnassignedEvent + | UnlabeledEvent + | UnlockedEvent + | UnsubscribedEvent + | UserBlockedEvent + +""" +An edge in a connection. +""" +type PullRequestTimelineItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestTimelineItem +} + +""" +An item in a pull request timeline +""" +union PullRequestTimelineItems = + AddedToMergeQueueEvent + | AddedToProjectEvent + | AssignedEvent + | AutoMergeDisabledEvent + | AutoMergeEnabledEvent + | AutoRebaseEnabledEvent + | AutoSquashEnabledEvent + | AutomaticBaseChangeFailedEvent + | AutomaticBaseChangeSucceededEvent + | BaseRefChangedEvent + | BaseRefDeletedEvent + | BaseRefForcePushedEvent + | ClosedEvent + | CommentDeletedEvent + | ConnectedEvent + | ConvertToDraftEvent + | ConvertedNoteToIssueEvent + | ConvertedToDiscussionEvent + | CrossReferencedEvent + | DemilestonedEvent + | DeployedEvent + | DeploymentEnvironmentChangedEvent + | DisconnectedEvent + | HeadRefDeletedEvent + | HeadRefForcePushedEvent + | HeadRefRestoredEvent + | IssueComment + | IssueTypeAddedEvent + | IssueTypeChangedEvent + | IssueTypeRemovedEvent + | LabeledEvent + | LockedEvent + | MarkedAsDuplicateEvent + | MentionedEvent + | MergedEvent + | MilestonedEvent + | MovedColumnsInProjectEvent + | ParentIssueAddedEvent + | ParentIssueRemovedEvent + | PinnedEvent + | PullRequestCommit + | PullRequestCommitCommentThread + | PullRequestReview + | PullRequestReviewThread + | PullRequestRevisionMarker + | ReadyForReviewEvent + | ReferencedEvent + | RemovedFromMergeQueueEvent + | RemovedFromProjectEvent + | RenamedTitleEvent + | ReopenedEvent + | ReviewDismissedEvent + | ReviewRequestRemovedEvent + | ReviewRequestedEvent + | SubIssueAddedEvent + | SubIssueRemovedEvent + | SubscribedEvent + | TransferredEvent + | UnassignedEvent + | UnlabeledEvent + | UnlockedEvent + | UnmarkedAsDuplicateEvent + | UnpinnedEvent + | UnsubscribedEvent + | UserBlockedEvent + +""" +The connection type for PullRequestTimelineItems. +""" +type PullRequestTimelineItemsConnection { + """ + A list of edges. + """ + edges: [PullRequestTimelineItemsEdge] + + """ + Identifies the count of items after applying `before` and `after` filters. + """ + filteredCount: Int! + + """ + A list of nodes. + """ + nodes: [PullRequestTimelineItems] + + """ + Identifies the count of items after applying `before`/`after` filters and `first`/`last`/`skip` slicing. + """ + pageCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the date and time when the timeline was last updated. + """ + updatedAt: DateTime! +} + +""" +An edge in a connection. +""" +type PullRequestTimelineItemsEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestTimelineItems +} + +""" +The possible item types found in a timeline. +""" +enum PullRequestTimelineItemsItemType { + """ + Represents an 'added_to_merge_queue' event on a given pull request. + """ + ADDED_TO_MERGE_QUEUE_EVENT + + """ + Represents a 'added_to_project' event on a given issue or pull request. + """ + ADDED_TO_PROJECT_EVENT + + """ + Represents an 'assigned' event on any assignable object. + """ + ASSIGNED_EVENT + + """ + Represents a 'automatic_base_change_failed' event on a given pull request. + """ + AUTOMATIC_BASE_CHANGE_FAILED_EVENT + + """ + Represents a 'automatic_base_change_succeeded' event on a given pull request. + """ + AUTOMATIC_BASE_CHANGE_SUCCEEDED_EVENT + + """ + Represents a 'auto_merge_disabled' event on a given pull request. + """ + AUTO_MERGE_DISABLED_EVENT + + """ + Represents a 'auto_merge_enabled' event on a given pull request. + """ + AUTO_MERGE_ENABLED_EVENT + + """ + Represents a 'auto_rebase_enabled' event on a given pull request. + """ + AUTO_REBASE_ENABLED_EVENT + + """ + Represents a 'auto_squash_enabled' event on a given pull request. + """ + AUTO_SQUASH_ENABLED_EVENT + + """ + Represents a 'base_ref_changed' event on a given issue or pull request. + """ + BASE_REF_CHANGED_EVENT + + """ + Represents a 'base_ref_deleted' event on a given pull request. + """ + BASE_REF_DELETED_EVENT + + """ + Represents a 'base_ref_force_pushed' event on a given pull request. + """ + BASE_REF_FORCE_PUSHED_EVENT + + """ + Represents a 'closed' event on any `Closable`. + """ + CLOSED_EVENT + + """ + Represents a 'comment_deleted' event on a given issue or pull request. + """ + COMMENT_DELETED_EVENT + + """ + Represents a 'connected' event on a given issue or pull request. + """ + CONNECTED_EVENT + + """ + Represents a 'converted_note_to_issue' event on a given issue or pull request. + """ + CONVERTED_NOTE_TO_ISSUE_EVENT + + """ + Represents a 'converted_to_discussion' event on a given issue. + """ + CONVERTED_TO_DISCUSSION_EVENT + + """ + Represents a 'convert_to_draft' event on a given pull request. + """ + CONVERT_TO_DRAFT_EVENT + + """ + Represents a mention made by one issue or pull request to another. + """ + CROSS_REFERENCED_EVENT + + """ + Represents a 'demilestoned' event on a given issue or pull request. + """ + DEMILESTONED_EVENT + + """ + Represents a 'deployed' event on a given pull request. + """ + DEPLOYED_EVENT + + """ + Represents a 'deployment_environment_changed' event on a given pull request. + """ + DEPLOYMENT_ENVIRONMENT_CHANGED_EVENT + + """ + Represents a 'disconnected' event on a given issue or pull request. + """ + DISCONNECTED_EVENT + + """ + Represents a 'head_ref_deleted' event on a given pull request. + """ + HEAD_REF_DELETED_EVENT + + """ + Represents a 'head_ref_force_pushed' event on a given pull request. + """ + HEAD_REF_FORCE_PUSHED_EVENT + + """ + Represents a 'head_ref_restored' event on a given pull request. + """ + HEAD_REF_RESTORED_EVENT + + """ + Represents a comment on an Issue. + """ + ISSUE_COMMENT + + """ + Represents a 'issue_type_added' event on a given issue. + """ + ISSUE_TYPE_ADDED_EVENT + + """ + Represents a 'issue_type_changed' event on a given issue. + """ + ISSUE_TYPE_CHANGED_EVENT + + """ + Represents a 'issue_type_removed' event on a given issue. + """ + ISSUE_TYPE_REMOVED_EVENT + + """ + Represents a 'labeled' event on a given issue or pull request. + """ + LABELED_EVENT + + """ + Represents a 'locked' event on a given issue or pull request. + """ + LOCKED_EVENT + + """ + Represents a 'marked_as_duplicate' event on a given issue or pull request. + """ + MARKED_AS_DUPLICATE_EVENT + + """ + Represents a 'mentioned' event on a given issue or pull request. + """ + MENTIONED_EVENT + + """ + Represents a 'merged' event on a given pull request. + """ + MERGED_EVENT + + """ + Represents a 'milestoned' event on a given issue or pull request. + """ + MILESTONED_EVENT + + """ + Represents a 'moved_columns_in_project' event on a given issue or pull request. + """ + MOVED_COLUMNS_IN_PROJECT_EVENT + + """ + Represents a 'parent_issue_added' event on a given issue. + """ + PARENT_ISSUE_ADDED_EVENT + + """ + Represents a 'parent_issue_removed' event on a given issue. + """ + PARENT_ISSUE_REMOVED_EVENT + + """ + Represents a 'pinned' event on a given issue or pull request. + """ + PINNED_EVENT + + """ + Represents a Git commit part of a pull request. + """ + PULL_REQUEST_COMMIT + + """ + Represents a commit comment thread part of a pull request. + """ + PULL_REQUEST_COMMIT_COMMENT_THREAD + + """ + A review object for a given pull request. + """ + PULL_REQUEST_REVIEW + + """ + A threaded list of comments for a given pull request. + """ + PULL_REQUEST_REVIEW_THREAD + + """ + Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. + """ + PULL_REQUEST_REVISION_MARKER + + """ + Represents a 'ready_for_review' event on a given pull request. + """ + READY_FOR_REVIEW_EVENT + + """ + Represents a 'referenced' event on a given `ReferencedSubject`. + """ + REFERENCED_EVENT + + """ + Represents a 'removed_from_merge_queue' event on a given pull request. + """ + REMOVED_FROM_MERGE_QUEUE_EVENT + + """ + Represents a 'removed_from_project' event on a given issue or pull request. + """ + REMOVED_FROM_PROJECT_EVENT + + """ + Represents a 'renamed' event on a given issue or pull request + """ + RENAMED_TITLE_EVENT + + """ + Represents a 'reopened' event on any `Closable`. + """ + REOPENED_EVENT + + """ + Represents a 'review_dismissed' event on a given issue or pull request. + """ + REVIEW_DISMISSED_EVENT + + """ + Represents an 'review_requested' event on a given pull request. + """ + REVIEW_REQUESTED_EVENT + + """ + Represents an 'review_request_removed' event on a given pull request. + """ + REVIEW_REQUEST_REMOVED_EVENT + + """ + Represents a 'subscribed' event on a given `Subscribable`. + """ + SUBSCRIBED_EVENT + + """ + Represents a 'sub_issue_added' event on a given issue. + """ + SUB_ISSUE_ADDED_EVENT + + """ + Represents a 'sub_issue_removed' event on a given issue. + """ + SUB_ISSUE_REMOVED_EVENT + + """ + Represents a 'transferred' event on a given issue or pull request. + """ + TRANSFERRED_EVENT + + """ + Represents an 'unassigned' event on any assignable object. + """ + UNASSIGNED_EVENT + + """ + Represents an 'unlabeled' event on a given issue or pull request. + """ + UNLABELED_EVENT + + """ + Represents an 'unlocked' event on a given issue or pull request. + """ + UNLOCKED_EVENT + + """ + Represents an 'unmarked_as_duplicate' event on a given issue or pull request. + """ + UNMARKED_AS_DUPLICATE_EVENT + + """ + Represents an 'unpinned' event on a given issue or pull request. + """ + UNPINNED_EVENT + + """ + Represents an 'unsubscribed' event on a given `Subscribable`. + """ + UNSUBSCRIBED_EVENT + + """ + Represents a 'user_blocked' event on a given user. + """ + USER_BLOCKED_EVENT +} + +""" +The possible target states when updating a pull request. +""" +enum PullRequestUpdateState { + """ + A pull request that has been closed without being merged. + """ + CLOSED + + """ + A pull request that is still open. + """ + OPEN +} + +""" +A Git push. +""" +type Push implements Node { + """ + The Node ID of the Push object + """ + id: ID! + + """ + The SHA after the push + """ + nextSha: GitObjectID + + """ + The permalink for this push. + """ + permalink: URI! + + """ + The SHA before the push + """ + previousSha: GitObjectID + + """ + The actor who pushed + """ + pusher: Actor! + + """ + The repository that was pushed to + """ + repository: Repository! +} + +""" +A team, user, or app who has the ability to push to a protected branch. +""" +type PushAllowance implements Node { + """ + The actor that can push. + """ + actor: PushAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user, team, or app. + """ + branchProtectionRule: BranchProtectionRule + + """ + The Node ID of the PushAllowance object + """ + id: ID! +} + +""" +Types that can be an actor. +""" +union PushAllowanceActor = App | Team | User + +""" +The connection type for PushAllowance. +""" +type PushAllowanceConnection { + """ + A list of edges. + """ + edges: [PushAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [PushAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PushAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PushAllowance +} + +""" +The query root of GitHub's GraphQL interface. +""" +type Query implements Node { + """ + Look up a code of conduct by its key + """ + codeOfConduct( + """ + The code of conduct's key + """ + key: String! + ): CodeOfConduct + + """ + Look up a code of conduct by its key + """ + codesOfConduct: [CodeOfConduct] + + """ + Look up an enterprise by URL slug. + """ + enterprise( + """ + The enterprise invitation token. + """ + invitationToken: String + + """ + The enterprise URL slug. + """ + slug: String! + ): Enterprise + + """ + Look up a pending enterprise administrator invitation by invitee, enterprise and role. + """ + enterpriseAdministratorInvitation( + """ + The slug of the enterprise the user was invited to join. + """ + enterpriseSlug: String! + + """ + The role for the enterprise member invitation. + """ + role: EnterpriseAdministratorRole! + + """ + The login of the user invited to join the enterprise. + """ + userLogin: String! + ): EnterpriseAdministratorInvitation + + """ + Look up a pending enterprise administrator invitation by invitation token. + """ + enterpriseAdministratorInvitationByToken( + """ + The invitation token sent with the invitation email. + """ + invitationToken: String! + ): EnterpriseAdministratorInvitation + + """ + Look up a pending enterprise unaffiliated member invitation by invitee and enterprise. + """ + enterpriseMemberInvitation( + """ + The slug of the enterprise the user was invited to join. + """ + enterpriseSlug: String! + + """ + The login of the user invited to join the enterprise. + """ + userLogin: String! + ): EnterpriseMemberInvitation + + """ + Look up a pending enterprise unaffiliated member invitation by invitation token. + """ + enterpriseMemberInvitationByToken( + """ + The invitation token sent with the invitation email. + """ + invitationToken: String! + ): EnterpriseMemberInvitation + + """ + ID of the object. + """ + id: ID! + + """ + Look up an open source license by its key + """ + license( + """ + The license's downcased SPDX ID + """ + key: String! + ): License + + """ + Return a list of known open source licenses + """ + licenses: [License]! + + """ + Get alphabetically sorted list of Marketplace categories + """ + marketplaceCategories( + """ + Exclude categories with no listings. + """ + excludeEmpty: Boolean + + """ + Returns top level categories only, excluding any subcategories. + """ + excludeSubcategories: Boolean + + """ + Return only the specified categories. + """ + includeCategories: [String!] + ): [MarketplaceCategory!]! + + """ + Look up a Marketplace category by its slug. + """ + marketplaceCategory( + """ + The URL slug of the category. + """ + slug: String! + + """ + Also check topic aliases for the category slug + """ + useTopicAliases: Boolean + ): MarketplaceCategory + + """ + Look up a single Marketplace listing + """ + marketplaceListing( + """ + Select the listing that matches this slug. It's the short name of the listing used in its URL. + """ + slug: String! + ): MarketplaceListing + + """ + Look up Marketplace listings + """ + marketplaceListings( + """ + Select listings that can be administered by the specified user. + """ + adminId: ID + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Select listings visible to the viewer even if they are not approved. If omitted or + false, only approved listings will be returned. + """ + allStates: Boolean + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Select only listings with the given category. + """ + categorySlug: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Select listings for products owned by the specified organization. + """ + organizationId: ID + + """ + Select only listings where the primary category matches the given category slug. + """ + primaryCategoryOnly: Boolean = false + + """ + Select the listings with these slugs, if they are visible to the viewer. + """ + slugs: [String] + + """ + Also check topic aliases for the category slug + """ + useTopicAliases: Boolean + + """ + Select listings to which user has admin access. If omitted, listings visible to the + viewer are returned. + """ + viewerCanAdmin: Boolean + + """ + Select only listings that offer a free trial. + """ + withFreeTrialsOnly: Boolean = false + ): MarketplaceListingConnection! + + """ + Return information about the GitHub instance + """ + meta: GitHubMetadata! + + """ + Fetches an object given its ID. + """ + node( + """ + ID of the object. + """ + id: ID! + ): Node + + """ + Lookup nodes by a list of IDs. + """ + nodes( + """ + The list of node IDs. + """ + ids: [ID!]! + ): [Node]! + + """ + Lookup a organization by login. + """ + organization( + """ + The organization's login. + """ + login: String! + ): Organization + + """ + The client's rate limit information. + """ + rateLimit( + """ + If true, calculate the cost for the query without evaluating it + """ + dryRun: Boolean = false + ): RateLimit + + """ + Workaround for re-exposing the root query object. (Refer to + https://github.com/facebook/relay/issues/112 for more information.) + """ + relay: Query! + + """ + Lookup a given repository by the owner and repository name. + """ + repository( + """ + Follow repository renames. If disabled, a repository referenced by its old name will return an error. + """ + followRenames: Boolean = true + + """ + The name of the repository + """ + name: String! + + """ + The login field of a user or organization + """ + owner: String! + ): Repository + + """ + Lookup a repository owner (ie. either a User or an Organization) by login. + """ + repositoryOwner( + """ + The username to lookup the owner by. + """ + login: String! + ): RepositoryOwner + + """ + Lookup resource by a URL. + """ + resource( + """ + The URL. + """ + url: URI! + ): UniformResourceLocatable + + """ + Perform a search across resources, returning a maximum of 1,000 results. + """ + search( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The search string to look for. GitHub search syntax is supported. For more + information, see "[Searching on + GitHub](https://docs.github.com/search-github/searching-on-github)," + "[Understanding the search syntax](https://docs.github.com/search-github/getting-started-with-searching-on-github/understanding-the-search-syntax)," + and "[Sorting search results](https://docs.github.com/search-github/getting-started-with-searching-on-github/sorting-search-results)." + """ + query: String! + + """ + The types of search items to search within. + """ + type: SearchType! + ): SearchResultItemConnection! + + """ + GitHub Security Advisories + """ + securityAdvisories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + A list of classifications to filter advisories by. + """ + classifications: [SecurityAdvisoryClassification!] + + """ + The EPSS percentage to filter advisories by. + """ + epssPercentage: Float + + """ + The EPSS percentile to filter advisories by. + """ + epssPercentile: Float + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter advisories by identifier, e.g. GHSA or CVE. + """ + identifier: SecurityAdvisoryIdentifierFilter + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityAdvisoryOrder = {field: UPDATED_AT, direction: DESC} + + """ + Filter advisories to those published since a time in the past. + """ + publishedSince: DateTime + + """ + Filter advisories to those updated since a time in the past. + """ + updatedSince: DateTime + ): SecurityAdvisoryConnection! + + """ + Fetch a Security Advisory by its GHSA ID + """ + securityAdvisory( + """ + GitHub Security Advisory ID. + """ + ghsaId: String! + ): SecurityAdvisory + + """ + Software Vulnerabilities documented by GitHub Security Advisories + """ + securityVulnerabilities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + A list of advisory classifications to filter vulnerabilities by. + """ + classifications: [SecurityAdvisoryClassification!] + + """ + An ecosystem to filter vulnerabilities by. + """ + ecosystem: SecurityAdvisoryEcosystem + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityVulnerabilityOrder = {field: UPDATED_AT, direction: DESC} + + """ + A package name to filter vulnerabilities by. + """ + package: String + + """ + A list of severities to filter vulnerabilities by. + """ + severities: [SecurityAdvisorySeverity!] + ): SecurityVulnerabilityConnection! + + """ + Users and organizations who can be sponsored via GitHub Sponsors. + """ + sponsorables( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Optional filter for which dependencies should be checked for sponsorable + owners. Only sponsorable owners of dependencies in this ecosystem will be + included. Used when onlyDependencies = true. + + **Upcoming Change on 2022-07-01 UTC** + **Description:** `dependencyEcosystem` will be removed. Use the ecosystem argument instead. + **Reason:** The type is switching from SecurityAdvisoryEcosystem to DependencyGraphEcosystem. + """ + dependencyEcosystem: SecurityAdvisoryEcosystem + + """ + Optional filter for which dependencies should be checked for sponsorable + owners. Only sponsorable owners of dependencies in this ecosystem will be + included. Used when onlyDependencies = true. + """ + ecosystem: DependencyGraphEcosystem + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Whether only sponsorables who own the viewer's dependencies will be + returned. Must be authenticated to use. Can check an organization instead + for their dependencies owned by sponsorables by passing + orgLoginForDependencies. + """ + onlyDependencies: Boolean = false + + """ + Ordering options for users and organizations returned from the connection. + """ + orderBy: SponsorableOrder = {field: LOGIN, direction: ASC} + + """ + Optional organization username for whose dependencies should be checked. + Used when onlyDependencies = true. Omit to check your own dependencies. If + you are not an administrator of the organization, only dependencies from its + public repositories will be considered. + """ + orgLoginForDependencies: String + ): SponsorableItemConnection! + + """ + Look up a topic by name. + """ + topic( + """ + The topic's name. + """ + name: String! + ): Topic + + """ + Lookup a user by login. + """ + user( + """ + The user's login. + """ + login: String! + ): User + + """ + The currently authenticated user. + """ + viewer: User! +} + +""" +Represents the client's rate limit. +""" +type RateLimit { + """ + The point cost for the current query counting against the rate limit. + """ + cost: Int! + + """ + The maximum number of points the client is permitted to consume in a 60 minute window. + """ + limit: Int! + + """ + The maximum number of nodes this query may return + """ + nodeCount: Int! + + """ + The number of points remaining in the current rate limit window. + """ + remaining: Int! + + """ + The time at which the current rate limit window resets in UTC epoch seconds. + """ + resetAt: DateTime! + + """ + The number of points used in the current rate limit window. + """ + used: Int! +} + +""" +Represents a subject that can be reacted on. +""" +interface Reactable { + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the Reactable object + """ + id: ID! + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! +} + +""" +The connection type for User. +""" +type ReactingUserConnection { + """ + A list of edges. + """ + edges: [ReactingUserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user that's made a reaction. +""" +type ReactingUserEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + The moment when the user made the reaction. + """ + reactedAt: DateTime! +} + +""" +An emoji reaction to a particular piece of content. +""" +type Reaction implements Node { + """ + Identifies the emoji reaction. + """ + content: ReactionContent! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the Reaction object + """ + id: ID! + + """ + The reactable piece of content + """ + reactable: Reactable! + + """ + Identifies the user who created this reaction. + """ + user: User +} + +""" +A list of reactions that have been left on the subject. +""" +type ReactionConnection { + """ + A list of edges. + """ + edges: [ReactionEdge] + + """ + A list of nodes. + """ + nodes: [Reaction] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Whether or not the authenticated user has left a reaction on the subject. + """ + viewerHasReacted: Boolean! +} + +""" +Emojis that can be attached to Issues, Pull Requests and Comments. +""" +enum ReactionContent { + """ + Represents the `:confused:` emoji. + """ + CONFUSED + + """ + Represents the `:eyes:` emoji. + """ + EYES + + """ + Represents the `:heart:` emoji. + """ + HEART + + """ + Represents the `:hooray:` emoji. + """ + HOORAY + + """ + Represents the `:laugh:` emoji. + """ + LAUGH + + """ + Represents the `:rocket:` emoji. + """ + ROCKET + + """ + Represents the `:-1:` emoji. + """ + THUMBS_DOWN + + """ + Represents the `:+1:` emoji. + """ + THUMBS_UP +} + +""" +An edge in a connection. +""" +type ReactionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Reaction +} + +""" +A group of emoji reactions to a particular piece of content. +""" +type ReactionGroup { + """ + Identifies the emoji reaction. + """ + content: ReactionContent! + + """ + Identifies when the reaction was created. + """ + createdAt: DateTime + + """ + Reactors to the reaction subject with the emotion represented by this reaction group. + """ + reactors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReactorConnection! + + """ + The subject that was reacted to. + """ + subject: Reactable! + + """ + Users who have reacted to the reaction subject with the emotion represented by this reaction group + """ + users( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReactingUserConnection! + @deprecated( + reason: "Reactors can now be mannequins, bots, and organizations. Use the `reactors` field instead. Removal on 2021-10-01 UTC." + ) + + """ + Whether or not the authenticated user has left a reaction on the subject. + """ + viewerHasReacted: Boolean! +} + +""" +Ways in which lists of reactions can be ordered upon return. +""" +input ReactionOrder { + """ + The direction in which to order reactions by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order reactions by. + """ + field: ReactionOrderField! +} + +""" +A list of fields that reactions can be ordered by. +""" +enum ReactionOrderField { + """ + Allows ordering a list of reactions by when they were created. + """ + CREATED_AT +} + +""" +Types that can be assigned to reactions. +""" +union Reactor = Bot | Mannequin | Organization | User + +""" +The connection type for Reactor. +""" +type ReactorConnection { + """ + A list of edges. + """ + edges: [ReactorEdge] + + """ + A list of nodes. + """ + nodes: [Reactor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents an author of a reaction. +""" +type ReactorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The author of the reaction. + """ + node: Reactor! + + """ + The moment when the user made the reaction. + """ + reactedAt: DateTime! +} + +""" +Represents a 'ready_for_review' event on a given pull request. +""" +type ReadyForReviewEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ReadyForReviewEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this ready for review event. + """ + resourcePath: URI! + + """ + The HTTP URL for this ready for review event. + """ + url: URI! +} + +""" +Represents a Git reference. +""" +type Ref implements Node { + """ + A list of pull requests with this ref as the head ref. + """ + associatedPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + Branch protection rules for this ref + """ + branchProtectionRule: BranchProtectionRule + + """ + Compares the current ref as a base ref to another head ref, if the comparison can be made. + """ + compare( + """ + The head ref to compare against. + """ + headRef: String! + ): Comparison + + """ + The Node ID of the Ref object + """ + id: ID! + + """ + The ref name. + """ + name: String! + + """ + The ref's prefix, such as `refs/heads/` or `refs/tags/`. + """ + prefix: String! + + """ + Branch protection rules that are viewable by non-admins + """ + refUpdateRule: RefUpdateRule + + """ + The repository the ref belongs to. + """ + repository: Repository! + + """ + A list of rules from active Repository and Organization rulesets that apply to this ref. + """ + rules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repository rules. + """ + orderBy: RepositoryRuleOrder = {field: UPDATED_AT, direction: DESC} + ): RepositoryRuleConnection + + """ + The object the ref points to. Returns null when object does not exist. + """ + target: GitObject +} + +""" +The connection type for Ref. +""" +type RefConnection { + """ + A list of edges. + """ + edges: [RefEdge] + + """ + A list of nodes. + """ + nodes: [Ref] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RefEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Ref +} + +""" +Parameters to be used for the ref_name condition +""" +type RefNameConditionTarget { + """ + Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match. + """ + exclude: [String!]! + + """ + Array of ref names or patterns to include. One of these patterns must match + for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the + default branch or `~ALL` to include all branches. + """ + include: [String!]! +} + +""" +Parameters to be used for the ref_name condition +""" +input RefNameConditionTargetInput { + """ + Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match. + """ + exclude: [String!]! + + """ + Array of ref names or patterns to include. One of these patterns must match + for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the + default branch or `~ALL` to include all branches. + """ + include: [String!]! +} + +""" +Ways in which lists of git refs can be ordered upon return. +""" +input RefOrder { + """ + The direction in which to order refs by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order refs by. + """ + field: RefOrderField! +} + +""" +Properties by which ref connections can be ordered. +""" +enum RefOrderField { + """ + Order refs by their alphanumeric name + """ + ALPHABETICAL + + """ + Order refs by underlying commit date if the ref prefix is refs/tags/ + """ + TAG_COMMIT_DATE +} + +""" +A ref update +""" +input RefUpdate { + """ + The value this ref should be updated to. + """ + afterOid: GitObjectID! + + """ + The value this ref needs to point to before the update. + """ + beforeOid: GitObjectID + + """ + Force a non fast-forward update. + """ + force: Boolean = false + + """ + The fully qualified name of the ref to be update. For example `refs/heads/branch-name` + """ + name: GitRefname! +} + +""" +Branch protection rules that are enforced on the viewer. +""" +type RefUpdateRule { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean! + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean! + + """ + Can matching branches be created. + """ + blocksCreations: Boolean! + + """ + Identifies the protection rule pattern. + """ + pattern: String! + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String] + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean! + + """ + Are conversations required to be resolved before merging. + """ + requiresConversationResolution: Boolean! + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean! + + """ + Are commits required to be signed. + """ + requiresSignatures: Boolean! + + """ + Is the viewer allowed to dismiss reviews. + """ + viewerAllowedToDismissReviews: Boolean! + + """ + Can the viewer push to the branch + """ + viewerCanPush: Boolean! +} + +""" +Represents a 'referenced' event on a given `ReferencedSubject`. +""" +type ReferencedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the commit associated with the 'referenced' event. + """ + commit: Commit + + """ + Identifies the repository associated with the 'referenced' event. + """ + commitRepository: Repository! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ReferencedEvent object + """ + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Checks if the commit message itself references the subject. Can be false in the case of a commit comment reference. + """ + isDirectReference: Boolean! + + """ + Object referenced by event. + """ + subject: ReferencedSubject! +} + +""" +Any referencable object +""" +union ReferencedSubject = Issue | PullRequest + +""" +Autogenerated input type of RegenerateEnterpriseIdentityProviderRecoveryCodes +""" +input RegenerateEnterpriseIdentityProviderRecoveryCodesInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set an identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) +} + +""" +Autogenerated return type of RegenerateEnterpriseIdentityProviderRecoveryCodes. +""" +type RegenerateEnterpriseIdentityProviderRecoveryCodesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider for the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of RegenerateVerifiableDomainToken +""" +input RegenerateVerifiableDomainTokenInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to regenerate the verification token of. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of RegenerateVerifiableDomainToken. +""" +type RegenerateVerifiableDomainTokenPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verification token that was generated. + """ + verificationToken: String +} + +""" +Autogenerated input type of RejectDeployments +""" +input RejectDeploymentsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Optional comment for rejecting deployments + """ + comment: String = "" + + """ + The ids of environments to reject deployments + """ + environmentIds: [ID!]! + + """ + The node ID of the workflow run containing the pending deployments. + """ + workflowRunId: ID! @possibleTypes(concreteTypes: ["WorkflowRun"]) +} + +""" +Autogenerated return type of RejectDeployments. +""" +type RejectDeploymentsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The affected deployments. + """ + deployments: [Deployment!] +} + +""" +A release contains the content for a release. +""" +type Release implements Node & Reactable & UniformResourceLocatable { + """ + The author of the release + """ + author: User + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the release. + """ + description: String + + """ + The description of this release rendered to HTML. + """ + descriptionHTML: HTML + + """ + The Node ID of the Release object + """ + id: ID! + + """ + Whether or not the release is a draft + """ + isDraft: Boolean! + + """ + Whether or not the release is the latest releast + """ + isLatest: Boolean! + + """ + Whether or not the release is a prerelease + """ + isPrerelease: Boolean! + + """ + A list of users mentioned in the release description + """ + mentions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection + + """ + The title of the release. + """ + name: String + + """ + Identifies the date and time when the release was created. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + List of releases assets which are dependent on this release. + """ + releaseAssets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + A name to filter the assets by. + """ + name: String + ): ReleaseAssetConnection! + + """ + The repository that the release belongs to. + """ + repository: Repository! + + """ + The HTTP path for this issue + """ + resourcePath: URI! + + """ + A description of the release, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML + + """ + The Git tag the release points to + """ + tag: Ref + + """ + The tag commit for this release. + """ + tagCommit: Commit + + """ + The name of the release's Git tag + """ + tagName: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue + """ + url: URI! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! +} + +""" +A release asset contains the content for a release asset. +""" +type ReleaseAsset implements Node { + """ + The asset's content-type + """ + contentType: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The number of times this asset was downloaded + """ + downloadCount: Int! + + """ + Identifies the URL where you can download the release asset via the browser. + """ + downloadUrl: URI! + + """ + The Node ID of the ReleaseAsset object + """ + id: ID! + + """ + Identifies the title of the release asset. + """ + name: String! + + """ + Release that the asset is associated with + """ + release: Release + + """ + The size (in bytes) of the asset + """ + size: Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user that performed the upload + """ + uploadedBy: User! + + """ + Identifies the URL of the release asset. + """ + url: URI! +} + +""" +The connection type for ReleaseAsset. +""" +type ReleaseAssetConnection { + """ + A list of edges. + """ + edges: [ReleaseAssetEdge] + + """ + A list of nodes. + """ + nodes: [ReleaseAsset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReleaseAssetEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReleaseAsset +} + +""" +The connection type for Release. +""" +type ReleaseConnection { + """ + A list of edges. + """ + edges: [ReleaseEdge] + + """ + A list of nodes. + """ + nodes: [Release] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReleaseEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Release +} + +""" +Ways in which lists of releases can be ordered upon return. +""" +input ReleaseOrder { + """ + The direction in which to order releases by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order releases by. + """ + field: ReleaseOrderField! +} + +""" +Properties by which release connections can be ordered. +""" +enum ReleaseOrderField { + """ + Order releases by creation time + """ + CREATED_AT + + """ + Order releases alphabetically by name + """ + NAME +} + +""" +Autogenerated input type of RemoveAssigneesFromAssignable +""" +input RemoveAssigneesFromAssignableInput { + """ + The id of the assignable object to remove assignees from. + """ + assignableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Assignable") + + """ + The id of users to remove as assignees. + """ + assigneeIds: [ID!]! @possibleTypes(concreteTypes: ["User"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of RemoveAssigneesFromAssignable. +""" +type RemoveAssigneesFromAssignablePayload { + """ + The item that was unassigned. + """ + assignable: Assignable + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of RemoveEnterpriseAdmin +""" +input RemoveEnterpriseAdminInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Enterprise ID from which to remove the administrator. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the user to remove as an administrator. + """ + login: String! +} + +""" +Autogenerated return type of RemoveEnterpriseAdmin. +""" +type RemoveEnterpriseAdminPayload { + """ + The user who was removed as an administrator. + """ + admin: User + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise + + """ + A message confirming the result of removing an administrator. + """ + message: String + + """ + The viewer performing the mutation. + """ + viewer: User +} + +""" +Autogenerated input type of RemoveEnterpriseIdentityProvider +""" +input RemoveEnterpriseIdentityProviderInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise from which to remove the identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) +} + +""" +Autogenerated return type of RemoveEnterpriseIdentityProvider. +""" +type RemoveEnterpriseIdentityProviderPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider that was removed from the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of RemoveEnterpriseMember +""" +input RemoveEnterpriseMemberInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise from which the user should be removed. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the user to remove from the enterprise. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of RemoveEnterpriseMember. +""" +type RemoveEnterpriseMemberPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise + + """ + The user that was removed from the enterprise. + """ + user: User + + """ + The viewer performing the mutation. + """ + viewer: User +} + +""" +Autogenerated input type of RemoveEnterpriseOrganization +""" +input RemoveEnterpriseOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise from which the organization should be removed. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization to remove from the enterprise. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of RemoveEnterpriseOrganization. +""" +type RemoveEnterpriseOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise + + """ + The organization that was removed from the enterprise. + """ + organization: Organization + + """ + The viewer performing the mutation. + """ + viewer: User +} + +""" +Autogenerated input type of RemoveEnterpriseSupportEntitlement +""" +input RemoveEnterpriseSupportEntitlementInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a member who will lose the support entitlement. + """ + login: String! +} + +""" +Autogenerated return type of RemoveEnterpriseSupportEntitlement. +""" +type RemoveEnterpriseSupportEntitlementPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of removing the support entitlement. + """ + message: String +} + +""" +Autogenerated input type of RemoveLabelsFromLabelable +""" +input RemoveLabelsFromLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ids of labels to remove. + """ + labelIds: [ID!]! @possibleTypes(concreteTypes: ["Label"]) + + """ + The id of the Labelable to remove labels from. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of RemoveLabelsFromLabelable. +""" +type RemoveLabelsFromLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Labelable the labels were removed from. + """ + labelable: Labelable +} + +""" +Autogenerated input type of RemoveOutsideCollaborator +""" +input RemoveOutsideCollaboratorInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization to remove the outside collaborator from. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The ID of the outside collaborator to remove. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of RemoveOutsideCollaborator. +""" +type RemoveOutsideCollaboratorPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was removed as an outside collaborator. + """ + removedUser: User +} + +""" +Autogenerated input type of RemoveReaction +""" +input RemoveReactionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the emoji reaction to remove. + """ + content: ReactionContent! + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! + @possibleTypes( + concreteTypes: [ + "CommitComment" + "Discussion" + "DiscussionComment" + "Issue" + "IssueComment" + "PullRequest" + "PullRequestReview" + "PullRequestReviewComment" + "Release" + "TeamDiscussion" + "TeamDiscussionComment" + ] + abstractType: "Reactable" + ) +} + +""" +Autogenerated return type of RemoveReaction. +""" +type RemoveReactionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The reaction object. + """ + reaction: Reaction + + """ + The reaction groups for the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + The reactable subject. + """ + subject: Reactable +} + +""" +Autogenerated input type of RemoveStar +""" +input RemoveStarInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Starrable ID to unstar. + """ + starrableId: ID! @possibleTypes(concreteTypes: ["Gist", "Repository", "Topic"], abstractType: "Starrable") +} + +""" +Autogenerated return type of RemoveStar. +""" +type RemoveStarPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The starrable. + """ + starrable: Starrable +} + +""" +Autogenerated input type of RemoveSubIssue +""" +input RemoveSubIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the issue. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The id of the sub-issue. + """ + subIssueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of RemoveSubIssue. +""" +type RemoveSubIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The parent of the sub-issue. + """ + issue: Issue + + """ + The sub-issue of the parent. + """ + subIssue: Issue +} + +""" +Autogenerated input type of RemoveUpvote +""" +input RemoveUpvoteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion or comment to remove upvote. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["Discussion", "DiscussionComment"], abstractType: "Votable") +} + +""" +Autogenerated return type of RemoveUpvote. +""" +type RemoveUpvotePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The votable subject. + """ + subject: Votable +} + +""" +Represents a 'removed_from_merge_queue' event on a given pull request. +""" +type RemovedFromMergeQueueEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the before commit SHA for the 'removed_from_merge_queue' event. + """ + beforeCommit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who removed this Pull Request from the merge queue + """ + enqueuer: User + + """ + The Node ID of the RemovedFromMergeQueueEvent object + """ + id: ID! + + """ + The merge queue where this pull request was removed from. + """ + mergeQueue: MergeQueue + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest + + """ + The reason this pull request was removed from the queue. + """ + reason: String +} + +""" +Represents a 'removed_from_project' event on a given issue or pull request. +""" +type RemovedFromProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The Node ID of the RemovedFromProjectEvent object + """ + id: ID! + + """ + Project referenced by event. + """ + project: Project + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Column name referenced by this project event. + """ + projectColumnName: String! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) +} + +""" +Represents a 'renamed' event on a given issue or pull request +""" +type RenamedTitleEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the current title of the issue or pull request. + """ + currentTitle: String! + + """ + The Node ID of the RenamedTitleEvent object + """ + id: ID! + + """ + Identifies the previous title of the issue or pull request. + """ + previousTitle: String! + + """ + Subject that was renamed. + """ + subject: RenamedTitleSubject! +} + +""" +An object which has a renamable title +""" +union RenamedTitleSubject = Issue | PullRequest + +""" +Autogenerated input type of ReopenDiscussion +""" +input ReopenDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the discussion to be reopened. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["Discussion"]) +} + +""" +Autogenerated return type of ReopenDiscussion. +""" +type ReopenDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that was reopened. + """ + discussion: Discussion +} + +""" +Autogenerated input type of ReopenIssue +""" +input ReopenIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue to be opened. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of ReopenIssue. +""" +type ReopenIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was opened. + """ + issue: Issue +} + +""" +Autogenerated input type of ReopenPullRequest +""" +input ReopenPullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be reopened. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of ReopenPullRequest. +""" +type ReopenPullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was reopened. + """ + pullRequest: PullRequest +} + +""" +Represents a 'reopened' event on any `Closable`. +""" +type ReopenedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Object that was reopened. + """ + closable: Closable! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ReopenedEvent object + """ + id: ID! + + """ + The reason the issue state was changed to open. + """ + stateReason: IssueStateReason +} + +""" +Autogenerated input type of ReorderEnvironment +""" +input ReorderEnvironmentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the environment to modify + """ + environmentId: ID! @possibleTypes(concreteTypes: ["Environment"]) + + """ + The desired position of the environment + """ + position: Int! +} + +""" +Autogenerated return type of ReorderEnvironment. +""" +type ReorderEnvironmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The environment that was reordered + """ + environment: Environment +} + +""" +Autogenerated input type of ReplaceActorsForAssignable +""" +input ReplaceActorsForAssignableInput { + """ + The ids of the actors to replace the existing assignees. + """ + actorIds: [ID!]! + @possibleTypes( + concreteTypes: ["Bot", "EnterpriseUserAccount", "Mannequin", "Organization", "User"] + abstractType: "Actor" + ) + + """ + The id of the assignable object to replace the assignees for. + """ + assignableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Assignable") + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of ReplaceActorsForAssignable. +""" +type ReplaceActorsForAssignablePayload { + """ + The item that was assigned. + """ + assignable: Assignable + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Audit log entry for a repo.access event. +""" +type RepoAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoAccessAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The visibility of the repository + """ + visibility: RepoAccessAuditEntryVisibility + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The privacy of a repository +""" +enum RepoAccessAuditEntryVisibility { + """ + The repository is visible only to users in the same enterprise. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.add_member event. +""" +type RepoAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoAddMemberAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The visibility of the repository + """ + visibility: RepoAddMemberAuditEntryVisibility + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The privacy of a repository +""" +enum RepoAddMemberAuditEntryVisibility { + """ + The repository is visible only to users in the same enterprise. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.add_topic event. +""" +type RepoAddTopicAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TopicAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoAddTopicAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.archived event. +""" +type RepoArchivedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoArchivedAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The visibility of the repository + """ + visibility: RepoArchivedAuditEntryVisibility + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The privacy of a repository +""" +enum RepoArchivedAuditEntryVisibility { + """ + The repository is visible only to users in the same enterprise. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.change_merge_setting event. +""" +type RepoChangeMergeSettingAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoChangeMergeSettingAuditEntry object + """ + id: ID! + + """ + Whether the change was to enable (true) or disable (false) the merge type + """ + isEnabled: Boolean + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The merge method affected by the change + """ + mergeType: RepoChangeMergeSettingAuditEntryMergeType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The merge options available for pull requests to this repository. +""" +enum RepoChangeMergeSettingAuditEntryMergeType { + """ + The pull request is added to the base branch in a merge commit. + """ + MERGE + + """ + Commits from the pull request are added onto the base branch individually without a merge commit. + """ + REBASE + + """ + The pull request's commits are squashed into a single commit before they are merged to the base branch. + """ + SQUASH +} + +""" +Audit log entry for a repo.config.disable_anonymous_git_access event. +""" +type RepoConfigDisableAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigDisableAnonymousGitAccessAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.disable_collaborators_only event. +""" +type RepoConfigDisableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigDisableCollaboratorsOnlyAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.disable_contributors_only event. +""" +type RepoConfigDisableContributorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigDisableContributorsOnlyAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.disable_sockpuppet_disallowed event. +""" +type RepoConfigDisableSockpuppetDisallowedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigDisableSockpuppetDisallowedAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.enable_anonymous_git_access event. +""" +type RepoConfigEnableAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigEnableAnonymousGitAccessAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.enable_collaborators_only event. +""" +type RepoConfigEnableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigEnableCollaboratorsOnlyAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.enable_contributors_only event. +""" +type RepoConfigEnableContributorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigEnableContributorsOnlyAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.enable_sockpuppet_disallowed event. +""" +type RepoConfigEnableSockpuppetDisallowedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigEnableSockpuppetDisallowedAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.lock_anonymous_git_access event. +""" +type RepoConfigLockAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigLockAnonymousGitAccessAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.config.unlock_anonymous_git_access event. +""" +type RepoConfigUnlockAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoConfigUnlockAnonymousGitAccessAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repo.create event. +""" +type RepoCreateAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the parent repository for this forked repository. + """ + forkParentName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the root repository for this network. + """ + forkSourceName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoCreateAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The visibility of the repository + """ + visibility: RepoCreateAuditEntryVisibility + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The privacy of a repository +""" +enum RepoCreateAuditEntryVisibility { + """ + The repository is visible only to users in the same enterprise. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.destroy event. +""" +type RepoDestroyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoDestroyAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The visibility of the repository + """ + visibility: RepoDestroyAuditEntryVisibility + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The privacy of a repository +""" +enum RepoDestroyAuditEntryVisibility { + """ + The repository is visible only to users in the same enterprise. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.remove_member event. +""" +type RepoRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoRemoveMemberAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The visibility of the repository + """ + visibility: RepoRemoveMemberAuditEntryVisibility + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The privacy of a repository +""" +enum RepoRemoveMemberAuditEntryVisibility { + """ + The repository is visible only to users in the same enterprise. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.remove_topic event. +""" +type RepoRemoveTopicAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TopicAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the RepoRemoveTopicAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The reasons a piece of content can be reported or minimized. +""" +enum ReportedContentClassifiers { + """ + An abusive or harassing piece of content + """ + ABUSE + + """ + A duplicated piece of content + """ + DUPLICATE + + """ + An irrelevant piece of content + """ + OFF_TOPIC + + """ + An outdated piece of content + """ + OUTDATED + + """ + The content has been resolved + """ + RESOLVED + + """ + A spammy piece of content + """ + SPAM +} + +""" +A repository contains the content for a project. +""" +type Repository implements Node & PackageOwner & ProjectOwner & ProjectV2Recent & RepositoryInfo & Starrable & Subscribable & UniformResourceLocatable { + """ + Whether or not a pull request head branch that is behind its base branch can + always be updated even if it is not required to be up to date before merging. + """ + allowUpdateBranch: Boolean! + + """ + Identifies the date and time when the repository was archived. + """ + archivedAt: DateTime + + """ + A list of users that can be assigned to issues in this repository. + """ + assignableUsers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters users with query on user name and login. + """ + query: String + ): UserConnection! + + """ + Whether or not Auto-merge can be enabled on pull requests in this repository. + """ + autoMergeAllowed: Boolean! + + """ + A list of branch protection rules for this repository. + """ + branchProtectionRules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BranchProtectionRuleConnection! + + """ + Returns the code of conduct for this repository + """ + codeOfConduct: CodeOfConduct + + """ + Information extracted from the repository's `CODEOWNERS` file. + """ + codeowners( + """ + The ref name used to return the associated `CODEOWNERS` file. + """ + refName: String + ): RepositoryCodeowners + + """ + A list of collaborators associated with the repository. + """ + collaborators( + """ + Collaborators affiliation level with a repository. + """ + affiliation: CollaboratorAffiliation + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The login of one specific collaborator. + """ + login: String + + """ + Filters users with query on user name and login + """ + query: String + ): RepositoryCollaboratorConnection + + """ + A list of commit comments associated with the repository. + """ + commitComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + Returns a list of contact links associated to the repository + """ + contactLinks: [RepositoryContactLink!] + + """ + Returns the contributing guidelines for this repository. + """ + contributingGuidelines: ContributingGuidelines + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Ref associated with the repository's default branch. + """ + defaultBranchRef: Ref + + """ + Whether or not branches are automatically deleted when merged in this repository. + """ + deleteBranchOnMerge: Boolean! + + """ + A list of dependency manifests contained in the repository + """ + dependencyGraphManifests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Cursor to paginate dependencies + """ + dependenciesAfter: String + + """ + Number of dependencies to fetch + """ + dependenciesFirst: Int + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Flag to scope to only manifests with dependencies + """ + withDependencies: Boolean + ): DependencyGraphManifestConnection + + """ + A list of deploy keys that are on this repository. + """ + deployKeys( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeployKeyConnection! + + """ + Deployments associated with the repository + """ + deployments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Environments to list deployments for + """ + environments: [String!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for deployments returned from the connection. + """ + orderBy: DeploymentOrder = {field: CREATED_AT, direction: ASC} + ): DeploymentConnection! + + """ + The description of the repository. + """ + description: String + + """ + The description of the repository rendered to HTML. + """ + descriptionHTML: HTML! + + """ + Returns a single discussion from the current repository by number. + """ + discussion( + """ + The number for the discussion to be returned. + """ + number: Int! + ): Discussion + + """ + A list of discussion categories that are available in the repository. + """ + discussionCategories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filter by categories that are assignable by the viewer. + """ + filterByAssignable: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DiscussionCategoryConnection! + + """ + A discussion category by slug. + """ + discussionCategory( + """ + The slug of the discussion category to be returned. + """ + slug: String! + ): DiscussionCategory + + """ + A list of discussions that have been opened in the repository. + """ + discussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Only show answered or unanswered discussions + """ + answered: Boolean = null + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Only include discussions that belong to the category with this ID. + """ + categoryId: ID = null + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for discussions returned from the connection. + """ + orderBy: DiscussionOrder = {field: UPDATED_AT, direction: DESC} + + """ + A list of states to filter the discussions by. + """ + states: [DiscussionState!] = [] + ): DiscussionConnection! + + """ + The number of kilobytes this repository occupies on disk. + """ + diskUsage: Int + + """ + Returns a single active environment from the current repository by name. + """ + environment( + """ + The name of the environment to be returned. + """ + name: String! + ): Environment + + """ + A list of environments that are in this repository. + """ + environments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The names of the environments to be returned. + """ + names: [String!] = [] + + """ + Ordering options for the environments + """ + orderBy: Environments = {field: NAME, direction: ASC} + + """ + Filter to control pinned environments return + """ + pinnedEnvironmentFilter: EnvironmentPinnedFilterField = ALL + ): EnvironmentConnection! + + """ + Returns how many forks there are of this repository in the whole network. + """ + forkCount: Int! + + """ + Whether this repository allows forks. + """ + forkingAllowed: Boolean! + + """ + A list of direct forked repositories. + """ + forks( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy. Internal + repositories are considered private; consider using the visibility argument + if only internal repositories are needed. Cannot be combined with the + visibility argument. + """ + privacy: RepositoryPrivacy + + """ + If non-null, filters repositories according to visibility. Cannot be combined with the privacy argument. + """ + visibility: RepositoryVisibility + ): RepositoryConnection! + + """ + The funding links for this repository + """ + fundingLinks: [FundingLink!]! + + """ + Indicates if the repository has the Discussions feature enabled. + """ + hasDiscussionsEnabled: Boolean! + + """ + Indicates if the repository has issues feature enabled. + """ + hasIssuesEnabled: Boolean! + + """ + Indicates if the repository has the Projects feature enabled. + """ + hasProjectsEnabled: Boolean! + + """ + Indicates if the repository displays a Sponsor button for financial contributions. + """ + hasSponsorshipsEnabled: Boolean! + + """ + Whether vulnerability alerts are enabled for the repository. + """ + hasVulnerabilityAlertsEnabled: Boolean! + + """ + Indicates if the repository has wiki feature enabled. + """ + hasWikiEnabled: Boolean! + + """ + The repository's URL. + """ + homepageUrl: URI + + """ + The Node ID of the Repository object + """ + id: ID! + + """ + The interaction ability settings for this repository. + """ + interactionAbility: RepositoryInteractionAbility + + """ + Indicates if the repository is unmaintained. + """ + isArchived: Boolean! + + """ + Returns true if blank issue creation is allowed + """ + isBlankIssuesEnabled: Boolean! + + """ + Returns whether or not this repository disabled. + """ + isDisabled: Boolean! + + """ + Returns whether or not this repository is empty. + """ + isEmpty: Boolean! + + """ + Identifies if the repository is a fork. + """ + isFork: Boolean! + + """ + Indicates if a repository is either owned by an organization, or is a private fork of an organization repository. + """ + isInOrganization: Boolean! + + """ + Indicates if the repository has been locked or not. + """ + isLocked: Boolean! + + """ + Identifies if the repository is a mirror. + """ + isMirror: Boolean! + + """ + Identifies if the repository is private or internal. + """ + isPrivate: Boolean! + + """ + Returns true if this repository has a security policy + """ + isSecurityPolicyEnabled: Boolean + + """ + Identifies if the repository is a template that can be used to generate new repositories. + """ + isTemplate: Boolean! + + """ + Is this repository a user configuration repository? + """ + isUserConfigurationRepository: Boolean! + + """ + Returns a single issue from the current repository by number. + """ + issue( + """ + The number for the issue to be returned. + """ + number: Int! + ): Issue + + """ + Returns a single issue-like object from the current repository by number. + """ + issueOrPullRequest( + """ + The number for the issue to be returned. + """ + number: Int! + ): IssueOrPullRequest + + """ + Returns a list of issue templates associated to the repository + """ + issueTemplates: [IssueTemplate!] + + """ + Returns a single issue type by name + """ + issueType( + """ + Issue type name. + """ + name: String! + ): IssueType + + """ + A list of the repository's issue types + """ + issueTypes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue types returned from the connection. + """ + orderBy: IssueTypeOrder = {field: CREATED_AT, direction: ASC} + ): IssueTypeConnection + + """ + A list of issues that have been opened in the repository. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Returns a single label by name + """ + label( + """ + Label name + """ + name: String! + ): Label + + """ + A list of labels associated with the repository. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + + """ + If provided, searches labels by name and description. + """ + query: String + ): LabelConnection + + """ + A list containing a breakdown of the language composition of the repository. + """ + languages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: LanguageOrder + ): LanguageConnection + + """ + Get the latest release for the repository if one exists. + """ + latestRelease: Release + + """ + The license associated with the repository + """ + licenseInfo: License + + """ + The reason the repository has been locked. + """ + lockReason: RepositoryLockReason + + """ + A list of Users that can be mentioned in the context of the repository. + """ + mentionableUsers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters users with query on user name and login + """ + query: String + ): UserConnection! + + """ + Whether or not PRs are merged with a merge commit on this repository. + """ + mergeCommitAllowed: Boolean! + + """ + How the default commit message will be generated when merging a pull request. + """ + mergeCommitMessage: MergeCommitMessage! + + """ + How the default commit title will be generated when merging a pull request. + """ + mergeCommitTitle: MergeCommitTitle! + + """ + The merge queue for a specified branch, otherwise the default branch if not provided. + """ + mergeQueue( + """ + The name of the branch to get the merge queue for. Case sensitive. + """ + branch: String + ): MergeQueue + + """ + Returns a single milestone from the current repository by number. + """ + milestone( + """ + The number for the milestone to be returned. + """ + number: Int! + ): Milestone + + """ + A list of milestones associated with the repository. + """ + milestones( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for milestones. + """ + orderBy: MilestoneOrder + + """ + Filters milestones with a query on the title + """ + query: String + + """ + Filter by the state of the milestones. + """ + states: [MilestoneState!] + ): MilestoneConnection + + """ + The repository's original mirror URL. + """ + mirrorUrl: URI + + """ + The name of the repository. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! + + """ + A Git object in the repository + """ + object( + """ + A Git revision expression suitable for rev-parse + """ + expression: String + + """ + The Git object ID + """ + oid: GitObjectID + ): GitObject + + """ + The image used to represent this repository in Open Graph data. + """ + openGraphImageUrl: URI! + + """ + The User owner of the repository. + """ + owner: RepositoryOwner! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + The repository parent, if this is a fork. + """ + parent: Repository + + """ + A list of discussions that have been pinned in this repository. + """ + pinnedDiscussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PinnedDiscussionConnection! + + """ + A list of pinned environments for this repository. + """ + pinnedEnvironments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the environments + """ + orderBy: PinnedEnvironmentOrder = {field: POSITION, direction: ASC} + ): PinnedEnvironmentConnection + + """ + A list of pinned issues for this repository. + """ + pinnedIssues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PinnedIssueConnection + + """ + Returns information about the availability of certain features and limits based on the repository's billing plan. + """ + planFeatures: RepositoryPlanFeatures! + + """ + The primary language of the repository's code. + """ + primaryLanguage: Language + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Finds and returns the Project according to the provided Project number. + """ + projectV2( + """ + The Project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP path listing the repository's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing the repository's projects + """ + projectsUrl: URI! + + """ + List of projects linked to this repository. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter projects based on user role. + """ + minPermissionLevel: ProjectV2PermissionLevel = READ + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for linked to the repo. + """ + query: String + ): ProjectV2Connection! + + """ + Returns a single pull request from the current repository by number. + """ + pullRequest( + """ + The number for the pull request to be returned. + """ + number: Int! + ): PullRequest + + """ + Returns a list of pull request templates associated to the repository + """ + pullRequestTemplates: [PullRequestTemplate!] + + """ + A list of pull requests that have been opened in the repository. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + Identifies the date and time when the repository was last pushed to. + """ + pushedAt: DateTime + + """ + Whether or not rebase-merging is enabled on this repository. + """ + rebaseMergeAllowed: Boolean! + + """ + Recent projects that this user has modified in the context of the owner. + """ + recentProjects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! + + """ + Fetch a given ref from the repository + """ + ref( + """ + The ref to retrieve. Fully qualified matches are checked in order + (`refs/heads/master`) before falling back onto checks for short name matches (`master`). + """ + qualifiedName: String! + ): Ref + + """ + Fetch a list of refs from the repository + """ + refs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + DEPRECATED: use orderBy. The ordering direction. + """ + direction: OrderDirection + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for refs returned from the connection. + """ + orderBy: RefOrder + + """ + Filters refs with query on name + """ + query: String + + """ + A ref name prefix like `refs/heads/`, `refs/tags/`, etc. + """ + refPrefix: String! + ): RefConnection + + """ + Lookup a single release given various criteria. + """ + release( + """ + The name of the Tag the Release was created from + """ + tagName: String! + ): Release + + """ + List of releases which are dependent on this repository. + """ + releases( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: ReleaseOrder + ): ReleaseConnection! + + """ + A list of applied repository-topic associations for this repository. + """ + repositoryTopics( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryTopicConnection! + + """ + The HTTP path for this repository + """ + resourcePath: URI! + + """ + Returns a single ruleset from the current repository by ID. + """ + ruleset( + """ + The ID of the ruleset to be returned. + """ + databaseId: Int! + + """ + Include rulesets configured at higher levels that apply to this repository + """ + includeParents: Boolean = true + ): RepositoryRuleset + + """ + A list of rulesets for this repository. + """ + rulesets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Return rulesets configured at higher levels that apply to this repository + """ + includeParents: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Return rulesets that apply to the specified target + """ + targets: [RepositoryRulesetTarget!] = null + ): RepositoryRulesetConnection + + """ + The security policy URL. + """ + securityPolicyUrl: URI + + """ + A description of the repository, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML! + + """ + Whether or not squash-merging is enabled on this repository. + """ + squashMergeAllowed: Boolean! + + """ + How the default commit message will be generated when squash merging a pull request. + """ + squashMergeCommitMessage: SquashMergeCommitMessage! + + """ + How the default commit title will be generated when squash merging a pull request. + """ + squashMergeCommitTitle: SquashMergeCommitTitle! + + """ + Whether a squash merge commit can use the pull request title as default. + """ + squashPrTitleUsedAsDefault: Boolean! + @deprecated( + reason: "`squashPrTitleUsedAsDefault` will be removed. Use `Repository.squashMergeCommitTitle` instead. Removal on 2023-04-01 UTC." + ) + + """ + The SSH URL to clone this repository + """ + sshUrl: GitSSHRemote! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a list of all submodules in this repository parsed from the + .gitmodules file as of the default branch's HEAD commit. + """ + submodules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SubmoduleConnection! + + """ + A list of suggested actors that can be attributed to content in this repository. + """ + suggestedActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + A list of capabilities to filter actors by. + """ + capabilities: [RepositorySuggestedActorFilter!]! + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + A comma separated list of login names to filter actors by. Only the first 10 logins will be used. + """ + loginNames: String + + """ + Search actors with query on user name and login. + """ + query: String + ): ActorConnection! + + """ + Temporary authentication token for cloning this repository. + """ + tempCloneToken: String + + """ + The repository from which this repository was generated, if any. + """ + templateRepository: Repository + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this repository + """ + url: URI! + + """ + Whether this repository has a custom image to use with Open Graph as opposed to being represented by the owner's avatar. + """ + usesCustomOpenGraphImage: Boolean! + + """ + Indicates whether the viewer has admin permissions on this repository. + """ + viewerCanAdminister: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Indicates whether the viewer can update the topics of this repository. + """ + viewerCanUpdateTopics: Boolean! + + """ + The last commit email for the viewer. + """ + viewerDefaultCommitEmail: String + + """ + The last used merge method by the viewer or the default for the repository. + """ + viewerDefaultMergeMethod: PullRequestMergeMethod! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! + + """ + The users permission level on the repository. Will return null if authenticated as an GitHub App. + """ + viewerPermission: RepositoryPermission + + """ + A list of emails this viewer can commit with. + """ + viewerPossibleCommitEmails: [String!] + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! + + """ + Returns a single vulnerability alert from the current repository by number. + """ + vulnerabilityAlert( + """ + The number for the vulnerability alert to be returned. + """ + number: Int! + ): RepositoryVulnerabilityAlert + + """ + A list of vulnerability alerts that are on this repository. + """ + vulnerabilityAlerts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filter by the scope of the alert's dependency + """ + dependencyScopes: [RepositoryVulnerabilityAlertDependencyScope!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter by the state of the alert + """ + states: [RepositoryVulnerabilityAlertState!] + ): RepositoryVulnerabilityAlertConnection + + """ + A list of users watching the repository. + """ + watchers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + Whether contributors are required to sign off on web-based commits in this repository. + """ + webCommitSignoffRequired: Boolean! +} + +""" +The affiliation of a user to a repository +""" +enum RepositoryAffiliation { + """ + Repositories that the user has been added to as a collaborator. + """ + COLLABORATOR + + """ + Repositories that the user has access to through being a member of an + organization. This includes every repository on every team that the user is on. + """ + ORGANIZATION_MEMBER + + """ + Repositories that are owned by the authenticated user. + """ + OWNER +} + +""" +Metadata for an audit entry with action repo.* +""" +interface RepositoryAuditEntryData { + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI +} + +""" +Information extracted from a repository's `CODEOWNERS` file. +""" +type RepositoryCodeowners { + """ + Any problems that were encountered while parsing the `CODEOWNERS` file. + """ + errors: [RepositoryCodeownersError!]! +} + +""" +An error in a `CODEOWNERS` file. +""" +type RepositoryCodeownersError { + """ + The column number where the error occurs. + """ + column: Int! + + """ + A short string describing the type of error. + """ + kind: String! + + """ + The line number where the error occurs. + """ + line: Int! + + """ + A complete description of the error, combining information from other fields. + """ + message: String! + + """ + The path to the file when the error occurs. + """ + path: String! + + """ + The content of the line where the error occurs. + """ + source: String! + + """ + A suggestion of how to fix the error. + """ + suggestion: String +} + +""" +The connection type for User. +""" +type RepositoryCollaboratorConnection { + """ + A list of edges. + """ + edges: [RepositoryCollaboratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user who is a collaborator of a repository. +""" +type RepositoryCollaboratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + The permission the user has on the repository. + """ + permission: RepositoryPermission! + + """ + A list of sources for the user's access to the repository. + """ + permissionSources: [PermissionSource!] +} + +""" +A list of repositories owned by the subject. +""" +type RepositoryConnection { + """ + A list of edges. + """ + edges: [RepositoryEdge] + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + The total size in kilobytes of all repositories in the connection. Value will + never be larger than max 32-bit signed integer. + """ + totalDiskUsage: Int! +} + +""" +A repository contact link. +""" +type RepositoryContactLink { + """ + The contact link purpose. + """ + about: String! + + """ + The contact link name. + """ + name: String! + + """ + The contact link URL. + """ + url: URI! +} + +""" +The reason a repository is listed as 'contributed'. +""" +enum RepositoryContributionType { + """ + Created a commit + """ + COMMIT + + """ + Created an issue + """ + ISSUE + + """ + Created a pull request + """ + PULL_REQUEST + + """ + Reviewed a pull request + """ + PULL_REQUEST_REVIEW + + """ + Created the repository + """ + REPOSITORY +} + +""" +Represents an author of discussions in repositories. +""" +interface RepositoryDiscussionAuthor { + """ + Discussions this user has started. + """ + repositoryDiscussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter discussions to only those that have been answered or not. Defaults to + including both answered and unanswered discussions. + """ + answered: Boolean = null + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for discussions returned from the connection. + """ + orderBy: DiscussionOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter discussions to only those in a specific repository. + """ + repositoryId: ID + + """ + A list of states to filter the discussions by. + """ + states: [DiscussionState!] = [] + ): DiscussionConnection! +} + +""" +Represents an author of discussion comments in repositories. +""" +interface RepositoryDiscussionCommentAuthor { + """ + Discussion comments this user has authored. + """ + repositoryDiscussionComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter discussion comments to only those that were marked as the answer + """ + onlyAnswers: Boolean = false + + """ + Filter discussion comments to only those in a specific repository. + """ + repositoryId: ID + ): DiscussionCommentConnection! +} + +""" +An edge in a connection. +""" +type RepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Repository +} + +""" +Parameters to be used for the repository_id condition +""" +type RepositoryIdConditionTarget { + """ + One of these repo IDs must match the repo. + """ + repositoryIds: [ID!]! +} + +""" +Parameters to be used for the repository_id condition +""" +input RepositoryIdConditionTargetInput { + """ + One of these repo IDs must match the repo. + """ + repositoryIds: [ID!]! +} + +""" +A subset of repository info. +""" +interface RepositoryInfo { + """ + Identifies the date and time when the repository was archived. + """ + archivedAt: DateTime + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The description of the repository. + """ + description: String + + """ + The description of the repository rendered to HTML. + """ + descriptionHTML: HTML! + + """ + Returns how many forks there are of this repository in the whole network. + """ + forkCount: Int! + + """ + Indicates if the repository has the Discussions feature enabled. + """ + hasDiscussionsEnabled: Boolean! + + """ + Indicates if the repository has issues feature enabled. + """ + hasIssuesEnabled: Boolean! + + """ + Indicates if the repository has the Projects feature enabled. + """ + hasProjectsEnabled: Boolean! + + """ + Indicates if the repository displays a Sponsor button for financial contributions. + """ + hasSponsorshipsEnabled: Boolean! + + """ + Indicates if the repository has wiki feature enabled. + """ + hasWikiEnabled: Boolean! + + """ + The repository's URL. + """ + homepageUrl: URI + + """ + Indicates if the repository is unmaintained. + """ + isArchived: Boolean! + + """ + Identifies if the repository is a fork. + """ + isFork: Boolean! + + """ + Indicates if a repository is either owned by an organization, or is a private fork of an organization repository. + """ + isInOrganization: Boolean! + + """ + Indicates if the repository has been locked or not. + """ + isLocked: Boolean! + + """ + Identifies if the repository is a mirror. + """ + isMirror: Boolean! + + """ + Identifies if the repository is private or internal. + """ + isPrivate: Boolean! + + """ + Identifies if the repository is a template that can be used to generate new repositories. + """ + isTemplate: Boolean! + + """ + The license associated with the repository + """ + licenseInfo: License + + """ + The reason the repository has been locked. + """ + lockReason: RepositoryLockReason + + """ + The repository's original mirror URL. + """ + mirrorUrl: URI + + """ + The name of the repository. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! + + """ + The image used to represent this repository in Open Graph data. + """ + openGraphImageUrl: URI! + + """ + The User owner of the repository. + """ + owner: RepositoryOwner! + + """ + Identifies the date and time when the repository was last pushed to. + """ + pushedAt: DateTime + + """ + The HTTP path for this repository + """ + resourcePath: URI! + + """ + A description of the repository, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this repository + """ + url: URI! + + """ + Whether this repository has a custom image to use with Open Graph as opposed to being represented by the owner's avatar. + """ + usesCustomOpenGraphImage: Boolean! + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! +} + +""" +Repository interaction limit that applies to this object. +""" +type RepositoryInteractionAbility { + """ + The time the currently active limit expires. + """ + expiresAt: DateTime + + """ + The current limit that is enabled on this object. + """ + limit: RepositoryInteractionLimit! + + """ + The origin of the currently active interaction limit. + """ + origin: RepositoryInteractionLimitOrigin! +} + +""" +A repository interaction limit. +""" +enum RepositoryInteractionLimit { + """ + Users that are not collaborators will not be able to interact with the repository. + """ + COLLABORATORS_ONLY + + """ + Users that have not previously committed to a repository’s default branch will be unable to interact with the repository. + """ + CONTRIBUTORS_ONLY + + """ + Users that have recently created their account will be unable to interact with the repository. + """ + EXISTING_USERS + + """ + No interaction limits are enabled. + """ + NO_LIMIT +} + +""" +The length for a repository interaction limit to be enabled for. +""" +enum RepositoryInteractionLimitExpiry { + """ + The interaction limit will expire after 1 day. + """ + ONE_DAY + + """ + The interaction limit will expire after 1 month. + """ + ONE_MONTH + + """ + The interaction limit will expire after 1 week. + """ + ONE_WEEK + + """ + The interaction limit will expire after 6 months. + """ + SIX_MONTHS + + """ + The interaction limit will expire after 3 days. + """ + THREE_DAYS +} + +""" +Indicates where an interaction limit is configured. +""" +enum RepositoryInteractionLimitOrigin { + """ + A limit that is configured at the organization level. + """ + ORGANIZATION + + """ + A limit that is configured at the repository level. + """ + REPOSITORY + + """ + A limit that is configured at the user-wide level. + """ + USER +} + +""" +An invitation for a user to be added to a repository. +""" +type RepositoryInvitation implements Node { + """ + The email address that received the invitation. + """ + email: String + + """ + The Node ID of the RepositoryInvitation object + """ + id: ID! + + """ + The user who received the invitation. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User! + + """ + The permalink for this repository invitation. + """ + permalink: URI! + + """ + The permission granted on this repository by this invitation. + """ + permission: RepositoryPermission! + + """ + The Repository the user is invited to. + """ + repository: RepositoryInfo +} + +""" +A list of repository invitations. +""" +type RepositoryInvitationConnection { + """ + A list of edges. + """ + edges: [RepositoryInvitationEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryInvitation +} + +""" +Ordering options for repository invitation connections. +""" +input RepositoryInvitationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repository invitations by. + """ + field: RepositoryInvitationOrderField! +} + +""" +Properties by which repository invitation connections can be ordered. +""" +enum RepositoryInvitationOrderField { + """ + Order repository invitations by creation time + """ + CREATED_AT +} + +""" +The possible reasons a given repository could be in a locked state. +""" +enum RepositoryLockReason { + """ + The repository is locked due to a billing related reason. + """ + BILLING + + """ + The repository is locked due to a migration. + """ + MIGRATING + + """ + The repository is locked due to a move. + """ + MOVING + + """ + The repository is locked due to a rename. + """ + RENAME + + """ + The repository is locked due to a trade controls related reason. + """ + TRADE_RESTRICTION + + """ + The repository is locked due to an ownership transfer. + """ + TRANSFERRING_OWNERSHIP +} + +""" +A GitHub Enterprise Importer (GEI) repository migration. +""" +type RepositoryMigration implements Migration & Node { + """ + The migration flag to continue on error. + """ + continueOnError: Boolean! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: String + + """ + The reason the migration failed. + """ + failureReason: String + + """ + The Node ID of the RepositoryMigration object + """ + id: ID! + + """ + The URL for the migration log (expires 1 day after migration completes). + """ + migrationLogUrl: URI + + """ + The migration source. + """ + migrationSource: MigrationSource! + + """ + The target repository name. + """ + repositoryName: String! + + """ + The migration source URL, for example `https://github.com` or `https://monalisa.ghe.com`. + """ + sourceUrl: URI! + + """ + The migration state. + """ + state: MigrationState! + + """ + The number of warnings encountered for this migration. To review the warnings, + check the [Migration Log](https://docs.github.com/migrations/using-github-enterprise-importer/completing-your-migration-with-github-enterprise-importer/accessing-your-migration-logs-for-github-enterprise-importer). + """ + warningsCount: Int! +} + +""" +A list of migrations. +""" +type RepositoryMigrationConnection { + """ + A list of edges. + """ + edges: [RepositoryMigrationEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryMigration] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a repository migration. +""" +type RepositoryMigrationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryMigration +} + +""" +Ordering options for repository migrations. +""" +input RepositoryMigrationOrder { + """ + The ordering direction. + """ + direction: RepositoryMigrationOrderDirection! + + """ + The field to order repository migrations by. + """ + field: RepositoryMigrationOrderField! +} + +""" +Possible directions in which to order a list of repository migrations when provided an `orderBy` argument. +""" +enum RepositoryMigrationOrderDirection { + """ + Specifies an ascending order for a given `orderBy` argument. + """ + ASC + + """ + Specifies a descending order for a given `orderBy` argument. + """ + DESC +} + +""" +Properties by which repository migrations can be ordered. +""" +enum RepositoryMigrationOrderField { + """ + Order mannequins why when they were created. + """ + CREATED_AT +} + +""" +Parameters to be used for the repository_name condition +""" +type RepositoryNameConditionTarget { + """ + Array of repository names or patterns to exclude. The condition will not pass if any of these patterns match. + """ + exclude: [String!]! + + """ + Array of repository names or patterns to include. One of these patterns must + match for the condition to pass. Also accepts `~ALL` to include all repositories. + """ + include: [String!]! + + """ + Target changes that match these patterns will be prevented except by those with bypass permissions. + """ + protected: Boolean! +} + +""" +Parameters to be used for the repository_name condition +""" +input RepositoryNameConditionTargetInput { + """ + Array of repository names or patterns to exclude. The condition will not pass if any of these patterns match. + """ + exclude: [String!]! + + """ + Array of repository names or patterns to include. One of these patterns must + match for the condition to pass. Also accepts `~ALL` to include all repositories. + """ + include: [String!]! + + """ + Target changes that match these patterns will be prevented except by those with bypass permissions. + """ + protected: Boolean +} + +""" +Represents a object that belongs to a repository. +""" +interface RepositoryNode { + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +Ordering options for repository connections +""" +input RepositoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: RepositoryOrderField! +} + +""" +Properties by which repository connections can be ordered. +""" +enum RepositoryOrderField { + """ + Order repositories by creation time + """ + CREATED_AT + + """ + Order repositories by name + """ + NAME + + """ + Order repositories by push time + """ + PUSHED_AT + + """ + Order repositories by number of stargazers + """ + STARGAZERS + + """ + Order repositories by update time + """ + UPDATED_AT +} + +""" +Represents an owner of a Repository. +""" +interface RepositoryOwner { + """ + A URL pointing to the owner's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The Node ID of the RepositoryOwner object + """ + id: ID! + + """ + The username used to login. + """ + login: String! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they are archived and not maintained + """ + isArchived: Boolean + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy. Internal + repositories are considered private; consider using the visibility argument + if only internal repositories are needed. Cannot be combined with the + visibility argument. + """ + privacy: RepositoryPrivacy + + """ + If non-null, filters repositories according to visibility. Cannot be combined with the privacy argument. + """ + visibility: RepositoryVisibility + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Follow repository renames. If disabled, a repository referenced by its old name will return an error. + """ + followRenames: Boolean = true + + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + The HTTP URL for the owner. + """ + resourcePath: URI! + + """ + The HTTP URL for the owner. + """ + url: URI! +} + +""" +The access level to a repository +""" +enum RepositoryPermission { + """ + Can read, clone, and push to this repository. Can also manage issues, pull + requests, and repository settings, including adding collaborators + """ + ADMIN + + """ + Can read, clone, and push to this repository. They can also manage issues, pull requests, and some repository settings + """ + MAINTAIN + + """ + Can read and clone this repository. Can also open and comment on issues and pull requests + """ + READ + + """ + Can read and clone this repository. Can also manage issues and pull requests + """ + TRIAGE + + """ + Can read, clone, and push to this repository. Can also manage issues and pull requests + """ + WRITE +} + +""" +Information about the availability of features and limits for a repository based on its billing plan. +""" +type RepositoryPlanFeatures { + """ + Whether reviews can be automatically requested and enforced with a CODEOWNERS file + """ + codeowners: Boolean! + + """ + Whether pull requests can be created as or converted to draft + """ + draftPullRequests: Boolean! + + """ + Maximum number of users that can be assigned to an issue or pull request + """ + maximumAssignees: Int! + + """ + Maximum number of manually-requested reviews on a pull request + """ + maximumManualReviewRequests: Int! + + """ + Whether teams can be requested to review pull requests + """ + teamReviewRequests: Boolean! +} + +""" +The privacy of a repository +""" +enum RepositoryPrivacy { + """ + Private + """ + PRIVATE + + """ + Public + """ + PUBLIC +} + +""" +Parameters to be used for the repository_property condition +""" +type RepositoryPropertyConditionTarget { + """ + Array of repository properties that must not match. + """ + exclude: [PropertyTargetDefinition!]! + + """ + Array of repository properties that must match + """ + include: [PropertyTargetDefinition!]! +} + +""" +Parameters to be used for the repository_property condition +""" +input RepositoryPropertyConditionTargetInput { + """ + Array of repository properties that must not match. + """ + exclude: [PropertyTargetDefinitionInput!]! + + """ + Array of repository properties that must match + """ + include: [PropertyTargetDefinitionInput!]! +} + +""" +A repository rule. +""" +type RepositoryRule implements Node { + """ + The Node ID of the RepositoryRule object + """ + id: ID! + + """ + The parameters for this rule. + """ + parameters: RuleParameters + + """ + The repository ruleset associated with this rule configuration + """ + repositoryRuleset: RepositoryRuleset + + """ + The type of rule. + """ + type: RepositoryRuleType! +} + +""" +Set of conditions that determine if a ruleset will evaluate +""" +type RepositoryRuleConditions { + """ + Configuration for the ref_name condition + """ + refName: RefNameConditionTarget + + """ + Configuration for the repository_id condition + """ + repositoryId: RepositoryIdConditionTarget + + """ + Configuration for the repository_name condition + """ + repositoryName: RepositoryNameConditionTarget + + """ + Configuration for the repository_property condition + """ + repositoryProperty: RepositoryPropertyConditionTarget +} + +""" +Specifies the conditions required for a ruleset to evaluate +""" +input RepositoryRuleConditionsInput { + """ + Configuration for the ref_name condition + """ + refName: RefNameConditionTargetInput + + """ + Configuration for the repository_id condition + """ + repositoryId: RepositoryIdConditionTargetInput + + """ + Configuration for the repository_name condition + """ + repositoryName: RepositoryNameConditionTargetInput + + """ + Configuration for the repository_property condition + """ + repositoryProperty: RepositoryPropertyConditionTargetInput +} + +""" +The connection type for RepositoryRule. +""" +type RepositoryRuleConnection { + """ + A list of edges. + """ + edges: [RepositoryRuleEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryRule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryRuleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryRule +} + +""" +Specifies the attributes for a new or updated rule. +""" +input RepositoryRuleInput { + """ + Optional ID of this rule when updating + """ + id: ID @possibleTypes(concreteTypes: ["RepositoryRule"]) + + """ + The parameters for the rule. + """ + parameters: RuleParametersInput + + """ + The type of rule to create. + """ + type: RepositoryRuleType! +} + +""" +Ordering options for repository rules. +""" +input RepositoryRuleOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repository rules by. + """ + field: RepositoryRuleOrderField! +} + +""" +Properties by which repository rule connections can be ordered. +""" +enum RepositoryRuleOrderField { + """ + Order repository rules by created time + """ + CREATED_AT + + """ + Order repository rules by type + """ + TYPE + + """ + Order repository rules by updated time + """ + UPDATED_AT +} + +""" +The rule types supported in rulesets +""" +enum RepositoryRuleType { + """ + Authorization + """ + AUTHORIZATION + + """ + Branch name pattern + """ + BRANCH_NAME_PATTERN + + """ + Choose which tools must provide code scanning results before the reference is + updated. When configured, code scanning must be enabled and have results for + both the commit and the reference being updated. + """ + CODE_SCANNING + + """ + Committer email pattern + """ + COMMITTER_EMAIL_PATTERN + + """ + Commit author email pattern + """ + COMMIT_AUTHOR_EMAIL_PATTERN + + """ + Commit message pattern + """ + COMMIT_MESSAGE_PATTERN + + """ + Only allow users with bypass permission to create matching refs. + """ + CREATION + + """ + Only allow users with bypass permissions to delete matching refs. + """ + DELETION + + """ + Prevent commits that include files with specified file extensions from being pushed to the commit graph. + """ + FILE_EXTENSION_RESTRICTION + + """ + Prevent commits that include changes in specified file and folder paths from + being pushed to the commit graph. This includes absolute paths that contain file names. + """ + FILE_PATH_RESTRICTION + + """ + Branch is read-only. Users cannot push to the branch. + """ + LOCK_BRANCH + + """ + Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph. + """ + MAX_FILE_PATH_LENGTH + + """ + Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph. + """ + MAX_FILE_SIZE + + """ + Max ref updates + """ + MAX_REF_UPDATES + + """ + Merges must be performed via a merge queue. + """ + MERGE_QUEUE + + """ + Merge queue locked ref + """ + MERGE_QUEUE_LOCKED_REF + + """ + Prevent users with push access from force pushing to refs. + """ + NON_FAST_FORWARD + + """ + Require all commits be made to a non-target branch and submitted via a pull request before they can be merged. + """ + PULL_REQUEST + + """ + Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule. + """ + REQUIRED_DEPLOYMENTS + + """ + Prevent merge commits from being pushed to matching refs. + """ + REQUIRED_LINEAR_HISTORY + + """ + When enabled, all conversations on code must be resolved before a pull request + can be merged into a branch that matches this rule. + """ + REQUIRED_REVIEW_THREAD_RESOLUTION + + """ + Commits pushed to matching refs must have verified signatures. + """ + REQUIRED_SIGNATURES + + """ + Choose which status checks must pass before the ref is updated. When enabled, + commits must first be pushed to another ref where the checks pass. + """ + REQUIRED_STATUS_CHECKS + + """ + Require all commits be made to a non-target branch and submitted via a pull + request and required workflow checks to pass before they can be merged. + """ + REQUIRED_WORKFLOW_STATUS_CHECKS + + """ + Secret scanning + """ + SECRET_SCANNING + + """ + Tag + """ + TAG + + """ + Tag name pattern + """ + TAG_NAME_PATTERN + + """ + Only allow users with bypass permission to update matching refs. + """ + UPDATE + + """ + Require all changes made to a targeted branch to pass the specified workflows before they can be merged. + """ + WORKFLOWS + + """ + Workflow files cannot be modified. + """ + WORKFLOW_UPDATES +} + +""" +A repository ruleset. +""" +type RepositoryRuleset implements Node { + """ + The actors that can bypass this ruleset + """ + bypassActors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryRulesetBypassActorConnection + + """ + The set of conditions that must evaluate to true for this ruleset to apply + """ + conditions: RepositoryRuleConditions! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The enforcement level of this ruleset + """ + enforcement: RuleEnforcement! + + """ + The Node ID of the RepositoryRuleset object + """ + id: ID! + + """ + Name of the ruleset. + """ + name: String! + + """ + List of rules. + """ + rules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The type of rule. + """ + type: RepositoryRuleType + ): RepositoryRuleConnection + + """ + Source of ruleset. + """ + source: RuleSource! + + """ + Target of the ruleset. + """ + target: RepositoryRulesetTarget + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +A team or app that has the ability to bypass a rules defined on a ruleset +""" +type RepositoryRulesetBypassActor implements Node { + """ + The actor that can bypass rules. + """ + actor: BypassActor + + """ + The mode for the bypass actor + """ + bypassMode: RepositoryRulesetBypassActorBypassMode + + """ + This actor represents the ability for a deploy key to bypass + """ + deployKey: Boolean! + + """ + This actor represents the ability for an enterprise owner to bypass + """ + enterpriseOwner: Boolean! + + """ + The Node ID of the RepositoryRulesetBypassActor object + """ + id: ID! + + """ + This actor represents the ability for an organization owner to bypass + """ + organizationAdmin: Boolean! + + """ + If the actor is a repository role, the repository role's ID that can bypass + """ + repositoryRoleDatabaseId: Int + + """ + If the actor is a repository role, the repository role's name that can bypass + """ + repositoryRoleName: String + + """ + Identifies the ruleset associated with the allowed actor + """ + repositoryRuleset: RepositoryRuleset +} + +""" +The bypass mode for a specific actor on a ruleset. +""" +enum RepositoryRulesetBypassActorBypassMode { + """ + The actor can always bypass rules + """ + ALWAYS + + """ + The actor can only bypass rules via a pull request + """ + PULL_REQUEST +} + +""" +The connection type for RepositoryRulesetBypassActor. +""" +type RepositoryRulesetBypassActorConnection { + """ + A list of edges. + """ + edges: [RepositoryRulesetBypassActorEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryRulesetBypassActor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryRulesetBypassActorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryRulesetBypassActor +} + +""" +Specifies the attributes for a new or updated ruleset bypass actor. Only one of +`actor_id`, `repository_role_database_id`, `organization_admin`, or `deploy_key` +should be specified. +""" +input RepositoryRulesetBypassActorInput { + """ + For Team and Integration bypasses, the Team or Integration ID + """ + actorId: ID + + """ + The bypass mode for this actor. + """ + bypassMode: RepositoryRulesetBypassActorBypassMode! + + """ + For deploy key bypasses, true. Can only use ALWAYS as the bypass mode + """ + deployKey: Boolean + + """ + For enterprise owner bypasses, true + """ + enterpriseOwner: Boolean + + """ + For organization owner bypasses, true + """ + organizationAdmin: Boolean + + """ + For role bypasses, the role database ID + """ + repositoryRoleDatabaseId: Int +} + +""" +The connection type for RepositoryRuleset. +""" +type RepositoryRulesetConnection { + """ + A list of edges. + """ + edges: [RepositoryRulesetEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryRuleset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryRulesetEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryRuleset +} + +""" +The targets supported for rulesets. +""" +enum RepositoryRulesetTarget { + """ + Branch + """ + BRANCH + + """ + Push + """ + PUSH + + """ + repository + """ + REPOSITORY + + """ + Tag + """ + TAG +} + +""" +The possible filters for suggested actors in a repository +""" +enum RepositorySuggestedActorFilter { + """ + Actors that can be assigned to issues and pull requests + """ + CAN_BE_ASSIGNED + + """ + Actors that can be the author of issues and pull requests + """ + CAN_BE_AUTHOR +} + +""" +A repository-topic connects a repository to a topic. +""" +type RepositoryTopic implements Node & UniformResourceLocatable { + """ + The Node ID of the RepositoryTopic object + """ + id: ID! + + """ + The HTTP path for this repository-topic. + """ + resourcePath: URI! + + """ + The topic. + """ + topic: Topic! + + """ + The HTTP URL for this repository-topic. + """ + url: URI! +} + +""" +The connection type for RepositoryTopic. +""" +type RepositoryTopicConnection { + """ + A list of edges. + """ + edges: [RepositoryTopicEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryTopic] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryTopicEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryTopic +} + +""" +The repository's visibility level. +""" +enum RepositoryVisibility { + """ + The repository is visible only to users in the same enterprise. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repository_visibility_change.disable event. +""" +type RepositoryVisibilityChangeDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + + """ + The Node ID of the RepositoryVisibilityChangeDisableAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a repository_visibility_change.enable event. +""" +type RepositoryVisibilityChangeEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + + """ + The Node ID of the RepositoryVisibilityChangeEnableAuditEntry object + """ + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +A Dependabot alert for a repository with a dependency affected by a security vulnerability. +""" +type RepositoryVulnerabilityAlert implements Node & RepositoryNode { + """ + When was the alert auto-dismissed? + """ + autoDismissedAt: DateTime + + """ + When was the alert created? + """ + createdAt: DateTime! + + """ + The associated Dependabot update + """ + dependabotUpdate: DependabotUpdate + + """ + The relationship of an alert's dependency. + """ + dependencyRelationship: RepositoryVulnerabilityAlertDependencyRelationship + + """ + The scope of an alert's dependency + """ + dependencyScope: RepositoryVulnerabilityAlertDependencyScope + + """ + Comment explaining the reason the alert was dismissed + """ + dismissComment: String + + """ + The reason the alert was dismissed + """ + dismissReason: String + + """ + When was the alert dismissed? + """ + dismissedAt: DateTime + + """ + The user who dismissed the alert + """ + dismisser: User + + """ + When was the alert fixed? + """ + fixedAt: DateTime + + """ + The Node ID of the RepositoryVulnerabilityAlert object + """ + id: ID! + + """ + Identifies the alert number. + """ + number: Int! + + """ + The associated repository + """ + repository: Repository! + + """ + The associated security advisory + """ + securityAdvisory: SecurityAdvisory + + """ + The associated security vulnerability + """ + securityVulnerability: SecurityVulnerability + + """ + Identifies the state of the alert. + """ + state: RepositoryVulnerabilityAlertState! + + """ + The vulnerable manifest filename + """ + vulnerableManifestFilename: String! + + """ + The vulnerable manifest path + """ + vulnerableManifestPath: String! + + """ + The vulnerable requirements + """ + vulnerableRequirements: String +} + +""" +The connection type for RepositoryVulnerabilityAlert. +""" +type RepositoryVulnerabilityAlertConnection { + """ + A list of edges. + """ + edges: [RepositoryVulnerabilityAlertEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryVulnerabilityAlert] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The possible relationships of an alert's dependency. +""" +enum RepositoryVulnerabilityAlertDependencyRelationship { + """ + A direct dependency of your project + """ + DIRECT + + """ + A transitive dependency of your project + """ + TRANSITIVE + + """ + The relationship is unknown + """ + UNKNOWN +} + +""" +The possible scopes of an alert's dependency. +""" +enum RepositoryVulnerabilityAlertDependencyScope { + """ + A dependency that is only used in development + """ + DEVELOPMENT + + """ + A dependency that is leveraged during application runtime + """ + RUNTIME +} + +""" +An edge in a connection. +""" +type RepositoryVulnerabilityAlertEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryVulnerabilityAlert +} + +""" +The possible states of an alert +""" +enum RepositoryVulnerabilityAlertState { + """ + An alert that has been automatically closed by Dependabot. + """ + AUTO_DISMISSED + + """ + An alert that has been manually closed by a user. + """ + DISMISSED + + """ + An alert that has been resolved by a code change. + """ + FIXED + + """ + An alert that is still open. + """ + OPEN +} + +""" +Autogenerated input type of ReprioritizeSubIssue +""" +input ReprioritizeSubIssueInput { + """ + The id of the sub-issue to be prioritized after (either positional argument after OR before should be specified). + """ + afterId: ID @possibleTypes(concreteTypes: ["Issue"]) + + """ + The id of the sub-issue to be prioritized before (either positional argument after OR before should be specified). + """ + beforeId: ID @possibleTypes(concreteTypes: ["Issue"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the parent issue. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The id of the sub-issue to reprioritize. + """ + subIssueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of ReprioritizeSubIssue. +""" +type ReprioritizeSubIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The parent issue that the sub-issue was reprioritized in. + """ + issue: Issue +} + +""" +Autogenerated input type of RequestReviews +""" +input RequestReviewsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request to modify. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node IDs of the team to request. + """ + teamIds: [ID!] @possibleTypes(concreteTypes: ["Team"]) + + """ + Add users to the set rather than replace. + """ + union: Boolean = false + + """ + The Node IDs of the user to request. + """ + userIds: [ID!] @possibleTypes(concreteTypes: ["Bot", "User"]) +} + +""" +Autogenerated return type of RequestReviews. +""" +type RequestReviewsPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that is getting requests. + """ + pullRequest: PullRequest + + """ + The edge from the pull request to the requested reviewers. + """ + requestedReviewersEdge: UserEdge +} + +""" +The possible states that can be requested when creating a check run. +""" +enum RequestableCheckStatusState { + """ + The check suite or run has been completed. + """ + COMPLETED + + """ + The check suite or run is in progress. + """ + IN_PROGRESS + + """ + The check suite or run is in pending state. + """ + PENDING + + """ + The check suite or run has been queued. + """ + QUEUED + + """ + The check suite or run is in waiting state. + """ + WAITING +} + +""" +Types that can be requested reviewers. +""" +union RequestedReviewer = Bot | Mannequin | Team | User + +""" +The connection type for RequestedReviewer. +""" +type RequestedReviewerConnection { + """ + A list of edges. + """ + edges: [RequestedReviewerEdge] + + """ + A list of nodes. + """ + nodes: [RequestedReviewer] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RequestedReviewerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RequestedReviewer +} + +""" +Represents a type that can be required by a pull request for merging. +""" +interface RequirableByPullRequest { + """ + Whether this is required to pass before merging for a specific pull request. + """ + isRequired( + """ + The id of the pull request this is required for + """ + pullRequestId: ID + + """ + The number of the pull request this is required for + """ + pullRequestNumber: Int + ): Boolean! +} + +""" +Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule. +""" +type RequiredDeploymentsParameters { + """ + The environments that must be successfully deployed to before branches can be merged. + """ + requiredDeploymentEnvironments: [String!]! +} + +""" +Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule. +""" +input RequiredDeploymentsParametersInput { + """ + The environments that must be successfully deployed to before branches can be merged. + """ + requiredDeploymentEnvironments: [String!]! +} + +""" +Represents a required status check for a protected branch, but not any specific run of that check. +""" +type RequiredStatusCheckDescription { + """ + The App that must provide this status in order for it to be accepted. + """ + app: App + + """ + The name of this status. + """ + context: String! +} + +""" +Specifies the attributes for a new or updated required status check. +""" +input RequiredStatusCheckInput { + """ + The ID of the App that must set the status in order for it to be accepted. + Omit this value to use whichever app has recently been setting this status, or + use "any" to allow any app to set the status. + """ + appId: ID + + """ + Status check context that must pass for commits to be accepted to the matching branch. + """ + context: String! +} + +""" +Choose which status checks must pass before the ref is updated. When enabled, +commits must first be pushed to another ref where the checks pass. +""" +type RequiredStatusChecksParameters { + """ + Allow repositories and branches to be created if a check would otherwise prohibit it. + """ + doNotEnforceOnCreate: Boolean! + + """ + Status checks that are required. + """ + requiredStatusChecks: [StatusCheckConfiguration!]! + + """ + Whether pull requests targeting a matching branch must be tested with the + latest code. This setting will not take effect unless at least one status + check is enabled. + """ + strictRequiredStatusChecksPolicy: Boolean! +} + +""" +Choose which status checks must pass before the ref is updated. When enabled, +commits must first be pushed to another ref where the checks pass. +""" +input RequiredStatusChecksParametersInput { + """ + Allow repositories and branches to be created if a check would otherwise prohibit it. + """ + doNotEnforceOnCreate: Boolean + + """ + Status checks that are required. + """ + requiredStatusChecks: [StatusCheckConfigurationInput!]! + + """ + Whether pull requests targeting a matching branch must be tested with the + latest code. This setting will not take effect unless at least one status + check is enabled. + """ + strictRequiredStatusChecksPolicy: Boolean! +} + +""" +Autogenerated input type of RerequestCheckSuite +""" +input RerequestCheckSuiteInput { + """ + The Node ID of the check suite. + """ + checkSuiteId: ID! @possibleTypes(concreteTypes: ["CheckSuite"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of RerequestCheckSuite. +""" +type RerequestCheckSuitePayload { + """ + The requested check suite. + """ + checkSuite: CheckSuite + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of ResolveReviewThread +""" +input ResolveReviewThreadInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the thread to resolve + """ + threadId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewThread"]) +} + +""" +Autogenerated return type of ResolveReviewThread. +""" +type ResolveReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The thread to resolve. + """ + thread: PullRequestReviewThread +} + +""" +Represents a private contribution a user made on GitHub. +""" +type RestrictedContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +Autogenerated input type of RetireSponsorsTier +""" +input RetireSponsorsTierInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the published tier to retire. + """ + tierId: ID! @possibleTypes(concreteTypes: ["SponsorsTier"]) +} + +""" +Autogenerated return type of RetireSponsorsTier. +""" +type RetireSponsorsTierPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The tier that was retired. + """ + sponsorsTier: SponsorsTier +} + +""" +Autogenerated input type of RevertPullRequest +""" +input RevertPullRequestInput { + """ + The description of the revert pull request. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Indicates whether the revert pull request should be a draft. + """ + draft: Boolean = false + + """ + The ID of the pull request to revert. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The title of the revert pull request. + """ + title: String +} + +""" +Autogenerated return type of RevertPullRequest. +""" +type RevertPullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was reverted. + """ + pullRequest: PullRequest + + """ + The new pull request that reverts the input pull request. + """ + revertPullRequest: PullRequest +} + +""" +A user, team, or app who has the ability to dismiss a review on a protected branch. +""" +type ReviewDismissalAllowance implements Node { + """ + The actor that can dismiss. + """ + actor: ReviewDismissalAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user, team, or app. + """ + branchProtectionRule: BranchProtectionRule + + """ + The Node ID of the ReviewDismissalAllowance object + """ + id: ID! +} + +""" +Types that can be an actor. +""" +union ReviewDismissalAllowanceActor = App | Team | User + +""" +The connection type for ReviewDismissalAllowance. +""" +type ReviewDismissalAllowanceConnection { + """ + A list of edges. + """ + edges: [ReviewDismissalAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [ReviewDismissalAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReviewDismissalAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReviewDismissalAllowance +} + +""" +Represents a 'review_dismissed' event on a given issue or pull request. +""" +type ReviewDismissedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Identifies the optional message associated with the 'review_dismissed' event. + """ + dismissalMessage: String + + """ + Identifies the optional message associated with the event, rendered to HTML. + """ + dismissalMessageHTML: String + + """ + The Node ID of the ReviewDismissedEvent object + """ + id: ID! + + """ + Identifies the previous state of the review with the 'review_dismissed' event. + """ + previousReviewState: PullRequestReviewState! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the commit which caused the review to become stale. + """ + pullRequestCommit: PullRequestCommit + + """ + The HTTP path for this review dismissed event. + """ + resourcePath: URI! + + """ + Identifies the review associated with the 'review_dismissed' event. + """ + review: PullRequestReview + + """ + The HTTP URL for this review dismissed event. + """ + url: URI! +} + +""" +A request for a user to review a pull request. +""" +type ReviewRequest implements Node { + """ + Whether this request was created for a code owner + """ + asCodeOwner: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the ReviewRequest object + """ + id: ID! + + """ + Identifies the pull request associated with this review request. + """ + pullRequest: PullRequest! + + """ + The reviewer that is requested. + """ + requestedReviewer: RequestedReviewer +} + +""" +The connection type for ReviewRequest. +""" +type ReviewRequestConnection { + """ + A list of edges. + """ + edges: [ReviewRequestEdge] + + """ + A list of nodes. + """ + nodes: [ReviewRequest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReviewRequestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReviewRequest +} + +""" +Represents an 'review_request_removed' event on a given pull request. +""" +type ReviewRequestRemovedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ReviewRequestRemovedEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the reviewer whose review request was removed. + """ + requestedReviewer: RequestedReviewer +} + +""" +Represents an 'review_requested' event on a given pull request. +""" +type ReviewRequestedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the ReviewRequestedEvent object + """ + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the reviewer whose review was requested. + """ + requestedReviewer: RequestedReviewer +} + +""" +A hovercard context with a message describing the current code review state of the pull +request. +""" +type ReviewStatusHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + The current status of the pull request with respect to code review. + """ + reviewDecision: PullRequestReviewDecision +} + +""" +Autogenerated input type of RevokeEnterpriseOrganizationsMigratorRole +""" +input RevokeEnterpriseOrganizationsMigratorRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise to which all organizations managed by it will be granted the migrator role. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the user to revoke the migrator role + """ + login: String! +} + +""" +Autogenerated return type of RevokeEnterpriseOrganizationsMigratorRole. +""" +type RevokeEnterpriseOrganizationsMigratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organizations that had the migrator role revoked for the given user. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationConnection +} + +""" +Autogenerated input type of RevokeMigratorRole +""" +input RevokeMigratorRoleInput { + """ + The user login or Team slug to revoke the migrator role from. + """ + actor: String! + + """ + Specifies the type of the actor, can be either USER or TEAM. + """ + actorType: ActorType! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization that the user/team belongs to. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of RevokeMigratorRole. +""" +type RevokeMigratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Did the operation succeed? + """ + success: Boolean +} + +""" +Possible roles a user may have in relation to an organization. +""" +enum RoleInOrganization { + """ + A user who is a direct member of the organization. + """ + DIRECT_MEMBER + + """ + A user with full administrative access to the organization. + """ + OWNER + + """ + A user who is unaffiliated with the organization. + """ + UNAFFILIATED +} + +""" +The level of enforcement for a rule or ruleset. +""" +enum RuleEnforcement { + """ + Rules will be enforced + """ + ACTIVE + + """ + Do not evaluate or enforce rules + """ + DISABLED + + """ + Allow admins to test rules before enforcing them. Admins can view insights on + the Rule Insights page (`evaluate` is only available with GitHub Enterprise). + """ + EVALUATE +} + +""" +Types which can be parameters for `RepositoryRule` objects. +""" +union RuleParameters = + BranchNamePatternParameters + | CodeScanningParameters + | CommitAuthorEmailPatternParameters + | CommitMessagePatternParameters + | CommitterEmailPatternParameters + | FileExtensionRestrictionParameters + | FilePathRestrictionParameters + | MaxFilePathLengthParameters + | MaxFileSizeParameters + | MergeQueueParameters + | PullRequestParameters + | RequiredDeploymentsParameters + | RequiredStatusChecksParameters + | TagNamePatternParameters + | UpdateParameters + | WorkflowsParameters + +""" +Specifies the parameters for a `RepositoryRule` object. Only one of the fields should be specified. +""" +input RuleParametersInput { + """ + Parameters used for the `branch_name_pattern` rule type + """ + branchNamePattern: BranchNamePatternParametersInput + + """ + Parameters used for the `code_scanning` rule type + """ + codeScanning: CodeScanningParametersInput + + """ + Parameters used for the `commit_author_email_pattern` rule type + """ + commitAuthorEmailPattern: CommitAuthorEmailPatternParametersInput + + """ + Parameters used for the `commit_message_pattern` rule type + """ + commitMessagePattern: CommitMessagePatternParametersInput + + """ + Parameters used for the `committer_email_pattern` rule type + """ + committerEmailPattern: CommitterEmailPatternParametersInput + + """ + Parameters used for the `file_extension_restriction` rule type + """ + fileExtensionRestriction: FileExtensionRestrictionParametersInput + + """ + Parameters used for the `file_path_restriction` rule type + """ + filePathRestriction: FilePathRestrictionParametersInput + + """ + Parameters used for the `max_file_path_length` rule type + """ + maxFilePathLength: MaxFilePathLengthParametersInput + + """ + Parameters used for the `max_file_size` rule type + """ + maxFileSize: MaxFileSizeParametersInput + + """ + Parameters used for the `merge_queue` rule type + """ + mergeQueue: MergeQueueParametersInput + + """ + Parameters used for the `pull_request` rule type + """ + pullRequest: PullRequestParametersInput + + """ + Parameters used for the `required_deployments` rule type + """ + requiredDeployments: RequiredDeploymentsParametersInput + + """ + Parameters used for the `required_status_checks` rule type + """ + requiredStatusChecks: RequiredStatusChecksParametersInput + + """ + Parameters used for the `tag_name_pattern` rule type + """ + tagNamePattern: TagNamePatternParametersInput + + """ + Parameters used for the `update` rule type + """ + update: UpdateParametersInput + + """ + Parameters used for the `workflows` rule type + """ + workflows: WorkflowsParametersInput +} + +""" +Types which can have `RepositoryRule` objects. +""" +union RuleSource = Enterprise | Organization | Repository + +""" +The possible digest algorithms used to sign SAML requests for an identity provider. +""" +enum SamlDigestAlgorithm { + """ + SHA1 + """ + SHA1 + + """ + SHA256 + """ + SHA256 + + """ + SHA384 + """ + SHA384 + + """ + SHA512 + """ + SHA512 +} + +""" +The possible signature algorithms used to sign SAML requests for a Identity Provider. +""" +enum SamlSignatureAlgorithm { + """ + RSA-SHA1 + """ + RSA_SHA1 + + """ + RSA-SHA256 + """ + RSA_SHA256 + + """ + RSA-SHA384 + """ + RSA_SHA384 + + """ + RSA-SHA512 + """ + RSA_SHA512 +} + +""" +A Saved Reply is text a user can use to reply quickly. +""" +type SavedReply implements Node { + """ + The body of the saved reply. + """ + body: String! + + """ + The saved reply body rendered to HTML. + """ + bodyHTML: HTML! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the SavedReply object + """ + id: ID! + + """ + The title of the saved reply. + """ + title: String! + + """ + The user that saved this reply. + """ + user: Actor +} + +""" +The connection type for SavedReply. +""" +type SavedReplyConnection { + """ + A list of edges. + """ + edges: [SavedReplyEdge] + + """ + A list of nodes. + """ + nodes: [SavedReply] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SavedReplyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SavedReply +} + +""" +Ordering options for saved reply connections. +""" +input SavedReplyOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order saved replies by. + """ + field: SavedReplyOrderField! +} + +""" +Properties by which saved reply connections can be ordered. +""" +enum SavedReplyOrderField { + """ + Order saved reply by when they were updated. + """ + UPDATED_AT +} + +""" +The results of a search. +""" +union SearchResultItem = App | Discussion | Issue | MarketplaceListing | Organization | PullRequest | Repository | User + +""" +A list of results that matched against a search query. Regardless of the number +of matches, a maximum of 1,000 results will be available across all types, +potentially split across many pages. +""" +type SearchResultItemConnection { + """ + The total number of pieces of code that matched the search query. Regardless + of the total number of matches, a maximum of 1,000 results will be available + across all types. + """ + codeCount: Int! + + """ + The total number of discussions that matched the search query. Regardless of + the total number of matches, a maximum of 1,000 results will be available + across all types. + """ + discussionCount: Int! + + """ + A list of edges. + """ + edges: [SearchResultItemEdge] + + """ + The total number of issues that matched the search query. Regardless of the + total number of matches, a maximum of 1,000 results will be available across all types. + """ + issueCount: Int! + + """ + A list of nodes. + """ + nodes: [SearchResultItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total number of repositories that matched the search query. Regardless of + the total number of matches, a maximum of 1,000 results will be available + across all types. + """ + repositoryCount: Int! + + """ + The total number of users that matched the search query. Regardless of the + total number of matches, a maximum of 1,000 results will be available across all types. + """ + userCount: Int! + + """ + The total number of wiki pages that matched the search query. Regardless of + the total number of matches, a maximum of 1,000 results will be available + across all types. + """ + wikiCount: Int! +} + +""" +An edge in a connection. +""" +type SearchResultItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SearchResultItem + + """ + Text matches on the result found. + """ + textMatches: [TextMatch] +} + +""" +Represents the individual results of a search. +""" +enum SearchType { + """ + Returns matching discussions in repositories. + """ + DISCUSSION + + """ + Returns results matching issues in repositories. + """ + ISSUE + + """ + Returns results matching issues in repositories. + """ + ISSUE_ADVANCED + @deprecated( + reason: "Search for issues and pull requests will be overridden by advanced search on September 4, 2025. You can read more about this change on https://github.blog/changelog/2025-03-06-github-issues-projects-api-support-for-issues-advanced-search-and-more/. Removal on 2025-09-04 UTC." + ) + + """ + Returns results matching repositories. + """ + REPOSITORY + + """ + Returns results matching users and organizations on GitHub. + """ + USER +} + +""" +A GitHub Security Advisory +""" +type SecurityAdvisory implements Node { + """ + The classification of the advisory + """ + classification: SecurityAdvisoryClassification! + + """ + The CVSS associated with this advisory + """ + cvss: CVSS! + @deprecated( + reason: "`cvss` will be removed. New `cvss_severities` field will now contain both `cvss_v3` and `cvss_v4` properties. Removal on 2025-10-01 UTC." + ) + + """ + The CVSS associated with this advisory + """ + cvssSeverities: CvssSeverities! + + """ + CWEs associated with this Advisory + """ + cwes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CWEConnection! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + This is a long plaintext description of the advisory + """ + description: String! + + """ + The Exploit Prediction Scoring System + """ + epss: EPSS + + """ + The GitHub Security Advisory ID + """ + ghsaId: String! + + """ + The Node ID of the SecurityAdvisory object + """ + id: ID! + + """ + A list of identifiers for this advisory + """ + identifiers: [SecurityAdvisoryIdentifier!]! + + """ + The permalink for the advisory's dependabot alerts page + """ + notificationsPermalink: URI + + """ + The organization that originated the advisory + """ + origin: String! + + """ + The permalink for the advisory + """ + permalink: URI + + """ + When the advisory was published + """ + publishedAt: DateTime! + + """ + A list of references for this advisory + """ + references: [SecurityAdvisoryReference!]! + + """ + The severity of the advisory + """ + severity: SecurityAdvisorySeverity! + + """ + A short plaintext summary of the advisory + """ + summary: String! + + """ + When the advisory was last updated + """ + updatedAt: DateTime! + + """ + Vulnerabilities associated with this Advisory + """ + vulnerabilities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + A list of advisory classifications to filter vulnerabilities by. + """ + classifications: [SecurityAdvisoryClassification!] + + """ + An ecosystem to filter vulnerabilities by. + """ + ecosystem: SecurityAdvisoryEcosystem + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityVulnerabilityOrder = {field: UPDATED_AT, direction: DESC} + + """ + A package name to filter vulnerabilities by. + """ + package: String + + """ + A list of severities to filter vulnerabilities by. + """ + severities: [SecurityAdvisorySeverity!] + ): SecurityVulnerabilityConnection! + + """ + When the advisory was withdrawn, if it has been withdrawn + """ + withdrawnAt: DateTime +} + +""" +Classification of the advisory. +""" +enum SecurityAdvisoryClassification { + """ + Classification of general advisories. + """ + GENERAL + + """ + Classification of malware advisories. + """ + MALWARE +} + +""" +The connection type for SecurityAdvisory. +""" +type SecurityAdvisoryConnection { + """ + A list of edges. + """ + edges: [SecurityAdvisoryEdge] + + """ + A list of nodes. + """ + nodes: [SecurityAdvisory] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The possible ecosystems of a security vulnerability's package. +""" +enum SecurityAdvisoryEcosystem { + """ + GitHub Actions + """ + ACTIONS + + """ + PHP packages hosted at packagist.org + """ + COMPOSER + + """ + Erlang/Elixir packages hosted at hex.pm + """ + ERLANG + + """ + Go modules + """ + GO + + """ + Java artifacts hosted at the Maven central repository + """ + MAVEN + + """ + JavaScript packages hosted at npmjs.com + """ + NPM + + """ + .NET packages hosted at the NuGet Gallery + """ + NUGET + + """ + Python packages hosted at PyPI.org + """ + PIP + + """ + Dart packages hosted at pub.dev + """ + PUB + + """ + Ruby gems hosted at RubyGems.org + """ + RUBYGEMS + + """ + Rust crates + """ + RUST + + """ + Swift packages + """ + SWIFT +} + +""" +An edge in a connection. +""" +type SecurityAdvisoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SecurityAdvisory +} + +""" +A GitHub Security Advisory Identifier +""" +type SecurityAdvisoryIdentifier { + """ + The identifier type, e.g. GHSA, CVE + """ + type: String! + + """ + The identifier + """ + value: String! +} + +""" +An advisory identifier to filter results on. +""" +input SecurityAdvisoryIdentifierFilter { + """ + The identifier type. + """ + type: SecurityAdvisoryIdentifierType! + + """ + The identifier string. Supports exact or partial matching. + """ + value: String! +} + +""" +Identifier formats available for advisories. +""" +enum SecurityAdvisoryIdentifierType { + """ + Common Vulnerabilities and Exposures Identifier. + """ + CVE + + """ + GitHub Security Advisory ID. + """ + GHSA +} + +""" +Ordering options for security advisory connections +""" +input SecurityAdvisoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order security advisories by. + """ + field: SecurityAdvisoryOrderField! +} + +""" +Properties by which security advisory connections can be ordered. +""" +enum SecurityAdvisoryOrderField { + """ + Order advisories by EPSS percentage + """ + EPSS_PERCENTAGE + + """ + Order advisories by EPSS percentile + """ + EPSS_PERCENTILE + + """ + Order advisories by publication time + """ + PUBLISHED_AT + + """ + Order advisories by update time + """ + UPDATED_AT +} + +""" +An individual package +""" +type SecurityAdvisoryPackage { + """ + The ecosystem the package belongs to, e.g. RUBYGEMS, NPM + """ + ecosystem: SecurityAdvisoryEcosystem! + + """ + The package name + """ + name: String! +} + +""" +An individual package version +""" +type SecurityAdvisoryPackageVersion { + """ + The package name or version + """ + identifier: String! +} + +""" +A GitHub Security Advisory Reference +""" +type SecurityAdvisoryReference { + """ + A publicly accessible reference + """ + url: URI! +} + +""" +Severity of the vulnerability. +""" +enum SecurityAdvisorySeverity { + """ + Critical. + """ + CRITICAL + + """ + High. + """ + HIGH + + """ + Low. + """ + LOW + + """ + Moderate. + """ + MODERATE +} + +""" +An individual vulnerability within an Advisory +""" +type SecurityVulnerability { + """ + The Advisory associated with this Vulnerability + """ + advisory: SecurityAdvisory! + + """ + The first version containing a fix for the vulnerability + """ + firstPatchedVersion: SecurityAdvisoryPackageVersion + + """ + A description of the vulnerable package + """ + package: SecurityAdvisoryPackage! + + """ + The severity of the vulnerability within this package + """ + severity: SecurityAdvisorySeverity! + + """ + When the vulnerability was last updated + """ + updatedAt: DateTime! + + """ + A string that describes the vulnerable package versions. + This string follows a basic syntax with a few forms. + + `= 0.2.0` denotes a single vulnerable version. + + `<= 1.0.8` denotes a version range up to and including the specified version + + `< 0.1.11` denotes a version range up to, but excluding, the specified version + + `>= 4.3.0, < 4.3.5` denotes a version range with a known minimum and maximum version. + + `>= 0.0.1` denotes a version range with a known minimum, but no known maximum + """ + vulnerableVersionRange: String! +} + +""" +The connection type for SecurityVulnerability. +""" +type SecurityVulnerabilityConnection { + """ + A list of edges. + """ + edges: [SecurityVulnerabilityEdge] + + """ + A list of nodes. + """ + nodes: [SecurityVulnerability] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SecurityVulnerabilityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SecurityVulnerability +} + +""" +Ordering options for security vulnerability connections +""" +input SecurityVulnerabilityOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order security vulnerabilities by. + """ + field: SecurityVulnerabilityOrderField! +} + +""" +Properties by which security vulnerability connections can be ordered. +""" +enum SecurityVulnerabilityOrderField { + """ + Order vulnerability by update time + """ + UPDATED_AT +} + +""" +Autogenerated input type of SetEnterpriseIdentityProvider +""" +input SetEnterpriseIdentityProviderInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The digest algorithm used to sign SAML requests for the identity provider. + """ + digestMethod: SamlDigestAlgorithm! + + """ + The ID of the enterprise on which to set an identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The x509 certificate used by the identity provider to sign assertions and responses. + """ + idpCertificate: String! + + """ + The Issuer Entity ID for the SAML identity provider + """ + issuer: String + + """ + The signature algorithm used to sign SAML requests for the identity provider. + """ + signatureMethod: SamlSignatureAlgorithm! + + """ + The URL endpoint for the identity provider's SAML SSO. + """ + ssoUrl: URI! +} + +""" +Autogenerated return type of SetEnterpriseIdentityProvider. +""" +type SetEnterpriseIdentityProviderPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider for the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of SetOrganizationInteractionLimit +""" +input SetOrganizationInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the organization to set a limit for. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of SetOrganizationInteractionLimit. +""" +type SetOrganizationInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization that the interaction limit was set for. + """ + organization: Organization +} + +""" +Autogenerated input type of SetRepositoryInteractionLimit +""" +input SetRepositoryInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the repository to set a limit for. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of SetRepositoryInteractionLimit. +""" +type SetRepositoryInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that the interaction limit was set for. + """ + repository: Repository +} + +""" +Autogenerated input type of SetUserInteractionLimit +""" +input SetUserInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the user to set a limit for. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of SetUserInteractionLimit. +""" +type SetUserInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that the interaction limit was set for. + """ + user: User +} + +""" +Represents an S/MIME signature on a Commit or Tag. +""" +type SmimeSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + The date the signature was verified, if valid + """ + verifiedAt: DateTime + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Social media profile associated with a user. +""" +type SocialAccount { + """ + Name of the social media account as it appears on the profile. + """ + displayName: String! + + """ + Software or company that hosts the social media account. + """ + provider: SocialAccountProvider! + + """ + URL of the social media account. + """ + url: URI! +} + +""" +The connection type for SocialAccount. +""" +type SocialAccountConnection { + """ + A list of edges. + """ + edges: [SocialAccountEdge] + + """ + A list of nodes. + """ + nodes: [SocialAccount] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SocialAccountEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SocialAccount +} + +""" +Software or company that hosts social media accounts. +""" +enum SocialAccountProvider { + """ + Decentralized microblogging social platform. + """ + BLUESKY + + """ + Social media and networking website. + """ + FACEBOOK + + """ + Catch-all for social media providers that do not yet have specific handling. + """ + GENERIC + + """ + Fork of Mastodon with a greater focus on local posting. + """ + HOMETOWN + + """ + Social media website with a focus on photo and video sharing. + """ + INSTAGRAM + + """ + Professional networking website. + """ + LINKEDIN + + """ + Open-source federated microblogging service. + """ + MASTODON + + """ + JavaScript package registry. + """ + NPM + + """ + Social news aggregation and discussion website. + """ + REDDIT + + """ + Live-streaming service. + """ + TWITCH + + """ + Microblogging website. + """ + TWITTER + + """ + Online video platform. + """ + YOUTUBE +} + +""" +Entities that can sponsor others via GitHub Sponsors +""" +union Sponsor = Organization | User + +""" +A GitHub account and the total amount in USD they've paid for sponsorships to a +particular maintainer. Does not include payments made via Patreon. +""" +type SponsorAndLifetimeValue { + """ + The amount in cents. + """ + amountInCents: Int! + + """ + The amount in USD, formatted as a string. + """ + formattedAmount: String! + + """ + The sponsor's GitHub account. + """ + sponsor: Sponsorable! + + """ + The maintainer's GitHub account. + """ + sponsorable: Sponsorable! +} + +""" +The connection type for SponsorAndLifetimeValue. +""" +type SponsorAndLifetimeValueConnection { + """ + A list of edges. + """ + edges: [SponsorAndLifetimeValueEdge] + + """ + A list of nodes. + """ + nodes: [SponsorAndLifetimeValue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorAndLifetimeValueEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorAndLifetimeValue +} + +""" +Ordering options for connections to get sponsor entities and associated USD amounts for GitHub Sponsors. +""" +input SponsorAndLifetimeValueOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order results by. + """ + field: SponsorAndLifetimeValueOrderField! +} + +""" +Properties by which sponsor and lifetime value connections can be ordered. +""" +enum SponsorAndLifetimeValueOrderField { + """ + Order results by how much money the sponsor has paid in total. + """ + LIFETIME_VALUE + + """ + Order results by the sponsor's login (username). + """ + SPONSOR_LOGIN + + """ + Order results by the sponsor's relevance to the viewer. + """ + SPONSOR_RELEVANCE +} + +""" +A list of users and organizations sponsoring someone via GitHub Sponsors. +""" +type SponsorConnection { + """ + A list of edges. + """ + edges: [SponsorEdge] + + """ + A list of nodes. + """ + nodes: [Sponsor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user or organization who is sponsoring someone in GitHub Sponsors. +""" +type SponsorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Sponsor +} + +""" +Ordering options for connections to get sponsor entities for GitHub Sponsors. +""" +input SponsorOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsor entities by. + """ + field: SponsorOrderField! +} + +""" +Properties by which sponsor connections can be ordered. +""" +enum SponsorOrderField { + """ + Order sponsorable entities by login (username). + """ + LOGIN + + """ + Order sponsors by their relevance to the viewer. + """ + RELEVANCE +} + +""" +Entities that can sponsor or be sponsored through GitHub Sponsors. +""" +interface Sponsorable { + """ + The estimated next GitHub Sponsors payout for this user/organization in cents (USD). + """ + estimatedNextSponsorsPayoutInCents: Int! + + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + + """ + Whether the given account is sponsoring this user/organization. + """ + isSponsoredBy( + """ + The target account's login. + """ + accountLogin: String! + ): Boolean! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + Calculate how much each sponsor has ever paid total to this maintainer via + GitHub Sponsors. Does not include sponsorships paid via Patreon. + """ + lifetimeReceivedSponsorshipValues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for results returned from the connection. + """ + orderBy: SponsorAndLifetimeValueOrder = {field: SPONSOR_LOGIN, direction: ASC} + ): SponsorAndLifetimeValueConnection! + + """ + The estimated monthly GitHub Sponsors income for this user/organization in cents (USD). + """ + monthlyEstimatedSponsorsIncomeInCents: Int! + + """ + List of users and organizations this entity is sponsoring. + """ + sponsoring( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the users and organizations returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + ): SponsorConnection! + + """ + List of sponsors for this user or organization. + """ + sponsors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsors returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + + """ + If given, will filter for sponsors at the given tier. Will only return + sponsors whose tier the viewer is permitted to see. + """ + tierId: ID + ): SponsorConnection! + + """ + Events involving this sponsorable, such as new sponsorships. + """ + sponsorsActivities( + """ + Filter activities to only the specified actions. + """ + actions: [SponsorsActivityAction!] = [] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to include those events where this sponsorable acted as the sponsor. + Defaults to only including events where this sponsorable was the recipient + of a sponsorship. + """ + includeAsSponsor: Boolean = false + + """ + Whether or not to include private activities in the result set. Defaults to including public and private activities. + """ + includePrivate: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for activity returned from the connection. + """ + orderBy: SponsorsActivityOrder = {field: TIMESTAMP, direction: DESC} + + """ + Filter activities returned to only those that occurred in the most recent + specified time period. Set to ALL to avoid filtering by when the activity + occurred. Will be ignored if `since` or `until` is given. + """ + period: SponsorsActivityPeriod = MONTH + + """ + Filter activities to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter activities to those that occurred before this time. + """ + until: DateTime + ): SponsorsActivityConnection! + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + The sponsorship from the viewer to this user/organization; that is, the sponsorship where you're the sponsor. + """ + sponsorshipForViewerAsSponsor( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the viewer's sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + The sponsorship from this user/organization to the viewer; that is, the sponsorship you're receiving. + """ + sponsorshipForViewerAsSponsorable( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + List of sponsorship updates sent from this sponsorable to sponsors. + """ + sponsorshipNewsletters( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorship updates returned from the connection. + """ + orderBy: SponsorshipNewsletterOrder = {field: CREATED_AT, direction: DESC} + ): SponsorshipNewsletterConnection! + + """ + The sponsorships where this user or organization is the maintainer receiving the funds. + """ + sponsorshipsAsMaintainer( + """ + Whether to include only sponsorships that are active right now, versus all + sponsorships this maintainer has ever received. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + The sponsorships where this user or organization is the funder. + """ + sponsorshipsAsSponsor( + """ + Whether to include only sponsorships that are active right now, versus all sponsorships this sponsor has ever made. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter sponsorships returned to those for the specified maintainers. That + is, the recipient of the sponsorship is a user or organization with one of + the given logins. + """ + maintainerLogins: [String!] + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + The amount in United States cents (e.g., 500 = $5.00 USD) that this entity has + spent on GitHub to fund sponsorships. Only returns a value when viewed by the + user themselves or by a user who can manage sponsorships for the requested organization. + """ + totalSponsorshipAmountAsSponsorInCents( + """ + Filter payments to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter payments to those made to the users or organizations with the specified usernames. + """ + sponsorableLogins: [String!] = [] + + """ + Filter payments to those that occurred before this time. + """ + until: DateTime + ): Int + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! +} + +""" +Entities that can be sponsored via GitHub Sponsors +""" +union SponsorableItem = Organization | User + +""" +The connection type for SponsorableItem. +""" +type SponsorableItemConnection { + """ + A list of edges. + """ + edges: [SponsorableItemEdge] + + """ + A list of nodes. + """ + nodes: [SponsorableItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorableItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorableItem +} + +""" +Ordering options for connections to get sponsorable entities for GitHub Sponsors. +""" +input SponsorableOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsorable entities by. + """ + field: SponsorableOrderField! +} + +""" +Properties by which sponsorable connections can be ordered. +""" +enum SponsorableOrderField { + """ + Order sponsorable entities by login (username). + """ + LOGIN +} + +""" +An event related to sponsorship activity. +""" +type SponsorsActivity implements Node { + """ + What action this activity indicates took place. + """ + action: SponsorsActivityAction! + + """ + The sponsor's current privacy level. + """ + currentPrivacyLevel: SponsorshipPrivacy + + """ + The Node ID of the SponsorsActivity object + """ + id: ID! + + """ + The platform that was used to pay for the sponsorship. + """ + paymentSource: SponsorshipPaymentSource + + """ + The tier that the sponsorship used to use, for tier change events. + """ + previousSponsorsTier: SponsorsTier + + """ + The user or organization who triggered this activity and was/is sponsoring the sponsorable. + """ + sponsor: Sponsor + + """ + The user or organization that is being sponsored, the maintainer. + """ + sponsorable: Sponsorable! + + """ + The associated sponsorship tier. + """ + sponsorsTier: SponsorsTier + + """ + The timestamp of this event. + """ + timestamp: DateTime + + """ + Was this sponsorship made alongside other sponsorships at the same time from the same sponsor? + """ + viaBulkSponsorship: Boolean! +} + +""" +The possible actions that GitHub Sponsors activities can represent. +""" +enum SponsorsActivityAction { + """ + The activity was cancelling a sponsorship. + """ + CANCELLED_SPONSORSHIP + + """ + The activity was starting a sponsorship. + """ + NEW_SPONSORSHIP + + """ + The activity was scheduling a downgrade or cancellation. + """ + PENDING_CHANGE + + """ + The activity was funds being refunded to the sponsor or GitHub. + """ + REFUND + + """ + The activity was disabling matching for a previously matched sponsorship. + """ + SPONSOR_MATCH_DISABLED + + """ + The activity was changing the sponsorship tier, either directly by the sponsor or by a scheduled/pending change. + """ + TIER_CHANGE +} + +""" +The connection type for SponsorsActivity. +""" +type SponsorsActivityConnection { + """ + A list of edges. + """ + edges: [SponsorsActivityEdge] + + """ + A list of nodes. + """ + nodes: [SponsorsActivity] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorsActivityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorsActivity +} + +""" +Ordering options for GitHub Sponsors activity connections. +""" +input SponsorsActivityOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order activity by. + """ + field: SponsorsActivityOrderField! +} + +""" +Properties by which GitHub Sponsors activity connections can be ordered. +""" +enum SponsorsActivityOrderField { + """ + Order activities by when they happened. + """ + TIMESTAMP +} + +""" +The possible time periods for which Sponsors activities can be requested. +""" +enum SponsorsActivityPeriod { + """ + Don't restrict the activity to any date range, include all activity. + """ + ALL + + """ + The previous calendar day. + """ + DAY + + """ + The previous thirty days. + """ + MONTH + + """ + The previous seven days. + """ + WEEK +} + +""" +Represents countries or regions for billing and residence for a GitHub Sponsors profile. +""" +enum SponsorsCountryOrRegionCode { + """ + Andorra + """ + AD + + """ + United Arab Emirates + """ + AE + + """ + Afghanistan + """ + AF + + """ + Antigua and Barbuda + """ + AG + + """ + Anguilla + """ + AI + + """ + Albania + """ + AL + + """ + Armenia + """ + AM + + """ + Angola + """ + AO + + """ + Antarctica + """ + AQ + + """ + Argentina + """ + AR + + """ + American Samoa + """ + AS + + """ + Austria + """ + AT + + """ + Australia + """ + AU + + """ + Aruba + """ + AW + + """ + Åland + """ + AX + + """ + Azerbaijan + """ + AZ + + """ + Bosnia and Herzegovina + """ + BA + + """ + Barbados + """ + BB + + """ + Bangladesh + """ + BD + + """ + Belgium + """ + BE + + """ + Burkina Faso + """ + BF + + """ + Bulgaria + """ + BG + + """ + Bahrain + """ + BH + + """ + Burundi + """ + BI + + """ + Benin + """ + BJ + + """ + Saint Barthélemy + """ + BL + + """ + Bermuda + """ + BM + + """ + Brunei Darussalam + """ + BN + + """ + Bolivia + """ + BO + + """ + Bonaire, Sint Eustatius and Saba + """ + BQ + + """ + Brazil + """ + BR + + """ + Bahamas + """ + BS + + """ + Bhutan + """ + BT + + """ + Bouvet Island + """ + BV + + """ + Botswana + """ + BW + + """ + Belarus + """ + BY + + """ + Belize + """ + BZ + + """ + Canada + """ + CA + + """ + Cocos (Keeling) Islands + """ + CC + + """ + Congo (Kinshasa) + """ + CD + + """ + Central African Republic + """ + CF + + """ + Congo (Brazzaville) + """ + CG + + """ + Switzerland + """ + CH + + """ + Côte d'Ivoire + """ + CI + + """ + Cook Islands + """ + CK + + """ + Chile + """ + CL + + """ + Cameroon + """ + CM + + """ + China + """ + CN + + """ + Colombia + """ + CO + + """ + Costa Rica + """ + CR + + """ + Cape Verde + """ + CV + + """ + Curaçao + """ + CW + + """ + Christmas Island + """ + CX + + """ + Cyprus + """ + CY + + """ + Czech Republic + """ + CZ + + """ + Germany + """ + DE + + """ + Djibouti + """ + DJ + + """ + Denmark + """ + DK + + """ + Dominica + """ + DM + + """ + Dominican Republic + """ + DO + + """ + Algeria + """ + DZ + + """ + Ecuador + """ + EC + + """ + Estonia + """ + EE + + """ + Egypt + """ + EG + + """ + Western Sahara + """ + EH + + """ + Eritrea + """ + ER + + """ + Spain + """ + ES + + """ + Ethiopia + """ + ET + + """ + Finland + """ + FI + + """ + Fiji + """ + FJ + + """ + Falkland Islands + """ + FK + + """ + Micronesia + """ + FM + + """ + Faroe Islands + """ + FO + + """ + France + """ + FR + + """ + Gabon + """ + GA + + """ + United Kingdom + """ + GB + + """ + Grenada + """ + GD + + """ + Georgia + """ + GE + + """ + French Guiana + """ + GF + + """ + Guernsey + """ + GG + + """ + Ghana + """ + GH + + """ + Gibraltar + """ + GI + + """ + Greenland + """ + GL + + """ + Gambia + """ + GM + + """ + Guinea + """ + GN + + """ + Guadeloupe + """ + GP + + """ + Equatorial Guinea + """ + GQ + + """ + Greece + """ + GR + + """ + South Georgia and South Sandwich Islands + """ + GS + + """ + Guatemala + """ + GT + + """ + Guam + """ + GU + + """ + Guinea-Bissau + """ + GW + + """ + Guyana + """ + GY + + """ + Hong Kong + """ + HK + + """ + Heard and McDonald Islands + """ + HM + + """ + Honduras + """ + HN + + """ + Croatia + """ + HR + + """ + Haiti + """ + HT + + """ + Hungary + """ + HU + + """ + Indonesia + """ + ID + + """ + Ireland + """ + IE + + """ + Israel + """ + IL + + """ + Isle of Man + """ + IM + + """ + India + """ + IN + + """ + British Indian Ocean Territory + """ + IO + + """ + Iraq + """ + IQ + + """ + Iran + """ + IR + + """ + Iceland + """ + IS + + """ + Italy + """ + IT + + """ + Jersey + """ + JE + + """ + Jamaica + """ + JM + + """ + Jordan + """ + JO + + """ + Japan + """ + JP + + """ + Kenya + """ + KE + + """ + Kyrgyzstan + """ + KG + + """ + Cambodia + """ + KH + + """ + Kiribati + """ + KI + + """ + Comoros + """ + KM + + """ + Saint Kitts and Nevis + """ + KN + + """ + Korea, South + """ + KR + + """ + Kuwait + """ + KW + + """ + Cayman Islands + """ + KY + + """ + Kazakhstan + """ + KZ + + """ + Laos + """ + LA + + """ + Lebanon + """ + LB + + """ + Saint Lucia + """ + LC + + """ + Liechtenstein + """ + LI + + """ + Sri Lanka + """ + LK + + """ + Liberia + """ + LR + + """ + Lesotho + """ + LS + + """ + Lithuania + """ + LT + + """ + Luxembourg + """ + LU + + """ + Latvia + """ + LV + + """ + Libya + """ + LY + + """ + Morocco + """ + MA + + """ + Monaco + """ + MC + + """ + Moldova + """ + MD + + """ + Montenegro + """ + ME + + """ + Saint Martin (French part) + """ + MF + + """ + Madagascar + """ + MG + + """ + Marshall Islands + """ + MH + + """ + Macedonia + """ + MK + + """ + Mali + """ + ML + + """ + Myanmar + """ + MM + + """ + Mongolia + """ + MN + + """ + Macau + """ + MO + + """ + Northern Mariana Islands + """ + MP + + """ + Martinique + """ + MQ + + """ + Mauritania + """ + MR + + """ + Montserrat + """ + MS + + """ + Malta + """ + MT + + """ + Mauritius + """ + MU + + """ + Maldives + """ + MV + + """ + Malawi + """ + MW + + """ + Mexico + """ + MX + + """ + Malaysia + """ + MY + + """ + Mozambique + """ + MZ + + """ + Namibia + """ + NA + + """ + New Caledonia + """ + NC + + """ + Niger + """ + NE + + """ + Norfolk Island + """ + NF + + """ + Nigeria + """ + NG + + """ + Nicaragua + """ + NI + + """ + Netherlands + """ + NL + + """ + Norway + """ + NO + + """ + Nepal + """ + NP + + """ + Nauru + """ + NR + + """ + Niue + """ + NU + + """ + New Zealand + """ + NZ + + """ + Oman + """ + OM + + """ + Panama + """ + PA + + """ + Peru + """ + PE + + """ + French Polynesia + """ + PF + + """ + Papua New Guinea + """ + PG + + """ + Philippines + """ + PH + + """ + Pakistan + """ + PK + + """ + Poland + """ + PL + + """ + Saint Pierre and Miquelon + """ + PM + + """ + Pitcairn + """ + PN + + """ + Puerto Rico + """ + PR + + """ + Palestine + """ + PS + + """ + Portugal + """ + PT + + """ + Palau + """ + PW + + """ + Paraguay + """ + PY + + """ + Qatar + """ + QA + + """ + Reunion + """ + RE + + """ + Romania + """ + RO + + """ + Serbia + """ + RS + + """ + Russian Federation + """ + RU + + """ + Rwanda + """ + RW + + """ + Saudi Arabia + """ + SA + + """ + Solomon Islands + """ + SB + + """ + Seychelles + """ + SC + + """ + Sudan + """ + SD + + """ + Sweden + """ + SE + + """ + Singapore + """ + SG + + """ + Saint Helena + """ + SH + + """ + Slovenia + """ + SI + + """ + Svalbard and Jan Mayen Islands + """ + SJ + + """ + Slovakia + """ + SK + + """ + Sierra Leone + """ + SL + + """ + San Marino + """ + SM + + """ + Senegal + """ + SN + + """ + Somalia + """ + SO + + """ + Suriname + """ + SR + + """ + South Sudan + """ + SS + + """ + Sao Tome and Principe + """ + ST + + """ + El Salvador + """ + SV + + """ + Sint Maarten (Dutch part) + """ + SX + + """ + Swaziland + """ + SZ + + """ + Turks and Caicos Islands + """ + TC + + """ + Chad + """ + TD + + """ + French Southern Lands + """ + TF + + """ + Togo + """ + TG + + """ + Thailand + """ + TH + + """ + Tajikistan + """ + TJ + + """ + Tokelau + """ + TK + + """ + Timor-Leste + """ + TL + + """ + Turkmenistan + """ + TM + + """ + Tunisia + """ + TN + + """ + Tonga + """ + TO + + """ + Türkiye + """ + TR + + """ + Trinidad and Tobago + """ + TT + + """ + Tuvalu + """ + TV + + """ + Taiwan + """ + TW + + """ + Tanzania + """ + TZ + + """ + Ukraine + """ + UA + + """ + Uganda + """ + UG + + """ + United States Minor Outlying Islands + """ + UM + + """ + United States of America + """ + US + + """ + Uruguay + """ + UY + + """ + Uzbekistan + """ + UZ + + """ + Vatican City + """ + VA + + """ + Saint Vincent and the Grenadines + """ + VC + + """ + Venezuela + """ + VE + + """ + Virgin Islands, British + """ + VG + + """ + Virgin Islands, U.S. + """ + VI + + """ + Vietnam + """ + VN + + """ + Vanuatu + """ + VU + + """ + Wallis and Futuna Islands + """ + WF + + """ + Samoa + """ + WS + + """ + Yemen + """ + YE + + """ + Mayotte + """ + YT + + """ + South Africa + """ + ZA + + """ + Zambia + """ + ZM + + """ + Zimbabwe + """ + ZW +} + +""" +A goal associated with a GitHub Sponsors listing, representing a target the sponsored maintainer would like to attain. +""" +type SponsorsGoal { + """ + A description of the goal from the maintainer. + """ + description: String + + """ + What the objective of this goal is. + """ + kind: SponsorsGoalKind! + + """ + The percentage representing how complete this goal is, between 0-100. + """ + percentComplete: Int! + + """ + What the goal amount is. Represents an amount in USD for monthly sponsorship + amount goals. Represents a count of unique sponsors for total sponsors count goals. + """ + targetValue: Int! + + """ + A brief summary of the kind and target value of this goal. + """ + title: String! +} + +""" +The different kinds of goals a GitHub Sponsors member can have. +""" +enum SponsorsGoalKind { + """ + The goal is about getting a certain amount in USD from sponsorships each month. + """ + MONTHLY_SPONSORSHIP_AMOUNT + + """ + The goal is about reaching a certain number of sponsors. + """ + TOTAL_SPONSORS_COUNT +} + +""" +A GitHub Sponsors listing. +""" +type SponsorsListing implements Node { + """ + The current goal the maintainer is trying to reach with GitHub Sponsors, if any. + """ + activeGoal: SponsorsGoal + + """ + The Stripe Connect account currently in use for payouts for this Sponsors + listing, if any. Will only return a value when queried by the maintainer + themselves, or by an admin of the sponsorable organization. + """ + activeStripeConnectAccount: StripeConnectAccount + + """ + The name of the country or region with the maintainer's bank account or fiscal + host. Will only return a value when queried by the maintainer themselves, or + by an admin of the sponsorable organization. + """ + billingCountryOrRegion: String + + """ + The email address used by GitHub to contact the sponsorable about their GitHub + Sponsors profile. Will only return a value when queried by the maintainer + themselves, or by an admin of the sponsorable organization. + """ + contactEmailAddress: String + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The HTTP path for the Sponsors dashboard for this Sponsors listing. + """ + dashboardResourcePath: URI! + + """ + The HTTP URL for the Sponsors dashboard for this Sponsors listing. + """ + dashboardUrl: URI! + + """ + The records featured on the GitHub Sponsors profile. + """ + featuredItems( + """ + The types of featured items to return. + """ + featureableTypes: [SponsorsListingFeaturedItemFeatureableType!] = [REPOSITORY, USER] + ): [SponsorsListingFeaturedItem!]! + + """ + The fiscal host used for payments, if any. Will only return a value when + queried by the maintainer themselves, or by an admin of the sponsorable organization. + """ + fiscalHost: Organization + + """ + The full description of the listing. + """ + fullDescription: String! + + """ + The full description of the listing rendered to HTML. + """ + fullDescriptionHTML: HTML! + + """ + The Node ID of the SponsorsListing object + """ + id: ID! + + """ + Whether this listing is publicly visible. + """ + isPublic: Boolean! + + """ + The listing's full name. + """ + name: String! + + """ + A future date on which this listing is eligible to receive a payout. + """ + nextPayoutDate: Date + + """ + The name of the country or region where the maintainer resides. Will only + return a value when queried by the maintainer themselves, or by an admin of + the sponsorable organization. + """ + residenceCountryOrRegion: String + + """ + The HTTP path for this Sponsors listing. + """ + resourcePath: URI! + + """ + The short description of the listing. + """ + shortDescription: String! + + """ + The short name of the listing. + """ + slug: String! + + """ + The entity this listing represents who can be sponsored on GitHub Sponsors. + """ + sponsorable: Sponsorable! + + """ + The tiers for this GitHub Sponsors profile. + """ + tiers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to include tiers that aren't published. Only admins of the Sponsors + listing can see draft tiers. Only admins of the Sponsors listing and viewers + who are currently sponsoring on a retired tier can see those retired tiers. + Defaults to including only published tiers, which are visible to anyone who + can see the GitHub Sponsors profile. + """ + includeUnpublished: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Sponsors tiers returned from the connection. + """ + orderBy: SponsorsTierOrder = {field: MONTHLY_PRICE_IN_CENTS, direction: ASC} + ): SponsorsTierConnection + + """ + The HTTP URL for this Sponsors listing. + """ + url: URI! +} + +""" +A record that can be featured on a GitHub Sponsors profile. +""" +union SponsorsListingFeatureableItem = Repository | User + +""" +A record that is promoted on a GitHub Sponsors profile. +""" +type SponsorsListingFeaturedItem implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Will either be a description from the sponsorable maintainer about why they + featured this item, or the item's description itself, such as a user's bio + from their GitHub profile page. + """ + description: String + + """ + The record that is featured on the GitHub Sponsors profile. + """ + featureable: SponsorsListingFeatureableItem! + + """ + The Node ID of the SponsorsListingFeaturedItem object + """ + id: ID! + + """ + The position of this featured item on the GitHub Sponsors profile with a lower + position indicating higher precedence. Starts at 1. + """ + position: Int! + + """ + The GitHub Sponsors profile that features this record. + """ + sponsorsListing: SponsorsListing! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The different kinds of records that can be featured on a GitHub Sponsors profile page. +""" +enum SponsorsListingFeaturedItemFeatureableType { + """ + A repository owned by the user or organization with the GitHub Sponsors profile. + """ + REPOSITORY + + """ + A user who belongs to the organization with the GitHub Sponsors profile. + """ + USER +} + +""" +A GitHub Sponsors tier associated with a GitHub Sponsors listing. +""" +type SponsorsTier implements Node { + """ + SponsorsTier information only visible to users that can administer the associated Sponsors listing. + """ + adminInfo: SponsorsTierAdminInfo + + """ + Get a different tier for this tier's maintainer that is at the same frequency + as this tier but with an equal or lesser cost. Returns the published tier with + the monthly price closest to this tier's without going over. + """ + closestLesserValueTier: SponsorsTier + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The description of the tier. + """ + description: String! + + """ + The tier description rendered to HTML + """ + descriptionHTML: HTML! + + """ + The Node ID of the SponsorsTier object + """ + id: ID! + + """ + Whether this tier was chosen at checkout time by the sponsor rather than + defined ahead of time by the maintainer who manages the Sponsors listing. + """ + isCustomAmount: Boolean! + + """ + Whether this tier is only for use with one-time sponsorships. + """ + isOneTime: Boolean! + + """ + How much this tier costs per month in cents. + """ + monthlyPriceInCents: Int! + + """ + How much this tier costs per month in USD. + """ + monthlyPriceInDollars: Int! + + """ + The name of the tier. + """ + name: String! + + """ + The sponsors listing that this tier belongs to. + """ + sponsorsListing: SponsorsListing! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +SponsorsTier information only visible to users that can administer the associated Sponsors listing. +""" +type SponsorsTierAdminInfo { + """ + Indicates whether this tier is still a work in progress by the sponsorable and + not yet published to the associated GitHub Sponsors profile. Draft tiers + cannot be used for new sponsorships and will not be in use on existing + sponsorships. Draft tiers cannot be seen by anyone but the admins of the + GitHub Sponsors profile. + """ + isDraft: Boolean! + + """ + Indicates whether this tier is published to the associated GitHub Sponsors + profile. Published tiers are visible to anyone who can see the GitHub Sponsors + profile, and are available for use in sponsorships if the GitHub Sponsors + profile is publicly visible. + """ + isPublished: Boolean! + + """ + Indicates whether this tier has been retired from the associated GitHub + Sponsors profile. Retired tiers are no longer shown on the GitHub Sponsors + profile and cannot be chosen for new sponsorships. Existing sponsorships may + still use retired tiers if the sponsor selected the tier before it was retired. + """ + isRetired: Boolean! + + """ + The sponsorships using this tier. + """ + sponsorships( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to return private sponsorships using this tier. Defaults to + only returning public sponsorships on this tier. + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! +} + +""" +The connection type for SponsorsTier. +""" +type SponsorsTierConnection { + """ + A list of edges. + """ + edges: [SponsorsTierEdge] + + """ + A list of nodes. + """ + nodes: [SponsorsTier] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorsTierEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorsTier +} + +""" +Ordering options for Sponsors tiers connections. +""" +input SponsorsTierOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order tiers by. + """ + field: SponsorsTierOrderField! +} + +""" +Properties by which Sponsors tiers connections can be ordered. +""" +enum SponsorsTierOrderField { + """ + Order tiers by creation time. + """ + CREATED_AT + + """ + Order tiers by their monthly price in cents + """ + MONTHLY_PRICE_IN_CENTS +} + +""" +A sponsorship relationship between a sponsor and a maintainer +""" +type Sponsorship implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the Sponsorship object + """ + id: ID! + + """ + Whether the sponsorship is active. False implies the sponsor is a past sponsor + of the maintainer, while true implies they are a current sponsor. + """ + isActive: Boolean! + + """ + Whether this sponsorship represents a one-time payment versus a recurring sponsorship. + """ + isOneTimePayment: Boolean! + + """ + Whether the sponsor has chosen to receive sponsorship update emails sent from + the sponsorable. Only returns a non-null value when the viewer has permission to know this. + """ + isSponsorOptedIntoEmail: Boolean + + """ + The entity that is being sponsored + """ + maintainer: User! + @deprecated( + reason: "`Sponsorship.maintainer` will be removed. Use `Sponsorship.sponsorable` instead. Removal on 2020-04-01 UTC." + ) + + """ + The platform that was most recently used to pay for the sponsorship. + """ + paymentSource: SponsorshipPaymentSource + + """ + The privacy level for this sponsorship. + """ + privacyLevel: SponsorshipPrivacy! + + """ + The user that is sponsoring. Returns null if the sponsorship is private or if sponsor is not a user. + """ + sponsor: User + @deprecated( + reason: "`Sponsorship.sponsor` will be removed. Use `Sponsorship.sponsorEntity` instead. Removal on 2020-10-01 UTC." + ) + + """ + The user or organization that is sponsoring, if you have permission to view them. + """ + sponsorEntity: Sponsor + + """ + The entity that is being sponsored + """ + sponsorable: Sponsorable! + + """ + The associated sponsorship tier + """ + tier: SponsorsTier + + """ + Identifies the date and time when the current tier was chosen for this sponsorship. + """ + tierSelectedAt: DateTime +} + +""" +A list of sponsorships either from the subject or received by the subject. +""" +type SponsorshipConnection { + """ + A list of edges. + """ + edges: [SponsorshipEdge] + + """ + A list of nodes. + """ + nodes: [Sponsorship] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + The total amount in cents of all recurring sponsorships in the connection + whose amount you can view. Does not include one-time sponsorships. + """ + totalRecurringMonthlyPriceInCents: Int! + + """ + The total amount in USD of all recurring sponsorships in the connection whose + amount you can view. Does not include one-time sponsorships. + """ + totalRecurringMonthlyPriceInDollars: Int! +} + +""" +An edge in a connection. +""" +type SponsorshipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Sponsorship +} + +""" +An update sent to sponsors of a user or organization on GitHub Sponsors. +""" +type SponsorshipNewsletter implements Node { + """ + The author of the newsletter. + """ + author: User + + """ + The contents of the newsletter, the message the sponsorable wanted to give. + """ + body: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the SponsorshipNewsletter object + """ + id: ID! + + """ + Indicates if the newsletter has been made available to sponsors. + """ + isPublished: Boolean! + + """ + The user or organization this newsletter is from. + """ + sponsorable: Sponsorable! + + """ + The subject of the newsletter, what it's about. + """ + subject: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for SponsorshipNewsletter. +""" +type SponsorshipNewsletterConnection { + """ + A list of edges. + """ + edges: [SponsorshipNewsletterEdge] + + """ + A list of nodes. + """ + nodes: [SponsorshipNewsletter] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorshipNewsletterEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorshipNewsletter +} + +""" +Ordering options for sponsorship newsletter connections. +""" +input SponsorshipNewsletterOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsorship newsletters by. + """ + field: SponsorshipNewsletterOrderField! +} + +""" +Properties by which sponsorship update connections can be ordered. +""" +enum SponsorshipNewsletterOrderField { + """ + Order sponsorship newsletters by when they were created. + """ + CREATED_AT +} + +""" +Ordering options for sponsorship connections. +""" +input SponsorshipOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsorship by. + """ + field: SponsorshipOrderField! +} + +""" +Properties by which sponsorship connections can be ordered. +""" +enum SponsorshipOrderField { + """ + Order sponsorship by creation time. + """ + CREATED_AT +} + +""" +How payment was made for funding a GitHub Sponsors sponsorship. +""" +enum SponsorshipPaymentSource { + """ + Payment was made through GitHub. + """ + GITHUB + + """ + Payment was made through Patreon. + """ + PATREON +} + +""" +The privacy of a sponsorship +""" +enum SponsorshipPrivacy { + """ + Private + """ + PRIVATE + + """ + Public + """ + PUBLIC +} + +""" +The possible default commit messages for squash merges. +""" +enum SquashMergeCommitMessage { + """ + Default to a blank commit message. + """ + BLANK + + """ + Default to the branch's commit messages. + """ + COMMIT_MESSAGES + + """ + Default to the pull request's body. + """ + PR_BODY +} + +""" +The possible default commit titles for squash merges. +""" +enum SquashMergeCommitTitle { + """ + Default to the commit's title (if only one commit) or the pull request's title (when more than one commit). + """ + COMMIT_OR_PR_TITLE + + """ + Default to the pull request's title. + """ + PR_TITLE +} + +""" +Represents an SSH signature on a Commit or Tag. +""" +type SshSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Hex-encoded fingerprint of the key that signed this object. + """ + keyFingerprint: String + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + The date the signature was verified, if valid + """ + verifiedAt: DateTime + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Ways in which star connections can be ordered. +""" +input StarOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field in which to order nodes by. + """ + field: StarOrderField! +} + +""" +Properties by which star connections can be ordered. +""" +enum StarOrderField { + """ + Allows ordering a list of stars by when they were created. + """ + STARRED_AT +} + +""" +The connection type for User. +""" +type StargazerConnection { + """ + A list of edges. + """ + edges: [StargazerEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user that's starred a repository. +""" +type StargazerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + Identifies when the item was starred. + """ + starredAt: DateTime! +} + +""" +Things that can be starred. +""" +interface Starrable { + """ + The Node ID of the Starrable object + """ + id: ID! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +The connection type for Repository. +""" +type StarredRepositoryConnection { + """ + A list of edges. + """ + edges: [StarredRepositoryEdge] + + """ + Is the list of stars for this user truncated? This is true for users that have many stars. + """ + isOverLimit: Boolean! + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a starred repository. +""" +type StarredRepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: Repository! + + """ + Identifies when the item was starred. + """ + starredAt: DateTime! +} + +""" +Autogenerated input type of StartOrganizationMigration +""" +input StartOrganizationMigrationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The migration source access token. + """ + sourceAccessToken: String! + + """ + The URL of the organization to migrate. + """ + sourceOrgUrl: URI! + + """ + The ID of the enterprise the target organization belongs to. + """ + targetEnterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The name of the target organization. + """ + targetOrgName: String! +} + +""" +Autogenerated return type of StartOrganizationMigration. +""" +type StartOrganizationMigrationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new organization migration. + """ + orgMigration: OrganizationMigration +} + +""" +Autogenerated input type of StartRepositoryMigration +""" +input StartRepositoryMigrationInput { + """ + The migration source access token. + """ + accessToken: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether to continue the migration on error. Defaults to `true`. + """ + continueOnError: Boolean + + """ + The signed URL to access the user-uploaded git archive. + """ + gitArchiveUrl: String + + """ + The GitHub personal access token of the user importing to the target repository. + """ + githubPat: String + + """ + Whether to lock the source repository. + """ + lockSource: Boolean + + """ + The signed URL to access the user-uploaded metadata archive. + """ + metadataArchiveUrl: String + + """ + The ID of the organization that will own the imported repository. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The name of the imported repository. + """ + repositoryName: String! + + """ + Whether to skip migrating releases for the repository. + """ + skipReleases: Boolean + + """ + The ID of the migration source. + """ + sourceId: ID! @possibleTypes(concreteTypes: ["MigrationSource"]) + + """ + The URL of the source repository. + """ + sourceRepositoryUrl: URI! + + """ + The visibility of the imported repository. + """ + targetRepoVisibility: String +} + +""" +Autogenerated return type of StartRepositoryMigration. +""" +type StartRepositoryMigrationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new repository migration. + """ + repositoryMigration: RepositoryMigration +} + +""" +Represents a commit status. +""" +type Status implements Node { + """ + A list of status contexts and check runs for this commit. + """ + combinedContexts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): StatusCheckRollupContextConnection! + + """ + The commit this status is attached to. + """ + commit: Commit + + """ + Looks up an individual status context by context name. + """ + context( + """ + The context name. + """ + name: String! + ): StatusContext + + """ + The individual status contexts for this commit. + """ + contexts: [StatusContext!]! + + """ + The Node ID of the Status object + """ + id: ID! + + """ + The combined commit status. + """ + state: StatusState! +} + +""" +Required status check +""" +type StatusCheckConfiguration { + """ + The status check context name that must be present on the commit. + """ + context: String! + + """ + The optional integration ID that this status check must originate from. + """ + integrationId: Int +} + +""" +Required status check +""" +input StatusCheckConfigurationInput { + """ + The status check context name that must be present on the commit. + """ + context: String! + + """ + The optional integration ID that this status check must originate from. + """ + integrationId: Int +} + +""" +Represents the rollup for both the check runs and status for a commit. +""" +type StatusCheckRollup implements Node { + """ + The commit the status and check runs are attached to. + """ + commit: Commit + + """ + A list of status contexts and check runs for this commit. + """ + contexts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): StatusCheckRollupContextConnection! + + """ + The Node ID of the StatusCheckRollup object + """ + id: ID! + + """ + The combined status for the commit. + """ + state: StatusState! +} + +""" +Types that can be inside a StatusCheckRollup context. +""" +union StatusCheckRollupContext = CheckRun | StatusContext + +""" +The connection type for StatusCheckRollupContext. +""" +type StatusCheckRollupContextConnection { + """ + The number of check runs in this rollup. + """ + checkRunCount: Int! + + """ + Counts of check runs by state. + """ + checkRunCountsByState: [CheckRunStateCount!] + + """ + A list of edges. + """ + edges: [StatusCheckRollupContextEdge] + + """ + A list of nodes. + """ + nodes: [StatusCheckRollupContext] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The number of status contexts in this rollup. + """ + statusContextCount: Int! + + """ + Counts of status contexts by state. + """ + statusContextCountsByState: [StatusContextStateCount!] + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type StatusCheckRollupContextEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: StatusCheckRollupContext +} + +""" +Represents an individual commit status context +""" +type StatusContext implements Node & RequirableByPullRequest { + """ + The avatar of the OAuth application or the user that created the status + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int = 40 + ): URI + + """ + This commit this status context is attached to. + """ + commit: Commit + + """ + The name of this status context. + """ + context: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created this status context. + """ + creator: Actor + + """ + The description for this status context. + """ + description: String + + """ + The Node ID of the StatusContext object + """ + id: ID! + + """ + Whether this is required to pass before merging for a specific pull request. + """ + isRequired( + """ + The id of the pull request this is required for + """ + pullRequestId: ID + + """ + The number of the pull request this is required for + """ + pullRequestNumber: Int + ): Boolean! + + """ + The state of this status context. + """ + state: StatusState! + + """ + The URL for this status context. + """ + targetUrl: URI +} + +""" +Represents a count of the state of a status context. +""" +type StatusContextStateCount { + """ + The number of statuses with this state. + """ + count: Int! + + """ + The state of a status context. + """ + state: StatusState! +} + +""" +The possible commit status states. +""" +enum StatusState { + """ + Status is errored. + """ + ERROR + + """ + Status is expected. + """ + EXPECTED + + """ + Status is failing. + """ + FAILURE + + """ + Status is pending. + """ + PENDING + + """ + Status is successful. + """ + SUCCESS +} + +""" +A Stripe Connect account for receiving sponsorship funds from GitHub Sponsors. +""" +type StripeConnectAccount { + """ + The account number used to identify this Stripe Connect account. + """ + accountId: String! + + """ + The name of the country or region of an external account, such as a bank + account, tied to the Stripe Connect account. Will only return a value when + queried by the maintainer of the associated GitHub Sponsors profile + themselves, or by an admin of the sponsorable organization. + """ + billingCountryOrRegion: String + + """ + The name of the country or region of the Stripe Connect account. Will only + return a value when queried by the maintainer of the associated GitHub + Sponsors profile themselves, or by an admin of the sponsorable organization. + """ + countryOrRegion: String + + """ + Whether this Stripe Connect account is currently in use for the associated GitHub Sponsors profile. + """ + isActive: Boolean! + + """ + The GitHub Sponsors profile associated with this Stripe Connect account. + """ + sponsorsListing: SponsorsListing! + + """ + The URL to access this Stripe Connect account on Stripe's website. + """ + stripeDashboardUrl: URI! +} + +""" +Represents a 'sub_issue_added' event on a given issue. +""" +type SubIssueAddedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the SubIssueAddedEvent object + """ + id: ID! + + """ + The sub-issue added. + """ + subIssue: Issue +} + +""" +Represents a 'sub_issue_removed' event on a given issue. +""" +type SubIssueRemovedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the SubIssueRemovedEvent object + """ + id: ID! + + """ + The sub-issue removed. + """ + subIssue: Issue +} + +""" +Summary of the state of an issue's sub-issues +""" +type SubIssuesSummary { + """ + Count of completed sub-issues + """ + completed: Int! + + """ + Percent of sub-issues which are completed + """ + percentCompleted: Int! + + """ + Count of total number of sub-issues + """ + total: Int! +} + +""" +Autogenerated input type of SubmitPullRequestReview +""" +input SubmitPullRequestReviewInput { + """ + The text field to set on the Pull Request Review. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The event to send to the Pull Request Review. + """ + event: PullRequestReviewEvent! + + """ + The Pull Request ID to submit any pending reviews. + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Pull Request Review ID to submit. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of SubmitPullRequestReview. +""" +type SubmitPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The submitted pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +A pointer to a repository at a specific revision embedded inside another repository. +""" +type Submodule { + """ + The branch of the upstream submodule for tracking updates + """ + branch: String + + """ + The git URL of the submodule repository + """ + gitUrl: URI! + + """ + The name of the submodule in .gitmodules + """ + name: String! + + """ + The name of the submodule in .gitmodules (Base64-encoded) + """ + nameRaw: Base64String! + + """ + The path in the superproject that this submodule is located in + """ + path: String! + + """ + The path in the superproject that this submodule is located in (Base64-encoded) + """ + pathRaw: Base64String! + + """ + The commit revision of the subproject repository being tracked by the submodule + """ + subprojectCommitOid: GitObjectID +} + +""" +The connection type for Submodule. +""" +type SubmoduleConnection { + """ + A list of edges. + """ + edges: [SubmoduleEdge] + + """ + A list of nodes. + """ + nodes: [Submodule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SubmoduleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Submodule +} + +""" +Entities that can be subscribed to for web and email notifications. +""" +interface Subscribable { + """ + The Node ID of the Subscribable object + """ + id: ID! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +Entities that can be subscribed to for web and email notifications. +""" +interface SubscribableThread { + """ + The Node ID of the SubscribableThread object + """ + id: ID! + + """ + Identifies the viewer's thread subscription form action. + """ + viewerThreadSubscriptionFormAction: ThreadSubscriptionFormAction + + """ + Identifies the viewer's thread subscription status. + """ + viewerThreadSubscriptionStatus: ThreadSubscriptionState +} + +""" +Represents a 'subscribed' event on a given `Subscribable`. +""" +type SubscribedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the SubscribedEvent object + """ + id: ID! + + """ + Object referenced by event. + """ + subscribable: Subscribable! +} + +""" +The possible states of a subscription. +""" +enum SubscriptionState { + """ + The User is never notified. + """ + IGNORED + + """ + The User is notified of all conversations. + """ + SUBSCRIBED + + """ + The User is only notified when participating or @mentioned. + """ + UNSUBSCRIBED +} + +""" +A suggestion to review a pull request based on a user's commit history and review comments. +""" +type SuggestedReviewer { + """ + Is this suggestion based on past commits? + """ + isAuthor: Boolean! + + """ + Is this suggestion based on past review comments? + """ + isCommenter: Boolean! + + """ + Identifies the user suggested to review the pull request. + """ + reviewer: User! +} + +""" +Represents a Git tag. +""" +type Tag implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + The Node ID of the Tag object + """ + id: ID! + + """ + The Git tag message. + """ + message: String + + """ + The Git tag name. + """ + name: String! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! + + """ + Details about the tag author. + """ + tagger: GitActor + + """ + The Git object the tag points to. + """ + target: GitObject! +} + +""" +Parameters to be used for the tag_name_pattern rule +""" +type TagNamePatternParameters { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean! + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +Parameters to be used for the tag_name_pattern rule +""" +input TagNamePatternParametersInput { + """ + How this rule will appear to users. + """ + name: String + + """ + If true, the rule will fail if the pattern matches. + """ + negate: Boolean + + """ + The operator to use for matching. + """ + operator: String! + + """ + The pattern to match with. + """ + pattern: String! +} + +""" +A team of users in an organization. +""" +type Team implements MemberStatusable & Node & Subscribable { + """ + A list of teams that are ancestors of this team. + """ + ancestors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + A URL pointing to the team's avatar. + """ + avatarUrl( + """ + The size in pixels of the resulting square image. + """ + size: Int = 400 + ): URI + + """ + List of child teams belonging to this team + """ + childTeams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to list immediate child teams or all descendant child teams. + """ + immediateOnly: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamOrder + + """ + User logins to filter by + """ + userLogins: [String!] + ): TeamConnection! + + """ + The slug corresponding to the organization and team. + """ + combinedSlug: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the team. + """ + description: String + + """ + Find a team discussion by its number. + """ + discussion( + """ + The sequence number of the discussion to find. + """ + number: Int! + ): TeamDiscussion + + """ + A list of team discussions. + """ + discussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If provided, filters discussions according to whether or not they are pinned. + """ + isPinned: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamDiscussionOrder + ): TeamDiscussionConnection! + + """ + The HTTP path for team discussions + """ + discussionsResourcePath: URI! + + """ + The HTTP URL for team discussions + """ + discussionsUrl: URI! + + """ + The HTTP path for editing this team + """ + editTeamResourcePath: URI! + + """ + The HTTP URL for editing this team + """ + editTeamUrl: URI! + + """ + The Node ID of the Team object + """ + id: ID! + + """ + A list of pending invitations for users to this team + """ + invitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationInvitationConnection + + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! + + """ + A list of users who are members of this team. + """ + members( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter by membership type + """ + membership: TeamMembershipType = ALL + + """ + Order for the connection. + """ + orderBy: TeamMemberOrder + + """ + The search string to look for. + """ + query: String + + """ + Filter by team member role + """ + role: TeamMemberRole + ): TeamMemberConnection! + + """ + The HTTP path for the team' members + """ + membersResourcePath: URI! + + """ + The HTTP URL for the team' members + """ + membersUrl: URI! + + """ + The name of the team. + """ + name: String! + + """ + The HTTP path creating a new team + """ + newTeamResourcePath: URI! + + """ + The HTTP URL creating a new team + """ + newTeamUrl: URI! + + """ + The notification setting that the team has set. + """ + notificationSetting: TeamNotificationSetting! + + """ + The organization that owns this team. + """ + organization: Organization! + + """ + The parent team of the team. + """ + parentTeam: Team + + """ + The level of privacy the team has. + """ + privacy: TeamPrivacy! + + """ + Finds and returns the project according to the provided project number. + """ + projectV2( + """ + The Project number. + """ + number: Int! + ): ProjectV2 + + """ + List of projects this team has collaborator access to. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for projects returned from this connection + """ + filterBy: ProjectV2Filters = {} + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter projects based on user role. + """ + minPermissionLevel: ProjectV2PermissionLevel = READ + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + The query to search projects by. + """ + query: String = "" + ): ProjectV2Connection! + + """ + A list of repositories this team has access to. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for the connection. + """ + orderBy: TeamRepositoryOrder + + """ + The search string to look for. Repositories will be returned where the name contains your search string. + """ + query: String + ): TeamRepositoryConnection! + + """ + The HTTP path for this team's repositories + """ + repositoriesResourcePath: URI! + + """ + The HTTP URL for this team's repositories + """ + repositoriesUrl: URI! + + """ + The HTTP path for this team + """ + resourcePath: URI! + + """ + What algorithm is used for review assignment for this team + """ + reviewRequestDelegationAlgorithm: TeamReviewAssignmentAlgorithm + + """ + True if review assignment is enabled for this team + """ + reviewRequestDelegationEnabled: Boolean! + + """ + How many team members are required for review assignment for this team + """ + reviewRequestDelegationMemberCount: Int + + """ + When assigning team members via delegation, whether the entire team should be notified as well. + """ + reviewRequestDelegationNotifyTeam: Boolean! + + """ + The slug corresponding to the team. + """ + slug: String! + + """ + The HTTP path for this team's teams + """ + teamsResourcePath: URI! + + """ + The HTTP URL for this team's teams + """ + teamsUrl: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this team + """ + url: URI! + + """ + Team is adminable by the viewer. + """ + viewerCanAdminister: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +Audit log entry for a team.add_member event. +""" +type TeamAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the TeamAddMemberAuditEntry object + """ + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a team.add_repository event. +""" +type TeamAddRepositoryAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the TeamAddRepositoryAuditEntry object + """ + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Metadata for an audit entry with action team.* +""" +interface TeamAuditEntryData { + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI +} + +""" +Audit log entry for a team.change_parent_team event. +""" +type TeamChangeParentTeamAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the TeamChangeParentTeamAuditEntry object + """ + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The new parent team. + """ + parentTeam: Team + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the new parent team + """ + parentTeamName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the former parent team + """ + parentTeamNameWas: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the parent team + """ + parentTeamResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the parent team + """ + parentTeamUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The former parent team. + """ + parentTeamWas: Team + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the previous parent team + """ + parentTeamWasResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the previous parent team + """ + parentTeamWasUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The connection type for Team. +""" +type TeamConnection { + """ + A list of edges. + """ + edges: [TeamEdge] + + """ + A list of nodes. + """ + nodes: [Team] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A team discussion. +""" +type TeamDiscussion implements Comment & Deletable & Node & Reactable & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the discussion's team. + """ + authorAssociation: CommentAuthorAssociation! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the discussion body hash. + """ + bodyVersion: String! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + A list of comments on this discussion. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + When provided, filters the connection such that results begin with the comment with this number. + """ + fromComment: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamDiscussionCommentOrder + ): TeamDiscussionCommentConnection! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The HTTP path for discussion comments + """ + commentsResourcePath: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The HTTP URL for discussion comments + """ + commentsUrl: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The Node ID of the TeamDiscussion object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Whether or not the discussion is pinned. + """ + isPinned: Boolean! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Whether or not the discussion is only visible to team members and organization owners. + """ + isPrivate: Boolean! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies the discussion within its team. + """ + number: Int! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The HTTP path for this discussion + """ + resourcePath: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The team that defines the context of this discussion. + """ + team: Team! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The title of the discussion + """ + title: String! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this discussion + """ + url: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Whether or not the current viewer can pin this discussion. + """ + viewerCanPin: Boolean! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +A comment on a team discussion. +""" +type TeamDiscussionComment implements Comment & Deletable & Node & Reactable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the comment's team. + """ + authorAssociation: CommentAuthorAssociation! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + The current version of the body content. + """ + bodyVersion: String! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The discussion this comment is about. + """ + discussion: TeamDiscussion! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The Node ID of the TeamDiscussionComment object + """ + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies the comment number. + """ + number: Int! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The HTTP path for this comment + """ + resourcePath: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this comment + """ + url: URI! + @deprecated( + reason: "The Team Discussions feature is deprecated in favor of Organization Discussions. Follow the guide at https://github.blog/changelog/2023-02-08-sunset-notice-team-discussions/ to find a suitable replacement. Removal on 2024-07-01 UTC." + ) + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for TeamDiscussionComment. +""" +type TeamDiscussionCommentConnection { + """ + A list of edges. + """ + edges: [TeamDiscussionCommentEdge] + + """ + A list of nodes. + """ + nodes: [TeamDiscussionComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type TeamDiscussionCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: TeamDiscussionComment +} + +""" +Ways in which team discussion comment connections can be ordered. +""" +input TeamDiscussionCommentOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field by which to order nodes. + """ + field: TeamDiscussionCommentOrderField! +} + +""" +Properties by which team discussion comment connections can be ordered. +""" +enum TeamDiscussionCommentOrderField { + """ + Allows sequential ordering of team discussion comments (which is equivalent to chronological ordering). + """ + NUMBER +} + +""" +The connection type for TeamDiscussion. +""" +type TeamDiscussionConnection { + """ + A list of edges. + """ + edges: [TeamDiscussionEdge] + + """ + A list of nodes. + """ + nodes: [TeamDiscussion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type TeamDiscussionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: TeamDiscussion +} + +""" +Ways in which team discussion connections can be ordered. +""" +input TeamDiscussionOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field by which to order nodes. + """ + field: TeamDiscussionOrderField! +} + +""" +Properties by which team discussion connections can be ordered. +""" +enum TeamDiscussionOrderField { + """ + Allows chronological ordering of team discussions. + """ + CREATED_AT +} + +""" +An edge in a connection. +""" +type TeamEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Team +} + +""" +The connection type for User. +""" +type TeamMemberConnection { + """ + A list of edges. + """ + edges: [TeamMemberEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user who is a member of a team. +""" +type TeamMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The HTTP path to the organization's member access page. + """ + memberAccessResourcePath: URI! + + """ + The HTTP URL to the organization's member access page. + """ + memberAccessUrl: URI! + node: User! + + """ + The role the member has on the team. + """ + role: TeamMemberRole! +} + +""" +Ordering options for team member connections +""" +input TeamMemberOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order team members by. + """ + field: TeamMemberOrderField! +} + +""" +Properties by which team member connections can be ordered. +""" +enum TeamMemberOrderField { + """ + Order team members by creation time + """ + CREATED_AT + + """ + Order team members by login + """ + LOGIN +} + +""" +The possible team member roles; either 'maintainer' or 'member'. +""" +enum TeamMemberRole { + """ + A team maintainer has permission to add and remove team members. + """ + MAINTAINER + + """ + A team member has no administrative permissions on the team. + """ + MEMBER +} + +""" +Defines which types of team members are included in the returned list. Can be one of IMMEDIATE, CHILD_TEAM or ALL. +""" +enum TeamMembershipType { + """ + Includes immediate and child team members for the team. + """ + ALL + + """ + Includes only child team members for the team. + """ + CHILD_TEAM + + """ + Includes only immediate members of the team. + """ + IMMEDIATE +} + +""" +The possible team notification values. +""" +enum TeamNotificationSetting { + """ + No one will receive notifications. + """ + NOTIFICATIONS_DISABLED + + """ + Everyone will receive notifications when the team is @mentioned. + """ + NOTIFICATIONS_ENABLED +} + +""" +Ways in which team connections can be ordered. +""" +input TeamOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field in which to order nodes by. + """ + field: TeamOrderField! +} + +""" +Properties by which team connections can be ordered. +""" +enum TeamOrderField { + """ + Allows ordering a list of teams by name. + """ + NAME +} + +""" +The possible team privacy values. +""" +enum TeamPrivacy { + """ + A secret team can only be seen by its members. + """ + SECRET + + """ + A visible team can be seen and @mentioned by every member of the organization. + """ + VISIBLE +} + +""" +Audit log entry for a team.remove_member event. +""" +type TeamRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the TeamRemoveMemberAuditEntry object + """ + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +Audit log entry for a team.remove_repository event. +""" +type TeamRemoveRepositoryAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The user who initiated the action + """ + actor: AuditEntryActor + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The IP address of the actor + """ + actorIp: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The username of the user who initiated the action + """ + actorLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Node ID of the TeamRemoveRepositoryAuditEntry object + """ + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The corresponding operation type for the action + """ + operationType: OperationType + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The name of the Organization. + """ + organizationName: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP path for the user. + """ + userResourcePath: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) + + """ + The HTTP URL for the user. + """ + userUrl: URI + @deprecated( + reason: "The GraphQL audit-log is deprecated. Please use the REST API instead. Removal on 2026-04-01 UTC." + ) +} + +""" +The connection type for Repository. +""" +type TeamRepositoryConnection { + """ + A list of edges. + """ + edges: [TeamRepositoryEdge] + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a team repository. +""" +type TeamRepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: Repository! + + """ + The permission level the team has on the repository + """ + permission: RepositoryPermission! +} + +""" +Ordering options for team repository connections +""" +input TeamRepositoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: TeamRepositoryOrderField! +} + +""" +Properties by which team repository connections can be ordered. +""" +enum TeamRepositoryOrderField { + """ + Order repositories by creation time + """ + CREATED_AT + + """ + Order repositories by name + """ + NAME + + """ + Order repositories by permission + """ + PERMISSION + + """ + Order repositories by push time + """ + PUSHED_AT + + """ + Order repositories by number of stargazers + """ + STARGAZERS + + """ + Order repositories by update time + """ + UPDATED_AT +} + +""" +The possible team review assignment algorithms +""" +enum TeamReviewAssignmentAlgorithm { + """ + Balance review load across the entire team + """ + LOAD_BALANCE + + """ + Alternate reviews between each team member + """ + ROUND_ROBIN +} + +""" +The role of a user on a team. +""" +enum TeamRole { + """ + User has admin rights on the team. + """ + ADMIN + + """ + User is a member of the team. + """ + MEMBER +} + +""" +A text match within a search result. +""" +type TextMatch { + """ + The specific text fragment within the property matched on. + """ + fragment: String! + + """ + Highlights within the matched fragment. + """ + highlights: [TextMatchHighlight!]! + + """ + The property matched on. + """ + property: String! +} + +""" +Represents a single highlight in a search result match. +""" +type TextMatchHighlight { + """ + The indice in the fragment where the matched text begins. + """ + beginIndice: Int! + + """ + The indice in the fragment where the matched text ends. + """ + endIndice: Int! + + """ + The text matched. + """ + text: String! +} + +""" +The possible states of a thread subscription form action +""" +enum ThreadSubscriptionFormAction { + """ + The User cannot subscribe or unsubscribe to the thread + """ + NONE + + """ + The User can subscribe to the thread + """ + SUBSCRIBE + + """ + The User can unsubscribe to the thread + """ + UNSUBSCRIBE +} + +""" +The possible states of a subscription. +""" +enum ThreadSubscriptionState { + """ + The subscription status is currently disabled. + """ + DISABLED + + """ + The User is never notified because they are ignoring the list + """ + IGNORING_LIST + + """ + The User is never notified because they are ignoring the thread + """ + IGNORING_THREAD + + """ + The User is not recieving notifications from this thread + """ + NONE + + """ + The User is notified becuase they are watching the list + """ + SUBSCRIBED_TO_LIST + + """ + The User is notified because they are subscribed to the thread + """ + SUBSCRIBED_TO_THREAD + + """ + The User is notified because they chose custom settings for this thread. + """ + SUBSCRIBED_TO_THREAD_EVENTS + + """ + The User is notified because they chose custom settings for this thread. + """ + SUBSCRIBED_TO_THREAD_TYPE + + """ + The subscription status is currently unavailable. + """ + UNAVAILABLE +} + +""" +A topic aggregates entities that are related to a subject. +""" +type Topic implements Node & Starrable { + """ + The Node ID of the Topic object + """ + id: ID! + + """ + The topic's name. + """ + name: String! + + """ + A list of related topics, including aliases of this topic, sorted with the most relevant + first. Returns up to 10 Topics. + """ + relatedTopics( + """ + How many topics to return. + """ + first: Int = 3 + ): [Topic!]! + + """ + A list of repositories. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy. Internal + repositories are considered private; consider using the visibility argument + if only internal repositories are needed. Cannot be combined with the + visibility argument. + """ + privacy: RepositoryPrivacy + + """ + If true, only repositories whose owner can be sponsored via GitHub Sponsors will be returned. + """ + sponsorableOnly: Boolean = false + + """ + If non-null, filters repositories according to visibility. Cannot be combined with the privacy argument. + """ + visibility: RepositoryVisibility + ): RepositoryConnection! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +Metadata for an audit entry with a topic. +""" +interface TopicAuditEntryData { + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String +} + +""" +Reason that the suggested topic is declined. +""" +enum TopicSuggestionDeclineReason { + """ + The suggested topic is not relevant to the repository. + """ + NOT_RELEVANT @deprecated(reason: "Suggested topics are no longer supported Removal on 2024-04-01 UTC.") + + """ + The viewer does not like the suggested topic. + """ + PERSONAL_PREFERENCE @deprecated(reason: "Suggested topics are no longer supported Removal on 2024-04-01 UTC.") + + """ + The suggested topic is too general for the repository. + """ + TOO_GENERAL @deprecated(reason: "Suggested topics are no longer supported Removal on 2024-04-01 UTC.") + + """ + The suggested topic is too specific for the repository (e.g. #ruby-on-rails-version-4-2-1). + """ + TOO_SPECIFIC @deprecated(reason: "Suggested topics are no longer supported Removal on 2024-04-01 UTC.") +} + +""" +The possible states of a tracked issue. +""" +enum TrackedIssueStates { + """ + The tracked issue is closed + """ + CLOSED + + """ + The tracked issue is open + """ + OPEN +} + +""" +Autogenerated input type of TransferEnterpriseOrganization +""" +input TransferEnterpriseOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise where the organization should be transferred. + """ + destinationEnterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization to transfer. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of TransferEnterpriseOrganization. +""" +type TransferEnterpriseOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization for which a transfer was initiated. + """ + organization: Organization +} + +""" +Autogenerated input type of TransferIssue +""" +input TransferIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether to create labels if they don't exist in the target repository (matched by name) + """ + createLabelsIfMissing: Boolean = false + + """ + The Node ID of the issue to be transferred + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The Node ID of the repository the issue should be transferred to + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of TransferIssue. +""" +type TransferIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was transferred + """ + issue: Issue +} + +""" +Represents a 'transferred' event on a given issue or pull request. +""" +type TransferredEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The repository this came from + """ + fromRepository: Repository + + """ + The Node ID of the TransferredEvent object + """ + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +Represents a Git tree. +""" +type Tree implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + A list of tree entries. + """ + entries: [TreeEntry!] + + """ + The Node ID of the Tree object + """ + id: ID! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! +} + +""" +Represents a Git tree entry. +""" +type TreeEntry { + """ + The extension of the file + """ + extension: String + + """ + Whether or not this tree entry is generated + """ + isGenerated: Boolean! + + """ + The programming language this file is written in. + """ + language: Language + + """ + Number of lines in the file. + """ + lineCount: Int + + """ + Entry file mode. + """ + mode: Int! + + """ + Entry file name. + """ + name: String! + + """ + Entry file name. (Base64-encoded) + """ + nameRaw: Base64String! + + """ + Entry file object. + """ + object: GitObject + + """ + Entry file Git object ID. + """ + oid: GitObjectID! + + """ + The full path of the file. + """ + path: String + + """ + The full path of the file. (Base64-encoded) + """ + pathRaw: Base64String + + """ + The Repository the tree entry belongs to + """ + repository: Repository! + + """ + Entry byte size + """ + size: Int! + + """ + If the TreeEntry is for a directory occupied by a submodule project, this returns the corresponding submodule + """ + submodule: Submodule + + """ + Entry file type. + """ + type: String! +} + +""" +Filters by whether or not 2FA is enabled and if the method configured is considered secure or insecure. +""" +enum TwoFactorCredentialSecurityType { + """ + No method of two-factor authentication. + """ + DISABLED + + """ + Has an insecure method of two-factor authentication. GitHub currently defines this as SMS two-factor authentication. + """ + INSECURE + + """ + Has only secure methods of two-factor authentication. + """ + SECURE +} + +""" +An RFC 3986, RFC 3987, and RFC 6570 (level 4) compliant URI string. +""" +scalar URI + +""" +Autogenerated input type of UnarchiveProjectV2Item +""" +input UnarchiveProjectV2ItemInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the ProjectV2Item to unarchive. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project to archive the item from. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of UnarchiveProjectV2Item. +""" +type UnarchiveProjectV2ItemPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item unarchived from the project. + """ + item: ProjectV2Item +} + +""" +Autogenerated input type of UnarchiveRepository +""" +input UnarchiveRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the repository to unarchive. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UnarchiveRepository. +""" +type UnarchiveRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that was unarchived. + """ + repository: Repository +} + +""" +Represents an 'unassigned' event on any assignable object. +""" +type UnassignedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the assignable associated with the event. + """ + assignable: Assignable! + + """ + Identifies the user or mannequin that was unassigned. + """ + assignee: Assignee + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the UnassignedEvent object + """ + id: ID! + + """ + Identifies the subject (user) who was unassigned. + """ + user: User + @deprecated(reason: "Assignees can now be mannequins. Use the `assignee` field instead. Removal on 2020-01-01 UTC.") +} + +""" +Autogenerated input type of UnfollowOrganization +""" +input UnfollowOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the organization to unfollow. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of UnfollowOrganization. +""" +type UnfollowOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization that was unfollowed. + """ + organization: Organization +} + +""" +Autogenerated input type of UnfollowUser +""" +input UnfollowUserInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the user to unfollow. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of UnfollowUser. +""" +type UnfollowUserPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was unfollowed. + """ + user: User +} + +""" +Represents a type that can be retrieved by a URL. +""" +interface UniformResourceLocatable { + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + The URL to this resource. + """ + url: URI! +} + +""" +Represents an unknown signature on a Commit or Tag. +""" +type UnknownSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + The date the signature was verified, if valid + """ + verifiedAt: DateTime + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Represents an 'unlabeled' event on a given issue or pull request. +""" +type UnlabeledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the UnlabeledEvent object + """ + id: ID! + + """ + Identifies the label associated with the 'unlabeled' event. + """ + label: Label! + + """ + Identifies the `Labelable` associated with the event. + """ + labelable: Labelable! +} + +""" +Autogenerated input type of UnlinkProjectV2FromRepository +""" +input UnlinkProjectV2FromRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the project to unlink from the repository. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The ID of the repository to unlink from the project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UnlinkProjectV2FromRepository. +""" +type UnlinkProjectV2FromRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository the project is no longer linked to. + """ + repository: Repository +} + +""" +Autogenerated input type of UnlinkProjectV2FromTeam +""" +input UnlinkProjectV2FromTeamInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the project to unlink from the team. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The ID of the team to unlink from the project. + """ + teamId: ID! @possibleTypes(concreteTypes: ["Team"]) +} + +""" +Autogenerated return type of UnlinkProjectV2FromTeam. +""" +type UnlinkProjectV2FromTeamPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The team the project is unlinked from + """ + team: Team +} + +""" +Autogenerated input type of UnlinkRepositoryFromProject +""" +input UnlinkRepositoryFromProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project linked to the Repository. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The ID of the Repository linked to the Project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UnlinkRepositoryFromProject. +""" +type UnlinkRepositoryFromProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The linked Project. + """ + project: Project + + """ + The linked Repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UnlockLockable +""" +input UnlockLockableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the item to be unlocked. + """ + lockableId: ID! @possibleTypes(concreteTypes: ["Discussion", "Issue", "PullRequest"], abstractType: "Lockable") +} + +""" +Autogenerated return type of UnlockLockable. +""" +type UnlockLockablePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was unlocked. + """ + unlockedRecord: Lockable +} + +""" +Represents an 'unlocked' event on a given issue or pull request. +""" +type UnlockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the UnlockedEvent object + """ + id: ID! + + """ + Object that was unlocked. + """ + lockable: Lockable! +} + +""" +Autogenerated input type of UnmarkDiscussionCommentAsAnswer +""" +input UnmarkDiscussionCommentAsAnswerInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion comment to unmark as an answer. + """ + id: ID! @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of UnmarkDiscussionCommentAsAnswer. +""" +type UnmarkDiscussionCommentAsAnswerPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion that includes the comment. + """ + discussion: Discussion +} + +""" +Autogenerated input type of UnmarkFileAsViewed +""" +input UnmarkFileAsViewedInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The path of the file to mark as unviewed + """ + path: String! + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of UnmarkFileAsViewed. +""" +type UnmarkFileAsViewedPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of UnmarkIssueAsDuplicate +""" +input UnmarkIssueAsDuplicateInput { + """ + ID of the issue or pull request currently considered canonical/authoritative/original. + """ + canonicalId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue or pull request currently marked as a duplicate. + """ + duplicateId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") +} + +""" +Autogenerated return type of UnmarkIssueAsDuplicate. +""" +type UnmarkIssueAsDuplicatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue or pull request that was marked as a duplicate. + """ + duplicate: IssueOrPullRequest +} + +""" +Autogenerated input type of UnmarkProjectV2AsTemplate +""" +input UnmarkProjectV2AsTemplateInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to unmark as a template. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of UnmarkProjectV2AsTemplate. +""" +type UnmarkProjectV2AsTemplatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The project. + """ + projectV2: ProjectV2 +} + +""" +Represents an 'unmarked_as_duplicate' event on a given issue or pull request. +""" +type UnmarkedAsDuplicateEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + The authoritative issue or pull request which has been duplicated by another. + """ + canonical: IssueOrPullRequest + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The issue or pull request which has been marked as a duplicate of another. + """ + duplicate: IssueOrPullRequest + + """ + The Node ID of the UnmarkedAsDuplicateEvent object + """ + id: ID! + + """ + Canonical and duplicate belong to different repositories. + """ + isCrossRepository: Boolean! +} + +""" +Autogenerated input type of UnminimizeComment +""" +input UnminimizeCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! + @possibleTypes( + concreteTypes: [ + "CommitComment" + "DiscussionComment" + "GistComment" + "IssueComment" + "PullRequestReview" + "PullRequestReviewComment" + ] + abstractType: "Minimizable" + ) +} + +""" +Autogenerated return type of UnminimizeComment. +""" +type UnminimizeCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The comment that was unminimized. + """ + unminimizedComment: Minimizable +} + +""" +Autogenerated input type of UnpinIssue +""" +input UnpinIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to be unpinned + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of UnpinIssue. +""" +type UnpinIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the pinned issue that was unpinned + """ + id: ID + + """ + The issue that was unpinned + """ + issue: Issue +} + +""" +Represents an 'unpinned' event on a given issue or pull request. +""" +type UnpinnedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the UnpinnedEvent object + """ + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +Autogenerated input type of UnresolveReviewThread +""" +input UnresolveReviewThreadInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the thread to unresolve + """ + threadId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewThread"]) +} + +""" +Autogenerated return type of UnresolveReviewThread. +""" +type UnresolveReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The thread to resolve. + """ + thread: PullRequestReviewThread +} + +""" +Represents an 'unsubscribed' event on a given `Subscribable`. +""" +type UnsubscribedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the UnsubscribedEvent object + """ + id: ID! + + """ + Object referenced by event. + """ + subscribable: Subscribable! +} + +""" +Entities that can be updated. +""" +interface Updatable { + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! +} + +""" +Comments that can be updated. +""" +interface UpdatableComment { + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! +} + +""" +Autogenerated input type of UpdateBranchProtectionRule +""" +input UpdateBranchProtectionRuleInput { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean + + """ + Is branch creation a protected operation. + """ + blocksCreations: Boolean + + """ + The global relay id of the branch protection rule to be updated. + """ + branchProtectionRuleId: ID! @possibleTypes(concreteTypes: ["BranchProtectionRule"]) + + """ + A list of User, Team, or App IDs allowed to bypass force push targeting matching branches. + """ + bypassForcePushActorIds: [ID!] + + """ + A list of User, Team, or App IDs allowed to bypass pull requests targeting matching branches. + """ + bypassPullRequestActorIds: [ID!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean + + """ + Can admins override branch protection. + """ + isAdminEnforced: Boolean + + """ + Whether users can pull changes from upstream when the branch is locked. Set to + `true` to allow fork syncing. Set to `false` to prevent fork syncing. + """ + lockAllowsFetchAndMerge: Boolean + + """ + Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. + """ + lockBranch: Boolean + + """ + The glob-like pattern used to determine matching branches. + """ + pattern: String + + """ + A list of User, Team, or App IDs allowed to push to matching branches. + """ + pushActorIds: [ID!] + + """ + Whether the most recent push must be approved by someone other than the person who pushed it + """ + requireLastPushApproval: Boolean + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + The list of required deployment environments + """ + requiredDeploymentEnvironments: [String!] + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String!] + + """ + The list of required status checks + """ + requiredStatusChecks: [RequiredStatusCheckInput!] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean + + """ + Are conversations required to be resolved before merging. + """ + requiresConversationResolution: Boolean + + """ + Are successful deployments required before merging. + """ + requiresDeployments: Boolean + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean + + """ + A list of User, Team, or App IDs allowed to dismiss reviews on pull requests targeting matching branches. + """ + reviewDismissalActorIds: [ID!] +} + +""" +Autogenerated return type of UpdateBranchProtectionRule. +""" +type UpdateBranchProtectionRulePayload { + """ + The newly created BranchProtectionRule. + """ + branchProtectionRule: BranchProtectionRule + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateCheckRun +""" +input UpdateCheckRunInput { + """ + Possible further actions the integrator can perform, which a user may trigger. + """ + actions: [CheckRunAction!] + + """ + The node of the check. + """ + checkRunId: ID! @possibleTypes(concreteTypes: ["CheckRun"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The time that the check run finished. + """ + completedAt: DateTime + + """ + The final conclusion of the check. + """ + conclusion: CheckConclusionState + + """ + The URL of the integrator's site that has the full details of the check. + """ + detailsUrl: URI + + """ + A reference for the run on the integrator's system. + """ + externalId: String + + """ + The name of the check. + """ + name: String + + """ + Descriptive details about the run. + """ + output: CheckRunOutput + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The time that the check run began. + """ + startedAt: DateTime + + """ + The current status. + """ + status: RequestableCheckStatusState +} + +""" +Autogenerated return type of UpdateCheckRun. +""" +type UpdateCheckRunPayload { + """ + The updated check run. + """ + checkRun: CheckRun + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateCheckSuitePreferences +""" +input UpdateCheckSuitePreferencesInput { + """ + The check suite preferences to modify. + """ + autoTriggerPreferences: [CheckSuiteAutoTriggerPreference!]! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UpdateCheckSuitePreferences. +""" +type UpdateCheckSuitePreferencesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateDiscussionComment +""" +input UpdateDiscussionCommentInput { + """ + The new contents of the comment body. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion comment to update. + """ + commentId: ID! @possibleTypes(concreteTypes: ["DiscussionComment"]) +} + +""" +Autogenerated return type of UpdateDiscussionComment. +""" +type UpdateDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The modified discussion comment. + """ + comment: DiscussionComment +} + +""" +Autogenerated input type of UpdateDiscussion +""" +input UpdateDiscussionInput { + """ + The new contents of the discussion body. + """ + body: String + + """ + The Node ID of a discussion category within the same repository to change this discussion to. + """ + categoryId: ID @possibleTypes(concreteTypes: ["DiscussionCategory"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion to update. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["Discussion"]) + + """ + The new discussion title. + """ + title: String +} + +""" +Autogenerated return type of UpdateDiscussion. +""" +type UpdateDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The modified discussion. + """ + discussion: Discussion +} + +""" +Autogenerated input type of UpdateEnterpriseAdministratorRole +""" +input UpdateEnterpriseAdministratorRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a administrator whose role is being changed. + """ + login: String! + + """ + The new role for the Enterprise administrator. + """ + role: EnterpriseAdministratorRole! +} + +""" +Autogenerated return type of UpdateEnterpriseAdministratorRole. +""" +type UpdateEnterpriseAdministratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of changing the administrator's role. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseAllowPrivateRepositoryForkingSetting +""" +input UpdateEnterpriseAllowPrivateRepositoryForkingSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the allow private repository forking setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the allow private repository forking policy on the enterprise. + """ + policyValue: EnterpriseAllowPrivateRepositoryForkingPolicyValue + + """ + The value for the allow private repository forking setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseAllowPrivateRepositoryForkingSetting. +""" +type UpdateEnterpriseAllowPrivateRepositoryForkingSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated allow private repository forking setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the allow private repository forking setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseDefaultRepositoryPermissionSetting +""" +input UpdateEnterpriseDefaultRepositoryPermissionSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the base repository permission setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the base repository permission setting on the enterprise. + """ + settingValue: EnterpriseDefaultRepositoryPermissionSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseDefaultRepositoryPermissionSetting. +""" +type UpdateEnterpriseDefaultRepositoryPermissionSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated base repository permission setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the base repository permission setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseDeployKeySetting +""" +input UpdateEnterpriseDeployKeySettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the deploy key setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the deploy key setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseDeployKeySetting. +""" +type UpdateEnterpriseDeployKeySettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated deploy key setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the deploy key setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanChangeRepositoryVisibilitySetting +""" +input UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can change repository visibility setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can change repository visibility setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanChangeRepositoryVisibilitySetting. +""" +type UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can change repository visibility setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can change repository visibility setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanCreateRepositoriesSetting +""" +input UpdateEnterpriseMembersCanCreateRepositoriesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can create repositories setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + Allow members to create internal repositories. Defaults to current value. + """ + membersCanCreateInternalRepositories: Boolean + + """ + Allow members to create private repositories. Defaults to current value. + """ + membersCanCreatePrivateRepositories: Boolean + + """ + Allow members to create public repositories. Defaults to current value. + """ + membersCanCreatePublicRepositories: Boolean + + """ + When false, allow member organizations to set their own repository creation member privileges. + """ + membersCanCreateRepositoriesPolicyEnabled: Boolean + + """ + Value for the members can create repositories setting on the enterprise. This + or the granular public/private/internal allowed fields (but not both) must be provided. + """ + settingValue: EnterpriseMembersCanCreateRepositoriesSettingValue +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanCreateRepositoriesSetting. +""" +type UpdateEnterpriseMembersCanCreateRepositoriesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can create repositories setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can create repositories setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanDeleteIssuesSetting +""" +input UpdateEnterpriseMembersCanDeleteIssuesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can delete issues setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can delete issues setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanDeleteIssuesSetting. +""" +type UpdateEnterpriseMembersCanDeleteIssuesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can delete issues setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can delete issues setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanDeleteRepositoriesSetting +""" +input UpdateEnterpriseMembersCanDeleteRepositoriesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can delete repositories setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can delete repositories setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanDeleteRepositoriesSetting. +""" +type UpdateEnterpriseMembersCanDeleteRepositoriesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can delete repositories setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can delete repositories setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanInviteCollaboratorsSetting +""" +input UpdateEnterpriseMembersCanInviteCollaboratorsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can invite collaborators setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can invite collaborators setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanInviteCollaboratorsSetting. +""" +type UpdateEnterpriseMembersCanInviteCollaboratorsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can invite collaborators setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can invite collaborators setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanMakePurchasesSetting +""" +input UpdateEnterpriseMembersCanMakePurchasesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can make purchases setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can make purchases setting on the enterprise. + """ + settingValue: EnterpriseMembersCanMakePurchasesSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanMakePurchasesSetting. +""" +type UpdateEnterpriseMembersCanMakePurchasesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can make purchases setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can make purchases setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanUpdateProtectedBranchesSetting +""" +input UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can update protected branches setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can update protected branches setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanUpdateProtectedBranchesSetting. +""" +type UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can update protected branches setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can update protected branches setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanViewDependencyInsightsSetting +""" +input UpdateEnterpriseMembersCanViewDependencyInsightsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can view dependency insights setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can view dependency insights setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanViewDependencyInsightsSetting. +""" +type UpdateEnterpriseMembersCanViewDependencyInsightsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can view dependency insights setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can view dependency insights setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseOrganizationProjectsSetting +""" +input UpdateEnterpriseOrganizationProjectsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the organization projects setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the organization projects setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseOrganizationProjectsSetting. +""" +type UpdateEnterpriseOrganizationProjectsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated organization projects setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the organization projects setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseOwnerOrganizationRole +""" +input UpdateEnterpriseOwnerOrganizationRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the owner belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization for membership change. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The role to assume in the organization. + """ + organizationRole: RoleInOrganization! +} + +""" +Autogenerated return type of UpdateEnterpriseOwnerOrganizationRole. +""" +type UpdateEnterpriseOwnerOrganizationRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of changing the owner's organization role. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseProfile +""" +input UpdateEnterpriseProfileInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The description of the enterprise. + """ + description: String + + """ + The Enterprise ID to update. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The location of the enterprise. + """ + location: String + + """ + The name of the enterprise. + """ + name: String + + """ + The URL of the enterprise's website. + """ + websiteUrl: String +} + +""" +Autogenerated return type of UpdateEnterpriseProfile. +""" +type UpdateEnterpriseProfilePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise +} + +""" +Autogenerated input type of UpdateEnterpriseRepositoryProjectsSetting +""" +input UpdateEnterpriseRepositoryProjectsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the repository projects setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the repository projects setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseRepositoryProjectsSetting. +""" +type UpdateEnterpriseRepositoryProjectsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated repository projects setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the repository projects setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseTeamDiscussionsSetting +""" +input UpdateEnterpriseTeamDiscussionsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the team discussions setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the team discussions setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseTeamDiscussionsSetting. +""" +type UpdateEnterpriseTeamDiscussionsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated team discussions setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the team discussions setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseTwoFactorAuthenticationDisallowedMethodsSetting +""" +input UpdateEnterpriseTwoFactorAuthenticationDisallowedMethodsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the two-factor authentication disallowed methods setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the two-factor authentication disallowed methods setting on the enterprise. + """ + settingValue: EnterpriseDisallowedMethodsSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseTwoFactorAuthenticationDisallowedMethodsSetting. +""" +type UpdateEnterpriseTwoFactorAuthenticationDisallowedMethodsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated two-factor authentication disallowed methods setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the two-factor authentication disallowed methods setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseTwoFactorAuthenticationRequiredSetting +""" +input UpdateEnterpriseTwoFactorAuthenticationRequiredSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the two factor authentication required setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the two factor authentication required setting on the enterprise. + """ + settingValue: EnterpriseEnabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseTwoFactorAuthenticationRequiredSetting. +""" +type UpdateEnterpriseTwoFactorAuthenticationRequiredSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated two factor authentication required setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the two factor authentication required setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnvironment +""" +input UpdateEnvironmentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The node ID of the environment. + """ + environmentId: ID! @possibleTypes(concreteTypes: ["Environment"]) + + """ + Whether deployments to this environment can be approved by the user who created the deployment. + """ + preventSelfReview: Boolean + + """ + The ids of users or teams that can approve deployments to this environment + """ + reviewers: [ID!] + + """ + The wait timer in minutes. + """ + waitTimer: Int +} + +""" +Autogenerated return type of UpdateEnvironment. +""" +type UpdateEnvironmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated environment. + """ + environment: Environment +} + +""" +Autogenerated input type of UpdateIpAllowListEnabledSetting +""" +input UpdateIpAllowListEnabledSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the owner on which to set the IP allow list enabled setting. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["App", "Enterprise", "Organization"], abstractType: "IpAllowListOwner") + + """ + The value for the IP allow list enabled setting. + """ + settingValue: IpAllowListEnabledSettingValue! +} + +""" +Autogenerated return type of UpdateIpAllowListEnabledSetting. +""" +type UpdateIpAllowListEnabledSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list owner on which the setting was updated. + """ + owner: IpAllowListOwner +} + +""" +Autogenerated input type of UpdateIpAllowListEntry +""" +input UpdateIpAllowListEntryInput { + """ + An IP address or range of addresses in CIDR notation. + """ + allowListValue: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IP allow list entry to update. + """ + ipAllowListEntryId: ID! @possibleTypes(concreteTypes: ["IpAllowListEntry"]) + + """ + Whether the IP allow list entry is active when an IP allow list is enabled. + """ + isActive: Boolean! + + """ + An optional name for the IP allow list entry. + """ + name: String +} + +""" +Autogenerated return type of UpdateIpAllowListEntry. +""" +type UpdateIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was updated. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of UpdateIpAllowListForInstalledAppsEnabledSetting +""" +input UpdateIpAllowListForInstalledAppsEnabledSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the owner. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["App", "Enterprise", "Organization"], abstractType: "IpAllowListOwner") + + """ + The value for the IP allow list configuration for installed GitHub Apps setting. + """ + settingValue: IpAllowListForInstalledAppsEnabledSettingValue! +} + +""" +Autogenerated return type of UpdateIpAllowListForInstalledAppsEnabledSetting. +""" +type UpdateIpAllowListForInstalledAppsEnabledSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list owner on which the setting was updated. + """ + owner: IpAllowListOwner +} + +""" +Autogenerated input type of UpdateIssueComment +""" +input UpdateIssueCommentInput { + """ + The updated text of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IssueComment to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["IssueComment"]) +} + +""" +Autogenerated return type of UpdateIssueComment. +""" +type UpdateIssueCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + issueComment: IssueComment +} + +""" +Autogenerated input type of UpdateIssue +""" +input UpdateIssueInput { + """ + An array of Node IDs of users for this issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body for the issue description. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Issue to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The ID of the Issue Type for this issue. + """ + issueTypeId: ID + + """ + An array of Node IDs of labels for this issue. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + The Node ID of the milestone for this issue. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + An array of Node IDs for projects associated with this issue. + """ + projectIds: [ID!] + + """ + The desired issue state. + """ + state: IssueState + + """ + The title for the issue. + """ + title: String +} + +""" +Autogenerated input type of UpdateIssueIssueType +""" +input UpdateIssueIssueTypeInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to update + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The ID of the issue type to update on the issue + """ + issueTypeId: ID +} + +""" +Autogenerated return type of UpdateIssueIssueType. +""" +type UpdateIssueIssueTypePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated issue + """ + issue: Issue +} + +""" +Autogenerated return type of UpdateIssue. +""" +type UpdateIssuePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue. + """ + issue: Issue +} + +""" +Autogenerated input type of UpdateIssueType +""" +input UpdateIssueTypeInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Color for the issue type + """ + color: IssueTypeColor + + """ + The description of the issue type + """ + description: String + + """ + Whether or not the issue type is enabled for the organization + """ + isEnabled: Boolean + + """ + The ID of the issue type to update + """ + issueTypeId: ID! @possibleTypes(concreteTypes: ["IssueType"]) + + """ + The name of the issue type + """ + name: String +} + +""" +Autogenerated return type of UpdateIssueType. +""" +type UpdateIssueTypePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated issue type + """ + issueType: IssueType +} + +""" +Autogenerated input type of UpdateLabel +""" +input UpdateLabelInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A 6 character hex code, without the leading #, identifying the updated color of the label. + """ + color: String + + """ + A brief description of the label, such as its purpose. + """ + description: String + + """ + The Node ID of the label to be updated. + """ + id: ID! @possibleTypes(concreteTypes: ["Label"]) + + """ + The updated name of the label. + """ + name: String +} + +""" +Autogenerated return type of UpdateLabel. +""" +type UpdateLabelPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated label. + """ + label: Label +} + +""" +Autogenerated input type of UpdateNotificationRestrictionSetting +""" +input UpdateNotificationRestrictionSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the owner on which to set the restrict notifications setting. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Enterprise", "Organization"], abstractType: "VerifiableDomainOwner") + + """ + The value for the restrict notifications setting. + """ + settingValue: NotificationRestrictionSettingValue! +} + +""" +Autogenerated return type of UpdateNotificationRestrictionSetting. +""" +type UpdateNotificationRestrictionSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owner on which the setting was updated. + """ + owner: VerifiableDomainOwner +} + +""" +Autogenerated input type of UpdateOrganizationAllowPrivateRepositoryForkingSetting +""" +input UpdateOrganizationAllowPrivateRepositoryForkingSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Enable forking of private repositories in the organization? + """ + forkingEnabled: Boolean! + + """ + The ID of the organization on which to set the allow private repository forking setting. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of UpdateOrganizationAllowPrivateRepositoryForkingSetting. +""" +type UpdateOrganizationAllowPrivateRepositoryForkingSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of updating the allow private repository forking setting. + """ + message: String + + """ + The organization with the updated allow private repository forking setting. + """ + organization: Organization +} + +""" +Autogenerated input type of UpdateOrganizationWebCommitSignoffSetting +""" +input UpdateOrganizationWebCommitSignoffSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization on which to set the web commit signoff setting. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + Enable signoff on web-based commits for repositories in the organization? + """ + webCommitSignoffRequired: Boolean! +} + +""" +Autogenerated return type of UpdateOrganizationWebCommitSignoffSetting. +""" +type UpdateOrganizationWebCommitSignoffSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of updating the web commit signoff setting. + """ + message: String + + """ + The organization with the updated web commit signoff setting. + """ + organization: Organization +} + +""" +Only allow users with bypass permission to update matching refs. +""" +type UpdateParameters { + """ + Branch can pull changes from its upstream repository + """ + updateAllowsFetchAndMerge: Boolean! +} + +""" +Only allow users with bypass permission to update matching refs. +""" +input UpdateParametersInput { + """ + Branch can pull changes from its upstream repository + """ + updateAllowsFetchAndMerge: Boolean! +} + +""" +Autogenerated input type of UpdatePatreonSponsorability +""" +input UpdatePatreonSponsorabilityInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether Patreon tiers should be shown on the GitHub Sponsors profile page, + allowing potential sponsors to make their payment through Patreon instead of GitHub. + """ + enablePatreonSponsorships: Boolean! + + """ + The username of the organization with the GitHub Sponsors profile, if any. + Defaults to the GitHub Sponsors profile for the authenticated user if omitted. + """ + sponsorableLogin: String +} + +""" +Autogenerated return type of UpdatePatreonSponsorability. +""" +type UpdatePatreonSponsorabilityPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The GitHub Sponsors profile. + """ + sponsorsListing: SponsorsListing +} + +""" +Autogenerated input type of UpdateProjectCard +""" +input UpdateProjectCardInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not the ProjectCard should be archived + """ + isArchived: Boolean + + """ + The note of ProjectCard. + """ + note: String + + """ + The ProjectCard ID to update. + """ + projectCardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) +} + +""" +Autogenerated return type of UpdateProjectCard. +""" +type UpdateProjectCardPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated ProjectCard. + """ + projectCard: ProjectCard +} + +""" +Autogenerated input type of UpdateProjectColumn +""" +input UpdateProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project column. + """ + name: String! + + """ + The ProjectColumn ID to update. + """ + projectColumnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of UpdateProjectColumn. +""" +type UpdateProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated project column. + """ + projectColumn: ProjectColumn +} + +""" +Autogenerated input type of UpdateProject +""" +input UpdateProjectInput { + """ + The description of project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project. + """ + name: String + + """ + The Project ID to update. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + Whether the project is public or not. + """ + public: Boolean + + """ + Whether the project is open or closed. + """ + state: ProjectState +} + +""" +Autogenerated return type of UpdateProject. +""" +type UpdateProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated project. + """ + project: Project +} + +""" +Autogenerated input type of UpdateProjectV2Collaborators +""" +input UpdateProjectV2CollaboratorsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The collaborators to update. + """ + collaborators: [ProjectV2Collaborator!]! + + """ + The ID of the project to update the collaborators for. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of UpdateProjectV2Collaborators. +""" +type UpdateProjectV2CollaboratorsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The collaborators granted a role + """ + collaborators( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ActorConnection +} + +""" +Autogenerated input type of UpdateProjectV2DraftIssue +""" +input UpdateProjectV2DraftIssueInput { + """ + The IDs of the assignees of the draft issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body of the draft issue. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the draft issue to update. + """ + draftIssueId: ID! @possibleTypes(concreteTypes: ["DraftIssue"]) + + """ + The title of the draft issue. + """ + title: String +} + +""" +Autogenerated return type of UpdateProjectV2DraftIssue. +""" +type UpdateProjectV2DraftIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The draft issue updated in the project. + """ + draftIssue: DraftIssue +} + +""" +Autogenerated input type of UpdateProjectV2Field +""" +input UpdateProjectV2FieldInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the field to update. + """ + fieldId: ID! + @possibleTypes( + concreteTypes: ["ProjectV2Field", "ProjectV2IterationField", "ProjectV2SingleSelectField"] + abstractType: "ProjectV2FieldConfiguration" + ) + + """ + Configuration for an iteration field. + """ + iterationConfiguration: ProjectV2IterationFieldConfigurationInput + + """ + The name to update. + """ + name: String + + """ + Options for a field of type SINGLE_SELECT. If empty, no changes will be made + to the options. If values are present, they will overwrite the existing + options for the field. + """ + singleSelectOptions: [ProjectV2SingleSelectFieldOptionInput!] +} + +""" +Autogenerated return type of UpdateProjectV2Field. +""" +type UpdateProjectV2FieldPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated field. + """ + projectV2Field: ProjectV2FieldConfiguration +} + +""" +Autogenerated input type of UpdateProjectV2 +""" +input UpdateProjectV2Input { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Set the project to closed or open. + """ + closed: Boolean + + """ + The ID of the Project to update. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + Set the project to public or private. + """ + public: Boolean + + """ + Set the readme description of the project. + """ + readme: String + + """ + Set the short description of the project. + """ + shortDescription: String + + """ + Set the title of the project. + """ + title: String +} + +""" +Autogenerated input type of UpdateProjectV2ItemFieldValue +""" +input UpdateProjectV2ItemFieldValueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the field to be updated. + """ + fieldId: ID! + @possibleTypes( + concreteTypes: ["ProjectV2Field", "ProjectV2IterationField", "ProjectV2SingleSelectField"] + abstractType: "ProjectV2FieldConfiguration" + ) + + """ + The ID of the item to be updated. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) + + """ + The value which will be set on the field. + """ + value: ProjectV2FieldValue! +} + +""" +Autogenerated return type of UpdateProjectV2ItemFieldValue. +""" +type UpdateProjectV2ItemFieldValuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated item. + """ + projectV2Item: ProjectV2Item +} + +""" +Autogenerated input type of UpdateProjectV2ItemPosition +""" +input UpdateProjectV2ItemPositionInput { + """ + The ID of the item to position this item after. If omitted or set to null the item will be moved to top. + """ + afterId: ID @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the item to be moved. + """ + itemId: ID! @possibleTypes(concreteTypes: ["ProjectV2Item"]) + + """ + The ID of the Project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["ProjectV2"]) +} + +""" +Autogenerated return type of UpdateProjectV2ItemPosition. +""" +type UpdateProjectV2ItemPositionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The items in the new order + """ + items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2ItemConnection +} + +""" +Autogenerated return type of UpdateProjectV2. +""" +type UpdateProjectV2Payload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated Project. + """ + projectV2: ProjectV2 +} + +""" +Autogenerated input type of UpdateProjectV2StatusUpdate +""" +input UpdateProjectV2StatusUpdateInput { + """ + The body of the status update. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The start date of the status update. + """ + startDate: Date + + """ + The status of the status update. + """ + status: ProjectV2StatusUpdateStatus + + """ + The ID of the status update to be updated. + """ + statusUpdateId: ID! @possibleTypes(concreteTypes: ["ProjectV2StatusUpdate"]) + + """ + The target date of the status update. + """ + targetDate: Date +} + +""" +Autogenerated return type of UpdateProjectV2StatusUpdate. +""" +type UpdateProjectV2StatusUpdatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The status update updated in the project. + """ + statusUpdate: ProjectV2StatusUpdate +} + +""" +Autogenerated input type of UpdatePullRequestBranch +""" +input UpdatePullRequestBranchInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The head ref oid for the upstream branch. + """ + expectedHeadOid: GitObjectID + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The update branch method to use. If omitted, defaults to 'MERGE' + """ + updateMethod: PullRequestBranchUpdateMethod +} + +""" +Autogenerated return type of UpdatePullRequestBranch. +""" +type UpdatePullRequestBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of UpdatePullRequest +""" +input UpdatePullRequestInput { + """ + An array of Node IDs of users for this pull request. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The name of the branch you want your changes pulled into. This should be an existing branch + on the current repository. + """ + baseRefName: String + + """ + The contents of the pull request. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + An array of Node IDs of labels for this pull request. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean + + """ + The Node ID of the milestone for this pull request. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + An array of Node IDs for projects associated with this pull request. + """ + projectIds: [ID!] + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The target state of the pull request. + """ + state: PullRequestUpdateState + + """ + The title of the pull request. + """ + title: String +} + +""" +Autogenerated return type of UpdatePullRequest. +""" +type UpdatePullRequestPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of UpdatePullRequestReviewComment +""" +input UpdatePullRequestReviewCommentInput { + """ + The text of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the comment to modify. + """ + pullRequestReviewCommentId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) +} + +""" +Autogenerated return type of UpdatePullRequestReviewComment. +""" +type UpdatePullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + pullRequestReviewComment: PullRequestReviewComment +} + +""" +Autogenerated input type of UpdatePullRequestReview +""" +input UpdatePullRequestReviewInput { + """ + The contents of the pull request review body. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request review to modify. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of UpdatePullRequestReview. +""" +type UpdatePullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +Autogenerated input type of UpdateRef +""" +input UpdateRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Permit updates of branch Refs that are not fast-forwards? + """ + force: Boolean = false + + """ + The GitObjectID that the Ref shall be updated to target. + """ + oid: GitObjectID! + + """ + The Node ID of the Ref to be updated. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) +} + +""" +Autogenerated return type of UpdateRef. +""" +type UpdateRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated Ref. + """ + ref: Ref +} + +""" +Autogenerated input type of UpdateRefs +""" +input UpdateRefsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A list of ref updates. + """ + refUpdates: [RefUpdate!]! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UpdateRefs. +""" +type UpdateRefsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateRepository +""" +input UpdateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A new description for the repository. Pass an empty string to erase the existing description. + """ + description: String + + """ + Indicates if the repository should have the discussions feature enabled. + """ + hasDiscussionsEnabled: Boolean + + """ + Indicates if the repository should have the issues feature enabled. + """ + hasIssuesEnabled: Boolean + + """ + Indicates if the repository should have the project boards feature enabled. + """ + hasProjectsEnabled: Boolean + + """ + Indicates if the repository displays a Sponsor button for financial contributions. + """ + hasSponsorshipsEnabled: Boolean + + """ + Indicates if the repository should have the wiki feature enabled. + """ + hasWikiEnabled: Boolean + + """ + The URL for a web page about this repository. Pass an empty string to erase the existing URL. + """ + homepageUrl: URI + + """ + The new name of the repository. + """ + name: String + + """ + The ID of the repository to update. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Whether this repository should be marked as a template such that anyone who + can access it can create new repositories with the same files and directory structure. + """ + template: Boolean +} + +""" +Autogenerated return type of UpdateRepository. +""" +type UpdateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateRepositoryRuleset +""" +input UpdateRepositoryRulesetInput { + """ + A list of actors that are allowed to bypass rules in this ruleset. + """ + bypassActors: [RepositoryRulesetBypassActorInput!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The list of conditions for this ruleset + """ + conditions: RepositoryRuleConditionsInput + + """ + The enforcement level for this ruleset + """ + enforcement: RuleEnforcement + + """ + The name of the ruleset. + """ + name: String + + """ + The global relay id of the repository ruleset to be updated. + """ + repositoryRulesetId: ID! @possibleTypes(concreteTypes: ["RepositoryRuleset"]) + + """ + The list of rules for this ruleset + """ + rules: [RepositoryRuleInput!] + + """ + The target of the ruleset. + """ + target: RepositoryRulesetTarget +} + +""" +Autogenerated return type of UpdateRepositoryRuleset. +""" +type UpdateRepositoryRulesetPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created Ruleset. + """ + ruleset: RepositoryRuleset +} + +""" +Autogenerated input type of UpdateRepositoryWebCommitSignoffSetting +""" +input UpdateRepositoryWebCommitSignoffSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the repository to update. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Indicates if the repository should require signoff on web-based commits. + """ + webCommitSignoffRequired: Boolean! +} + +""" +Autogenerated return type of UpdateRepositoryWebCommitSignoffSetting. +""" +type UpdateRepositoryWebCommitSignoffSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of updating the web commit signoff setting. + """ + message: String + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateSponsorshipPreferences +""" +input UpdateSponsorshipPreferencesInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Specify whether others should be able to see that the sponsor is sponsoring + the sponsorable. Public visibility still does not reveal which tier is used. + """ + privacyLevel: SponsorshipPrivacy = PUBLIC + + """ + Whether the sponsor should receive email updates from the sponsorable. + """ + receiveEmails: Boolean = true + + """ + The ID of the user or organization who is acting as the sponsor, paying for + the sponsorship. Required if sponsorLogin is not given. + """ + sponsorId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsor") + + """ + The username of the user or organization who is acting as the sponsor, paying + for the sponsorship. Required if sponsorId is not given. + """ + sponsorLogin: String + + """ + The ID of the user or organization who is receiving the sponsorship. Required if sponsorableLogin is not given. + """ + sponsorableId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "Sponsorable") + + """ + The username of the user or organization who is receiving the sponsorship. Required if sponsorableId is not given. + """ + sponsorableLogin: String +} + +""" +Autogenerated return type of UpdateSponsorshipPreferences. +""" +type UpdateSponsorshipPreferencesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The sponsorship that was updated. + """ + sponsorship: Sponsorship +} + +""" +Autogenerated input type of UpdateSubscription +""" +input UpdateSubscriptionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new state of the subscription. + """ + state: SubscriptionState! + + """ + The Node ID of the subscribable object to modify. + """ + subscribableId: ID! + @possibleTypes( + concreteTypes: ["Commit", "Discussion", "Issue", "PullRequest", "Repository", "Team", "TeamDiscussion"] + abstractType: "Subscribable" + ) +} + +""" +Autogenerated return type of UpdateSubscription. +""" +type UpdateSubscriptionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The input subscribable entity. + """ + subscribable: Subscribable +} + +""" +Autogenerated input type of UpdateTeamDiscussionComment +""" +input UpdateTeamDiscussionCommentInput { + """ + The updated text of the comment. + """ + body: String! + + """ + The current version of the body content. + """ + bodyVersion: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussionComment"]) +} + +""" +Autogenerated return type of UpdateTeamDiscussionComment. +""" +type UpdateTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + teamDiscussionComment: TeamDiscussionComment +} + +""" +Autogenerated input type of UpdateTeamDiscussion +""" +input UpdateTeamDiscussionInput { + """ + The updated text of the discussion. + """ + body: String + + """ + The current version of the body content. If provided, this update operation + will be rejected if the given version does not match the latest version on the server. + """ + bodyVersion: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussion"]) + + """ + If provided, sets the pinned state of the updated discussion. + """ + pinned: Boolean + + """ + The updated title of the discussion. + """ + title: String +} + +""" +Autogenerated return type of UpdateTeamDiscussion. +""" +type UpdateTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated discussion. + """ + teamDiscussion: TeamDiscussion +} + +""" +Autogenerated input type of UpdateTeamReviewAssignment +""" +input UpdateTeamReviewAssignmentInput { + """ + The algorithm to use for review assignment + """ + algorithm: TeamReviewAssignmentAlgorithm = ROUND_ROBIN + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Count any members whose review has already been requested against the required number of members assigned to review + """ + countMembersAlreadyRequested: Boolean = true + + """ + Turn on or off review assignment + """ + enabled: Boolean! + + """ + An array of team member IDs to exclude + """ + excludedTeamMemberIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The Node ID of the team to update review assignments of + """ + id: ID! @possibleTypes(concreteTypes: ["Team"]) + + """ + Include the members of any child teams when assigning + """ + includeChildTeamMembers: Boolean = true + + """ + Notify the entire team of the PR if it is delegated + """ + notifyTeam: Boolean = true + + """ + Remove the team review request when assigning + """ + removeTeamRequest: Boolean = true + + """ + The number of team members to assign + """ + teamMemberCount: Int = 1 +} + +""" +Autogenerated return type of UpdateTeamReviewAssignment. +""" +type UpdateTeamReviewAssignmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The team that was modified + """ + team: Team +} + +""" +Autogenerated input type of UpdateTeamsRepository +""" +input UpdateTeamsRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Permission that should be granted to the teams. + """ + permission: RepositoryPermission! + + """ + Repository ID being granted access to. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + A list of teams being granted access. Limit: 10 + """ + teamIds: [ID!]! @possibleTypes(concreteTypes: ["Team"]) +} + +""" +Autogenerated return type of UpdateTeamsRepository. +""" +type UpdateTeamsRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that was updated. + """ + repository: Repository + + """ + The teams granted permission on the repository. + """ + teams: [Team!] +} + +""" +Autogenerated input type of UpdateTopics +""" +input UpdateTopicsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + An array of topic names. + """ + topicNames: [String!]! +} + +""" +Autogenerated return type of UpdateTopics. +""" +type UpdateTopicsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Names of the provided topics that are not valid. + """ + invalidTopicNames: [String!] + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateUserList +""" +input UpdateUserListInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A description of the list + """ + description: String + + """ + Whether or not the list is private + """ + isPrivate: Boolean + + """ + The ID of the list to update. + """ + listId: ID! @possibleTypes(concreteTypes: ["UserList"]) + + """ + The name of the list + """ + name: String +} + +""" +Autogenerated return type of UpdateUserList. +""" +type UpdateUserListPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The list that was just updated + """ + list: UserList +} + +""" +Autogenerated input type of UpdateUserListsForItem +""" +input UpdateUserListsForItemInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item to add to the list + """ + itemId: ID! @possibleTypes(concreteTypes: ["Repository"], abstractType: "UserListItems") + + """ + The lists to which this item should belong + """ + listIds: [ID!]! @possibleTypes(concreteTypes: ["UserList"]) + + """ + The suggested lists to create and add this item to + """ + suggestedListIds: [ID!] @possibleTypes(concreteTypes: ["UserListSuggestion"]) +} + +""" +Autogenerated return type of UpdateUserListsForItem. +""" +type UpdateUserListsForItemPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was added + """ + item: UserListItems + + """ + The lists to which this item belongs + """ + lists: [UserList!] + + """ + The user who owns the lists + """ + user: User +} + +""" +A user is an individual's account on GitHub that owns repositories and can make new content. +""" +type User implements Actor & Node & PackageOwner & ProfileOwner & ProjectOwner & ProjectV2Owner & ProjectV2Recent & RepositoryDiscussionAuthor & RepositoryDiscussionCommentAuthor & RepositoryOwner & Sponsorable & UniformResourceLocatable { + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + A URL pointing to the user's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The user's public profile bio. + """ + bio: String + + """ + The user's public profile bio as HTML. + """ + bioHTML: HTML! + + """ + Could this user receive email notifications, if the organization had notification restrictions enabled? + """ + canReceiveOrganizationEmailsWhenNotificationsRestricted( + """ + The login of the organization to check. + """ + login: String! + ): Boolean! + + """ + A list of commit comments made by this user. + """ + commitComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The user's public profile company. + """ + company: String + + """ + The user's public profile company as HTML. + """ + companyHTML: HTML! + + """ + The collection of contributions this user has made to different repositories. + """ + contributionsCollection( + """ + Only contributions made at this time or later will be counted. If omitted, defaults to a year ago. + """ + from: DateTime + + """ + The ID of the organization used to filter contributions. + """ + organizationID: ID + + """ + Only contributions made before and up to (including) this time will be + counted. If omitted, defaults to the current time or one year from the + provided from argument. + """ + to: DateTime + ): ContributionsCollection! + + """ + The user's Copilot endpoint information + """ + copilotEndpoints: CopilotEndpoints + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The user's publicly visible profile email. + """ + email: String! + + """ + A list of enterprises that the user belongs to. + """ + enterprises( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter enterprises returned based on the user's membership type. + """ + membershipType: EnterpriseMembershipType = ALL + + """ + Ordering options for the User's enterprises. + """ + orderBy: EnterpriseOrder = {field: NAME, direction: ASC} + ): EnterpriseConnection + + """ + The estimated next GitHub Sponsors payout for this user/organization in cents (USD). + """ + estimatedNextSponsorsPayoutInCents: Int! + + """ + A list of users the given user is followed by. + """ + followers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FollowerConnection! + + """ + A list of users the given user is following. + """ + following( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FollowingConnection! + + """ + Find gist by repo name. + """ + gist( + """ + The gist name to find. + """ + name: String! + ): Gist + + """ + A list of gist comments made by this user. + """ + gistComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GistCommentConnection! + + """ + A list of the Gists the user has created. + """ + gists( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for gists returned from the connection + """ + orderBy: GistOrder + + """ + Filters Gists according to privacy. + """ + privacy: GistPrivacy + ): GistConnection! + + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + + """ + The hovercard information for this user in a given context + """ + hovercard( + """ + The ID of the subject to get the hovercard in the context of + """ + primarySubjectId: ID + ): Hovercard! + + """ + The Node ID of the User object + """ + id: ID! + + """ + The interaction ability settings for this user. + """ + interactionAbility: RepositoryInteractionAbility + + """ + Whether or not this user is a participant in the GitHub Security Bug Bounty. + """ + isBountyHunter: Boolean! + + """ + Whether or not this user is a participant in the GitHub Campus Experts Program. + """ + isCampusExpert: Boolean! + + """ + Whether or not this user is a GitHub Developer Program member. + """ + isDeveloperProgramMember: Boolean! + + """ + Whether or not this user is a GitHub employee. + """ + isEmployee: Boolean! + + """ + Whether or not this user is following the viewer. Inverse of viewerIsFollowing + """ + isFollowingViewer: Boolean! + + """ + Whether or not this user is a member of the GitHub Stars Program. + """ + isGitHubStar: Boolean! + + """ + Whether or not the user has marked themselves as for hire. + """ + isHireable: Boolean! + + """ + Whether or not this user is a site administrator. + """ + isSiteAdmin: Boolean! + + """ + Whether the given account is sponsoring this user/organization. + """ + isSponsoredBy( + """ + The target account's login. + """ + accountLogin: String! + ): Boolean! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + Whether or not this user is the viewing user. + """ + isViewer: Boolean! + + """ + A list of issue comments made by this user. + """ + issueComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + A list of issues associated with this user. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + Calculate how much each sponsor has ever paid total to this maintainer via + GitHub Sponsors. Does not include sponsorships paid via Patreon. + """ + lifetimeReceivedSponsorshipValues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for results returned from the connection. + """ + orderBy: SponsorAndLifetimeValueOrder = {field: SPONSOR_LOGIN, direction: ASC} + ): SponsorAndLifetimeValueConnection! + + """ + A user-curated list of repositories + """ + lists( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserListConnection! + + """ + The user's public profile location. + """ + location: String + + """ + The username used to login. + """ + login: String! + + """ + The estimated monthly GitHub Sponsors income for this user/organization in cents (USD). + """ + monthlyEstimatedSponsorsIncomeInCents: Int! + + """ + The user's public profile name. + """ + name: String + + """ + Find an organization by its login that the user belongs to. + """ + organization( + """ + The login of the organization to find. + """ + login: String! + ): Organization + + """ + Verified email addresses that match verified domains for a specified organization the user is a member of. + """ + organizationVerifiedDomainEmails( + """ + The login of the organization to match verified domains from. + """ + login: String! + ): [String!]! + + """ + A list of organizations the user belongs to. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the User's organizations. + """ + orderBy: OrganizationOrder = null + ): OrganizationConnection! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Find a project by number. + """ + projectV2( + """ + The project number. + """ + number: Int! + ): ProjectV2 + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + The HTTP path listing user's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing user's projects + """ + projectsUrl: URI! + + """ + A list of projects under the owner. + """ + projectsV2( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter projects based on user role. + """ + minPermissionLevel: ProjectV2PermissionLevel = READ + + """ + How to order the returned projects. + """ + orderBy: ProjectV2Order = {field: NUMBER, direction: DESC} + + """ + A project to search for under the owner. + """ + query: String + ): ProjectV2Connection! + + """ + The user's profile pronouns + """ + pronouns: String + + """ + A list of public keys associated with this user. + """ + publicKeys( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PublicKeyConnection! + + """ + A list of pull requests associated with this user. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + Recent projects that this user has modified in the context of the owner. + """ + recentProjects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectV2Connection! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they are archived and not maintained + """ + isArchived: Boolean + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy. Internal + repositories are considered private; consider using the visibility argument + if only internal repositories are needed. Cannot be combined with the + visibility argument. + """ + privacy: RepositoryPrivacy + + """ + If non-null, filters repositories according to visibility. Cannot be combined with the privacy argument. + """ + visibility: RepositoryVisibility + ): RepositoryConnection! + + """ + A list of repositories that the user recently contributed to. + """ + repositoriesContributedTo( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + If non-null, include only the specified types of contributions. The + GitHub.com UI uses [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY] + """ + contributionTypes: [RepositoryContributionType] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssues: Boolean + + """ + If true, include user repositories + """ + includeUserRepositories: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Follow repository renames. If disabled, a repository referenced by its old name will return an error. + """ + followRenames: Boolean = true + + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + Discussion comments this user has authored. + """ + repositoryDiscussionComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter discussion comments to only those that were marked as the answer + """ + onlyAnswers: Boolean = false + + """ + Filter discussion comments to only those in a specific repository. + """ + repositoryId: ID + ): DiscussionCommentConnection! + + """ + Discussions this user has started. + """ + repositoryDiscussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter discussions to only those that have been answered or not. Defaults to + including both answered and unanswered discussions. + """ + answered: Boolean = null + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for discussions returned from the connection. + """ + orderBy: DiscussionOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter discussions to only those in a specific repository. + """ + repositoryId: ID + + """ + A list of states to filter the discussions by. + """ + states: [DiscussionState!] = [] + ): DiscussionConnection! + + """ + The HTTP path for this user + """ + resourcePath: URI! + + """ + Replies this user has saved + """ + savedReplies( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The field to order saved replies by. + """ + orderBy: SavedReplyOrder = {field: UPDATED_AT, direction: DESC} + ): SavedReplyConnection + + """ + The user's social media accounts, ordered as they appear on the user's profile. + """ + socialAccounts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SocialAccountConnection! + + """ + List of users and organizations this entity is sponsoring. + """ + sponsoring( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the users and organizations returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + ): SponsorConnection! + + """ + List of sponsors for this user or organization. + """ + sponsors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsors returned from the connection. + """ + orderBy: SponsorOrder = {field: RELEVANCE, direction: DESC} + + """ + If given, will filter for sponsors at the given tier. Will only return + sponsors whose tier the viewer is permitted to see. + """ + tierId: ID + ): SponsorConnection! + + """ + Events involving this sponsorable, such as new sponsorships. + """ + sponsorsActivities( + """ + Filter activities to only the specified actions. + """ + actions: [SponsorsActivityAction!] = [] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to include those events where this sponsorable acted as the sponsor. + Defaults to only including events where this sponsorable was the recipient + of a sponsorship. + """ + includeAsSponsor: Boolean = false + + """ + Whether or not to include private activities in the result set. Defaults to including public and private activities. + """ + includePrivate: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for activity returned from the connection. + """ + orderBy: SponsorsActivityOrder = {field: TIMESTAMP, direction: DESC} + + """ + Filter activities returned to only those that occurred in the most recent + specified time period. Set to ALL to avoid filtering by when the activity + occurred. Will be ignored if `since` or `until` is given. + """ + period: SponsorsActivityPeriod = MONTH + + """ + Filter activities to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter activities to those that occurred before this time. + """ + until: DateTime + ): SponsorsActivityConnection! + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + The sponsorship from the viewer to this user/organization; that is, the sponsorship where you're the sponsor. + """ + sponsorshipForViewerAsSponsor( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the viewer's sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + The sponsorship from this user/organization to the viewer; that is, the sponsorship you're receiving. + """ + sponsorshipForViewerAsSponsorable( + """ + Whether to return the sponsorship only if it's still active. Pass false to + get the sponsorship back even if it has been cancelled. + """ + activeOnly: Boolean = true + ): Sponsorship + + """ + List of sponsorship updates sent from this sponsorable to sponsors. + """ + sponsorshipNewsletters( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorship updates returned from the connection. + """ + orderBy: SponsorshipNewsletterOrder = {field: CREATED_AT, direction: DESC} + ): SponsorshipNewsletterConnection! + + """ + The sponsorships where this user or organization is the maintainer receiving the funds. + """ + sponsorshipsAsMaintainer( + """ + Whether to include only sponsorships that are active right now, versus all + sponsorships this maintainer has ever received. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + The sponsorships where this user or organization is the funder. + """ + sponsorshipsAsSponsor( + """ + Whether to include only sponsorships that are active right now, versus all sponsorships this sponsor has ever made. + """ + activeOnly: Boolean = true + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter sponsorships returned to those for the specified maintainers. That + is, the recipient of the sponsorship is a user or organization with one of + the given logins. + """ + maintainerLogins: [String!] + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + Repositories the user has starred. + """ + starredRepositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + + """ + Filters starred repositories to only return repositories owned by the viewer. + """ + ownedByViewer: Boolean + ): StarredRepositoryConnection! + + """ + The user's description of what they're currently doing. + """ + status: UserStatus + + """ + Suggested names for user lists + """ + suggestedListNames: [UserListSuggestion!]! + + """ + Repositories the user has contributed to, ordered by contribution rank, plus repositories the user has created + """ + topRepositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder! + + """ + How far back in time to fetch contributed repositories + """ + since: DateTime + ): RepositoryConnection! + + """ + The amount in United States cents (e.g., 500 = $5.00 USD) that this entity has + spent on GitHub to fund sponsorships. Only returns a value when viewed by the + user themselves or by a user who can manage sponsorships for the requested organization. + """ + totalSponsorshipAmountAsSponsorInCents( + """ + Filter payments to those that occurred on or after this time. + """ + since: DateTime + + """ + Filter payments to those made to the users or organizations with the specified usernames. + """ + sponsorableLogins: [String!] = [] + + """ + Filter payments to those that occurred before this time. + """ + until: DateTime + ): Int + + """ + The user's Twitter username. + """ + twitterUsername: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this user + """ + url: URI! + + """ + Whether the request returns publicly visible information or privately visible information about the user + """ + userViewType: UserViewType! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + @deprecated( + reason: "Projects (classic) is being deprecated in favor of the new Projects experience, see: https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/. Removal on 2025-04-01 UTC." + ) + + """ + Whether or not the viewer is able to follow the user. + """ + viewerCanFollow: Boolean! + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + Whether or not this user is followed by the viewer. Inverse of isFollowingViewer. + """ + viewerIsFollowing: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! + + """ + A list of repositories the given user is watching. + """ + watching( + """ + Affiliation options for repositories returned from the connection. If none + specified, the results will include repositories for which the current + viewer is an owner or collaborator, or member. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have issues enabled + """ + hasIssuesEnabled: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy. Internal + repositories are considered private; consider using the visibility argument + if only internal repositories are needed. Cannot be combined with the + visibility argument. + """ + privacy: RepositoryPrivacy + + """ + If non-null, filters repositories according to visibility. Cannot be combined with the privacy argument. + """ + visibility: RepositoryVisibility + ): RepositoryConnection! + + """ + A URL pointing to the user's public website/blog. + """ + websiteUrl: URI +} + +""" +The possible durations that a user can be blocked for. +""" +enum UserBlockDuration { + """ + The user was blocked for 1 day + """ + ONE_DAY + + """ + The user was blocked for 30 days + """ + ONE_MONTH + + """ + The user was blocked for 7 days + """ + ONE_WEEK + + """ + The user was blocked permanently + """ + PERMANENT + + """ + The user was blocked for 3 days + """ + THREE_DAYS +} + +""" +Represents a 'user_blocked' event on a given user. +""" +type UserBlockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Number of days that the user was blocked for. + """ + blockDuration: UserBlockDuration! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The Node ID of the UserBlockedEvent object + """ + id: ID! + + """ + The user who was blocked. + """ + subject: User +} + +""" +A list of users. +""" +type UserConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edit on user content +""" +type UserContentEdit implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the date and time when the object was deleted. + """ + deletedAt: DateTime + + """ + The actor who deleted this content + """ + deletedBy: Actor + + """ + A summary of the changes for this edit + """ + diff: String + + """ + When this content was edited + """ + editedAt: DateTime! + + """ + The actor who edited this content + """ + editor: Actor + + """ + The Node ID of the UserContentEdit object + """ + id: ID! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +A list of edits to content. +""" +type UserContentEditConnection { + """ + A list of edges. + """ + edges: [UserContentEditEdge] + + """ + A list of nodes. + """ + nodes: [UserContentEdit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserContentEditEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserContentEdit +} + +""" +Represents a user. +""" +type UserEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User +} + +""" +Email attributes from External Identity +""" +type UserEmailMetadata { + """ + Boolean to identify primary emails + """ + primary: Boolean + + """ + Type of email + """ + type: String + + """ + Email id + """ + value: String! +} + +""" +A user-curated list of repositories +""" +type UserList implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The description of this list + """ + description: String + + """ + The Node ID of the UserList object + """ + id: ID! + + """ + Whether or not this list is private + """ + isPrivate: Boolean! + + """ + The items associated with this list + """ + items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserListItemsConnection! + + """ + The date and time at which this list was created or last had items added to it + """ + lastAddedAt: DateTime! + + """ + The name of this list + """ + name: String! + + """ + The slug of this list + """ + slug: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user to which this list belongs + """ + user: User! +} + +""" +The connection type for UserList. +""" +type UserListConnection { + """ + A list of edges. + """ + edges: [UserListEdge] + + """ + A list of nodes. + """ + nodes: [UserList] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserListEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserList +} + +""" +Types that can be added to a user list. +""" +union UserListItems = Repository + +""" +The connection type for UserListItems. +""" +type UserListItemsConnection { + """ + A list of edges. + """ + edges: [UserListItemsEdge] + + """ + A list of nodes. + """ + nodes: [UserListItems] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserListItemsEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserListItems +} + +""" +Represents a suggested user list. +""" +type UserListSuggestion { + """ + The ID of the suggested user list + """ + id: ID + + """ + The name of the suggested user list + """ + name: String +} + +""" +A repository owned by an Enterprise Managed user. +""" +type UserNamespaceRepository implements Node { + """ + The Node ID of the UserNamespaceRepository object + """ + id: ID! + + """ + The name of the repository. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! + + """ + The user owner of the repository. + """ + owner: RepositoryOwner! +} + +""" +A list of repositories owned by users in an enterprise with Enterprise Managed Users. +""" +type UserNamespaceRepositoryConnection { + """ + A list of edges. + """ + edges: [UserNamespaceRepositoryEdge] + + """ + A list of nodes. + """ + nodes: [UserNamespaceRepository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserNamespaceRepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserNamespaceRepository +} + +""" +The user's description of what they're currently doing. +""" +type UserStatus implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + An emoji summarizing the user's status. + """ + emoji: String + + """ + The status emoji as HTML. + """ + emojiHTML: HTML + + """ + If set, the status will not be shown after this date. + """ + expiresAt: DateTime + + """ + The Node ID of the UserStatus object + """ + id: ID! + + """ + Whether this status indicates the user is not fully available on GitHub. + """ + indicatesLimitedAvailability: Boolean! + + """ + A brief message describing what the user is doing. + """ + message: String + + """ + The organization whose members can see this status. If null, this status is publicly visible. + """ + organization: Organization + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user who has this status. + """ + user: User! +} + +""" +The connection type for UserStatus. +""" +type UserStatusConnection { + """ + A list of edges. + """ + edges: [UserStatusEdge] + + """ + A list of nodes. + """ + nodes: [UserStatus] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserStatusEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserStatus +} + +""" +Ordering options for user status connections. +""" +input UserStatusOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user statuses by. + """ + field: UserStatusOrderField! +} + +""" +Properties by which user status connections can be ordered. +""" +enum UserStatusOrderField { + """ + Order user statuses by when they were updated. + """ + UPDATED_AT +} + +""" +Whether a user being viewed contains public or private information. +""" +enum UserViewType { + """ + A user containing information only visible to the authenticated user. + """ + PRIVATE + + """ + A user that is publicly visible. + """ + PUBLIC +} + +""" +A domain that can be verified or approved for an organization or an enterprise. +""" +type VerifiableDomain implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The DNS host name that should be used for verification. + """ + dnsHostName: URI + + """ + The unicode encoded domain. + """ + domain: URI! + + """ + Whether a TXT record for verification with the expected host name was found. + """ + hasFoundHostName: Boolean! + + """ + Whether a TXT record for verification with the expected verification token was found. + """ + hasFoundVerificationToken: Boolean! + + """ + The Node ID of the VerifiableDomain object + """ + id: ID! + + """ + Whether or not the domain is approved. + """ + isApproved: Boolean! + + """ + Whether this domain is required to exist for an organization or enterprise policy to be enforced. + """ + isRequiredForPolicyEnforcement: Boolean! + + """ + Whether or not the domain is verified. + """ + isVerified: Boolean! + + """ + The owner of the domain. + """ + owner: VerifiableDomainOwner! + + """ + The punycode encoded domain. + """ + punycodeEncodedDomain: URI! + + """ + The time that the current verification token will expire. + """ + tokenExpirationTime: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The current verification token for the domain. + """ + verificationToken: String +} + +""" +The connection type for VerifiableDomain. +""" +type VerifiableDomainConnection { + """ + A list of edges. + """ + edges: [VerifiableDomainEdge] + + """ + A list of nodes. + """ + nodes: [VerifiableDomain] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type VerifiableDomainEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: VerifiableDomain +} + +""" +Ordering options for verifiable domain connections. +""" +input VerifiableDomainOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order verifiable domains by. + """ + field: VerifiableDomainOrderField! +} + +""" +Properties by which verifiable domain connections can be ordered. +""" +enum VerifiableDomainOrderField { + """ + Order verifiable domains by their creation date. + """ + CREATED_AT + + """ + Order verifiable domains by the domain name. + """ + DOMAIN +} + +""" +Types that can own a verifiable domain. +""" +union VerifiableDomainOwner = Enterprise | Organization + +""" +Autogenerated input type of VerifyVerifiableDomain +""" +input VerifyVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to verify. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of VerifyVerifiableDomain. +""" +type VerifyVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verifiable domain that was verified. + """ + domain: VerifiableDomain +} + +""" +A hovercard context with a message describing how the viewer is related. +""" +type ViewerHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Identifies the user who is related to this context. + """ + viewer: User! +} + +""" +A subject that may be upvoted. +""" +interface Votable { + """ + Number of upvotes that this subject has received. + """ + upvoteCount: Int! + + """ + Whether or not the current user can add or remove an upvote on this subject. + """ + viewerCanUpvote: Boolean! + + """ + Whether or not the current user has already upvoted this subject. + """ + viewerHasUpvoted: Boolean! +} + +""" +A workflow contains meta information about an Actions workflow file. +""" +type Workflow implements Node & UniformResourceLocatable { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Node ID of the Workflow object + """ + id: ID! + + """ + The name of the workflow. + """ + name: String! + + """ + The HTTP path for this workflow + """ + resourcePath: URI! + + """ + The runs of the workflow. + """ + runs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the connection + """ + orderBy: WorkflowRunOrder = {field: CREATED_AT, direction: DESC} + ): WorkflowRunConnection! + + """ + The state of the workflow. + """ + state: WorkflowState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this workflow + """ + url: URI! +} + +""" +A workflow that must run for this rule to pass +""" +type WorkflowFileReference { + """ + The path to the workflow file + """ + path: String! + + """ + The ref (branch or tag) of the workflow file to use + """ + ref: String + + """ + The ID of the repository where the workflow is defined + """ + repositoryId: Int! + + """ + The commit SHA of the workflow file to use + """ + sha: String +} + +""" +A workflow that must run for this rule to pass +""" +input WorkflowFileReferenceInput { + """ + The path to the workflow file + """ + path: String! + + """ + The ref (branch or tag) of the workflow file to use + """ + ref: String + + """ + The ID of the repository where the workflow is defined + """ + repositoryId: Int! + + """ + The commit SHA of the workflow file to use + """ + sha: String +} + +""" +A workflow run. +""" +type WorkflowRun implements Node & UniformResourceLocatable { + """ + The check suite this workflow run belongs to. + """ + checkSuite: CheckSuite! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The log of deployment reviews + """ + deploymentReviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentReviewConnection! + + """ + The event that triggered the workflow run + """ + event: String! + + """ + The workflow file + """ + file: WorkflowRunFile + + """ + The Node ID of the WorkflowRun object + """ + id: ID! + + """ + The pending deployment requests of all check runs in this workflow run + """ + pendingDeploymentRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentRequestConnection! + + """ + The HTTP path for this workflow run + """ + resourcePath: URI! + + """ + A number that uniquely identifies this workflow run in its parent workflow. + """ + runNumber: Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this workflow run + """ + url: URI! + + """ + The workflow executed in this workflow run. + """ + workflow: Workflow! +} + +""" +The connection type for WorkflowRun. +""" +type WorkflowRunConnection { + """ + A list of edges. + """ + edges: [WorkflowRunEdge] + + """ + A list of nodes. + """ + nodes: [WorkflowRun] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type WorkflowRunEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: WorkflowRun +} + +""" +An executed workflow file for a workflow run. +""" +type WorkflowRunFile implements Node & UniformResourceLocatable { + """ + The Node ID of the WorkflowRunFile object + """ + id: ID! + + """ + The path of the workflow file relative to its repository. + """ + path: String! + + """ + The direct link to the file in the repository which stores the workflow file. + """ + repositoryFileUrl: URI! + + """ + The repository name and owner which stores the workflow file. + """ + repositoryName: URI! + + """ + The HTTP path for this workflow run file + """ + resourcePath: URI! + + """ + The parent workflow run execution for this file. + """ + run: WorkflowRun! + + """ + The HTTP URL for this workflow run file + """ + url: URI! + + """ + If the viewer has permissions to push to the repository which stores the workflow. + """ + viewerCanPushRepository: Boolean! + + """ + If the viewer has permissions to read the repository which stores the workflow. + """ + viewerCanReadRepository: Boolean! +} + +""" +Ways in which lists of workflow runs can be ordered upon return. +""" +input WorkflowRunOrder { + """ + The direction in which to order workflow runs by the specified field. + """ + direction: OrderDirection! + + """ + The field by which to order workflows. + """ + field: WorkflowRunOrderField! +} + +""" +Properties by which workflow run connections can be ordered. +""" +enum WorkflowRunOrderField { + """ + Order workflow runs by most recently created + """ + CREATED_AT +} + +""" +The possible states for a workflow. +""" +enum WorkflowState { + """ + The workflow is active. + """ + ACTIVE + + """ + The workflow was deleted from the git repository. + """ + DELETED + + """ + The workflow was disabled by default on a fork. + """ + DISABLED_FORK + + """ + The workflow was disabled for inactivity in the repository. + """ + DISABLED_INACTIVITY + + """ + The workflow was disabled manually. + """ + DISABLED_MANUALLY +} + +""" +Require all changes made to a targeted branch to pass the specified workflows before they can be merged. +""" +type WorkflowsParameters { + """ + Allow repositories and branches to be created if a check would otherwise prohibit it. + """ + doNotEnforceOnCreate: Boolean! + + """ + Workflows that must pass for this rule to pass. + """ + workflows: [WorkflowFileReference!]! +} + +""" +Require all changes made to a targeted branch to pass the specified workflows before they can be merged. +""" +input WorkflowsParametersInput { + """ + Allow repositories and branches to be created if a check would otherwise prohibit it. + """ + doNotEnforceOnCreate: Boolean + + """ + Workflows that must pass for this rule to pass. + """ + workflows: [WorkflowFileReferenceInput!]! +} + +""" +A valid x509 certificate string +""" +scalar X509Certificate diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/petstore-1.0-examples.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/petstore-1.0-examples.yaml new file mode 100644 index 000000000..35a42a64d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/petstore-1.0-examples.yaml @@ -0,0 +1,123 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: Petstore Graph API + version: '1.0' +operations: + allPets: + allPets: + request: + body: + query: |- + query { + allPets + } + response: + mediaType: application/json + body: + data: + allPets: + - id: "1" + name: Zaza + color: blue + - id: "2" + name: Tigress + color: stripped + - id: "3" + name: Maki + color: calico + - id: "4" + name: Toufik + color: stripped + + searchPets: + k pets: + request: + body: + query: |- + query searchPets ($name: String) { + searchPets (name: $name) { + id + name + color + } + } + variables: + name: k + response: + mediaType: application/json + body: |- + { + "data": { + "searchPets": [ + { + "id": "3", + "name": "Maki", + "color": "calico" + }, + { + "id": "4", + "name": "Toufik", + "color": "stripped" + } + ] + } + } + 'i pets': + request: + body: + query: |- + query searchPets ($name: String) { + searchPets (name: $name) { + id + name + color + } + } + variables: + name: i + response: + mediaType: application/json + body: + data: + searchPets: + - id: "2" + name: Tigress + color: stripped + - id: "3" + name: Maki + color: calico + - id: "4" + name: Toufik + color: stripped + createPet: + new pet: + request: + body: + query: |- + mutation createPet($newPet: NewPet) { + createPet(review: $newPet) { + id + name + color + } + } + variables: |- + { + "newPet": { + "name": "Jojo", + "color": "tuxedo" + } + } + response: + body: |- + { + "data": { + "createPet": { + "id": "{{ randomInt(5, 10) }}", + "name": "{{ request.body/variables/newPet/name }}", + "color": "{{ request.body/variables/newPet/color }}" + } + } + } + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/petstore-1.0.graphql b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/petstore-1.0.graphql new file mode 100644 index 000000000..3fe61202b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/graphql/petstore-1.0.graphql @@ -0,0 +1,33 @@ +# microcksId: Petstore Graph API : 1.0 +schema { + query: Query + mutation: Mutation +} + +type Pet { + id: ID! + name: String! + color: String! +} + +type NewPet { + name: String! + color: String! +} + +type Filter { + name: String! + value: String! +} + +type Query { + # Retrieve all pets from the store. + # This is not a paginated query. + allPets: [Pet]! + searchPets(name: String!): [Pet] + advancedSearchPets(filters: [Filter]!): [Pet] +} + +type Mutation { + createPet(newPet: NewPet!): Pet +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/HelloService.metadata.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/HelloService.metadata.yml new file mode 100644 index 000000000..b4c218715 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/HelloService.metadata.yml @@ -0,0 +1,20 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIMetadata +metadata: + name: io.github.microcks.grpc.hello.v1.HelloService + version: v1 + labels: + domain: samples + status: GA +operations: + 'greeting': + dispatcher: JSON_BODY + dispatcherRules: |- + { + "exp": "/firstname", + "operator": "equals", + "cases": { + "Laurent": "Laurent", + "default": "Philippe" + } + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/HelloService.postman.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/HelloService.postman.json new file mode 100644 index 000000000..973f852d0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/HelloService.postman.json @@ -0,0 +1,99 @@ +{ + "info": { + "_postman_id": "448f2755-4b3c-45a7-b6d4-af16d8110e9d", + "name": "io.github.microcks.grpc.hello.v1.HelloService", + "description": "version=v1 - This collection holds mock messages for HelloService v1 GRPC", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "greeting", + "item": [ + { + "name": "greeting", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http:///greeting", + "protocol": "http", + "path": [ + "greeting" + ] + } + }, + "response": [ + { + "name": "Philippe", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstname\": \"Philippe\",\n \"lastname\": \"Huet\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http:///greeting", + "protocol": "http", + "path": [ + "greeting" + ] + } + }, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"greeting\": \"Hello Philippe Huet !\"\n}" + }, + { + "name": "Laurent", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstname\": \"Laurent\",\n \"lastname\": \"Broudoux\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http:///greeting", + "protocol": "http", + "path": [ + "greeting" + ] + } + }, + "_postman_previewlanguage": "json", + "header": null, + "cookie": [], + "body": "{\n \"greeting\": \"Hello Laurent Broudoux !\"\n}" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/TestServiceMissingEnum.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/TestServiceMissingEnum.proto new file mode 100644 index 000000000..d99714951 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/TestServiceMissingEnum.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package hello; + +import "protos/common.proto"; + +service TestService { + rpc unary(UnaryRequest) returns (UnaryResponse) {} +} + +message UnaryRequest { + // Comment this line, import will work + EnumTest type = 1; + Filters filters = 2; +} + +message UnaryResponse { + string message = 1; +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/example-any-v1.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/example-any-v1.proto new file mode 100644 index 000000000..ba72a8a71 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/example-any-v1.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; +package example.v1; + +import "google/protobuf/any.proto"; + +service ExampleServiceV1 { + rpc Example (ExampleMessage) returns (ExampleResponseMessage) {} +} + +message ExampleMessage { + message ExamplePayload { + google.protobuf.Any parameters = 1; + } + ExamplePayload payload = 1; +} + +message ExampleResponseMessage { + message ExampleResponsePayload { + } + ExampleResponsePayload payload = 1; +} + +message ExampleParametersV1 { + string text = 1; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/goodbye-v1.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/goodbye-v1.proto new file mode 100644 index 000000000..f2eed0528 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/goodbye-v1.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package io.github.microcks.grpc.goodbye.v1; + +import "shared/uuid.proto"; + +option java_multiple_files = true; + +message GoodbyeRequest { + string firstname = 1; + string lastname = 2; +} + +message GoodbyeResponse { + string farewell = 1; + shared.UUID messageId = 2; +} + +service GoodbyeService { + rpc goodbye(GoodbyeRequest) returns (GoodbyeResponse); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/google/api/annotations.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/google/api/annotations.proto new file mode 100644 index 000000000..d06b0238e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/google/api/annotations.proto @@ -0,0 +1,27 @@ + +// Copyright (c) 2015, Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +syntax = "proto3"; +package google.api; +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/google/api/http.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/google/api/http.proto new file mode 100644 index 000000000..10bc1f274 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/google/api/http.proto @@ -0,0 +1,275 @@ +// Copyright 2016 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +syntax = "proto3"; +package google.api; +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; +// Defines the HTTP configuration for a service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; +} +// `HttpRule` defines the mapping of an RPC method to one or more HTTP +// REST APIs. The mapping determines what portions of the request +// message are populated from the path, query parameters, or body of +// the HTTP request. The mapping is typically specified as an +// `google.api.http` annotation, see "google/api/annotations.proto" +// for details. +// +// The mapping consists of a field specifying the path template and +// method kind. The path template can refer to fields in the request +// message, as in the example below which describes a REST GET +// operation on a resource collection of messages: +// +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}"; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // mapped to the URL +// SubMessage sub = 2; // `sub.subfield` is url-mapped +// } +// message Message { +// string text = 1; // content of the resource +// } +// +// The same http annotation can alternatively be expressed inside the +// `GRPC API Configuration` YAML file. +// +// http: +// rules: +// - selector: .Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// This definition enables an automatic, bidrectional mapping of HTTP +// JSON to RPC. Example: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))` +// +// In general, not only fields but also field paths can be referenced +// from a path pattern. Fields mapped to the path pattern cannot be +// repeated and must have a primitive (non-message) type. +// +// Any fields in the request message which are not bound by the path +// pattern automatically become (optional) HTTP query +// parameters. Assume the following definition of the request message: +// +// +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // mapped to the URL +// int64 revision = 2; // becomes a parameter +// SubMessage sub = 3; // `sub.subfield` becomes a parameter +// } +// +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))` +// +// Note that fields which are mapped to HTTP parameters must have a +// primitive type or a repeated primitive type. Message types are not +// allowed. In the case of a repeated type, the parameter can be +// repeated in the URL, as in `...?param=A¶m=B`. +// +// For HTTP method kinds which allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// put: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | RPC +// -----|----- +// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// put: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | RPC +// -----|----- +// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice of +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// +// This enables the following two alternative HTTP JSON to RPC +// mappings: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")` +// +// # Rules for HTTP mapping +// +// The rules for mapping HTTP path, query parameters, and body fields +// to the request message are as follows: +// +// 1. The `body` field specifies either `*` or a field path, or is +// omitted. If omitted, it assumes there is no HTTP body. +// 2. Leaf fields (recursive expansion of nested messages in the +// request) can be classified into three types: +// (a) Matched in the URL template. +// (b) Covered by body (if body is `*`, everything except (a) fields; +// else everything under the body field) +// (c) All other fields. +// 3. URL query parameters found in the HTTP request are mapped to (c) fields. +// 4. Any body sent with an HTTP request can contain only (b) fields. +// +// The syntax of the path template is as follows: +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single path segment. It follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion. +// +// The syntax `**` matches zero or more path segments. It follows the semantics +// of [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.3 Reserved +// Expansion. NOTE: it must be the last segment in the path except the Verb. +// +// The syntax `LITERAL` matches literal text in the URL path. +// +// The syntax `Variable` matches the entire path as specified by its template; +// this nested template must not contain further variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// NOTE: the field paths in variables and in the `body` must not refer to +// repeated fields or map fields. +// +// Use CustomHttpPattern to specify any HTTP method that is not included in the +// `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for +// a given URL path rule. The wild-card rule is useful for services that provide +// content to Web (HTML) clients. +message HttpRule { + // Selects methods to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Used for listing and getting information about resources. + string get = 2; + // Used for updating a resource. + string put = 3; + // Used for creating a resource. + string post = 4; + // Used for deleting a resource. + string delete = 5; + // Used for updating a resource. + string patch = 6; + // Custom pattern is used for defining custom verbs. + CustomHttpPattern custom = 8; + } + // The name of the request field whose value is mapped to the HTTP body, or + // `*` for mapping all fields not captured by the path pattern to the HTTP + // body. NOTE: the referred field must not be a repeated field and must be + // present at the top-level of request message type. + string body = 7; + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + // The path matched by this custom verb. + string path = 2; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-examples-errors.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-examples-errors.yml new file mode 100644 index 000000000..41e8e18a4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-examples-errors.yml @@ -0,0 +1,24 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: io.github.microcks.grpc.hello.v1.HelloService + version: v1 +operations: + 'greeting': + Laurent: + request: + body: + firstname: Laurent + lastname: Broudoux + response: + status: 200 + body: + greeting: Hello Laurent Broudoux ! + John: + request: + body: |- + {"firstname": "John", "lastname": "Doe"} + response: + status: 5 + body: + greeting: Unknown user \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-examples.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-examples.yml new file mode 100644 index 000000000..6185b60b4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-examples.yml @@ -0,0 +1,22 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: io.github.microcks.grpc.hello.v1.HelloService + version: v1 +operations: + 'greeting': + Laurent: + request: + body: + firstname: Laurent + lastname: Broudoux + response: + body: + greeting: Hello Laurent Broudoux ! + John: + request: + body: |- + {"firstname": "John", "lastname": "Doe"} + response: + body: + greeting: Hello John Doe ! diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-option.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-option.proto new file mode 100644 index 000000000..895b9a1a0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1-option.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package io.github.microcks.grpc.hello.v1; + +option java_multiple_files = true; + +import "google/api/annotations.proto"; +import "google/api/http.proto"; + +message HelloRequest { + string firstname = 1; + string lastname = 2; +} + +message HelloResponse { + string greeting = 1; +} + +service HelloService { + rpc greeting(HelloRequest) returns (HelloResponse){ + option (google.api.http) = { + post: "/v1/hello/" + body: "*" + }; + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1.proto new file mode 100644 index 000000000..b15ca5b69 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/hello-v1.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package io.github.microcks.grpc.hello.v1; + +option java_multiple_files = true; + +message HelloRequest { + string firstname = 1; + string lastname = 2; +} + +message HelloResponse { + string greeting = 1; +} + +service HelloService { + rpc greeting(HelloRequest) returns (HelloResponse); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v1-examples.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v1-examples.yaml new file mode 100644 index 000000000..3e9ef49ec --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v1-examples.yaml @@ -0,0 +1,65 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: org.acme.petstore.v1.PetstoreService + version: v1 +operations: + getPets: + All Pets: + request: + body: "" + response: + body: + pets: + - id: 1 + name: Zaza + - id: 2 + name: Tigress + - id: 3 + name: Maki + - id: 4 + name: Toufik + searchPets: + k pets: + request: + body: |- + { "name": "k" } + response: + body: |- + { + "pets": [ + { + "id": 3, + "name": "Maki" + }, + { + "id": 4, + "name": "Toufik" + } + ] + } + 'i pets': + request: + body: + name: i + response: + body: + pets: + - id: 2 + name: Tigress + - id: 3 + name: Maki + - id: 4 + name: Toufik + createPet: + new pet: + request: + body: + name: Jojo + response: + body: |- + { + "id": {{ randomInt(5,10) }}, + "name": "{{ request.body/name }}" + } + diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v1.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v1.proto new file mode 100644 index 000000000..0faa3b3c7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v1.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package org.acme.petstore.v1; + +message Pet { + int32 id = 1; + string name = 2; +} + +message AllPetsRequest {} + +message PetsResponse { + repeated Pet pets = 1; +} + +message PetSearchRequest { + string name = 1; +} + +message PetNameRequest { + string name = 1; +} + +service PetstoreService { + rpc getPets(AllPetsRequest) returns (PetsResponse); + rpc searchPets(PetSearchRequest) returns (PetsResponse); + rpc createPet(PetNameRequest) returns (Pet); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v2.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v2.proto new file mode 100644 index 000000000..57b418e59 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/petstore-v2.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; + +package org.acme.petstore.v2; + +message Pet { + int32 id = 1; + string name = 2; + Coat coat = 3; + repeated Bug bugs = 4; +} + +message Coat { + string name = 1; + Tint tint = 2; +} + +enum Tint { + LIGHT = 0; + DARK = 1; +} + +enum Bug { + TICK = 0; + FLEA = 1; +} + +message AllPetsRequest {} + +message PetsResponse { + repeated Pet pets = 1; +} + +message PetSearchRequest { + string name = 1; +} + +message PetNameRequest { + string name = 1; + Coat coat = 2; + repeated Bug bugs = 3; + repeated string tags = 4; + repeated FooBar foobars = 5; +} + +message FooBar { + string foo = 1; + string bar = 2; +} + +service PetstoreService { + rpc getPets(AllPetsRequest) returns (PetsResponse); + rpc searchPets(PetSearchRequest) returns (PetsResponse); + rpc createPet(PetNameRequest) returns (Pet); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/protos/common.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/protos/common.proto new file mode 100644 index 000000000..d6456dc10 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/protos/common.proto @@ -0,0 +1,14 @@ + +syntax = "proto3"; + +package hello; + +enum EnumTest { + ENUM_TEST_UNSPECIFIED = 0; + ENUM_TEST_TOTO = 1; + ENUM_TEST_TATA = 2; +} + +message Filters { + string message = 1; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/remote/goodbye-v1.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/remote/goodbye-v1.proto new file mode 100644 index 000000000..f2eed0528 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/remote/goodbye-v1.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package io.github.microcks.grpc.goodbye.v1; + +import "shared/uuid.proto"; + +option java_multiple_files = true; + +message GoodbyeRequest { + string firstname = 1; + string lastname = 2; +} + +message GoodbyeResponse { + string farewell = 1; + shared.UUID messageId = 2; +} + +service GoodbyeService { + rpc goodbye(GoodbyeRequest) returns (GoodbyeResponse); +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/shared/uuid.proto b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/shared/uuid.proto new file mode 100644 index 000000000..a32736fc7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/grpc/shared/uuid.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package shared; + +message UUID { + string id = 1; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastries-0.0.1-with-prefix.har b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastries-0.0.1-with-prefix.har new file mode 100644 index 000000000..40bb3f754 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastries-0.0.1-with-prefix.har @@ -0,0 +1,929 @@ +{ + "log": { + "version": "1.2", + "comment": "microcksId: API Pastries:0.0.1 \n apiPrefix: /rest/API+Pastries/0.0.1", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [ + { + "startedDateTime": "2023-08-16T13:03:11.877Z", + "id": "page_6", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=L", + "pageTimings": { + "onContentLoad": 39.11599999992177, + "onLoad": 38.55900000780821 + } + }, + { + "startedDateTime": "2023-08-17T08:42:25.550Z", + "id": "page_7", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=S", + "pageTimings": { + "onContentLoad": 47.222999986843206, + "onLoad": 47.03399998834357 + } + }, + { + "startedDateTime": "2023-08-17T08:43:27.255Z", + "id": "page_8", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=M", + "pageTimings": { + "onContentLoad": 32.441999996080995, + "onLoad": 32.24200000113342 + } + }, + { + "startedDateTime": "2023-08-17T08:43:38.923Z", + "id": "page_9", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Millefeuille", + "pageTimings": { + "onContentLoad": 32.896999997319654, + "onLoad": 32.69799999543466 + } + }, + { + "startedDateTime": "2023-08-17T08:43:48.176Z", + "id": "page_10", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Eclair+Cafe", + "pageTimings": { + "onContentLoad": 30.78599998843856, + "onLoad": 30.63899998960551 + } + } + ], + "entries": [ + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "275560", + "pageref": "page_6", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=L", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "L" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "256" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Wed, 16 Aug 2023 13:03:11 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 256, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Baba Rhum\",\"description\":\"Delicieux Baba au Rhum pas calorique du tout\",\"size\":\"L\",\"price\":3.2,\"status\":\"available\"},{\"name\":\"Millefeuille\",\"description\":\"Delicieux Millefeuille pas calorique du tout\",\"size\":\"L\",\"price\":4.4,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 257, + "_transferSize": 514, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T13:03:11.873Z", + "time": 25.34499999912642, + "timings": { + "blocked": 6.360000002148562, + "dns": 0.008000000000000007, + "ssl": -1, + "connect": 0.43100000000000005, + "send": 0.10299999999999976, + "wait": 17.997999999124556, + "receive": 0.44499999785330147, + "_blocked_queueing": 3.832000002148561 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "394649", + "pageref": "page_7", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=S", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "S" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "131" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:42:25 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 131, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Tartelette Fraise\",\"description\":\"Delicieuse Tartelette aux Fraises fraiches\",\"size\":\"S\",\"price\":2,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 132, + "_transferSize": 389, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:42:25.547Z", + "time": 33.43599999586027, + "timings": { + "blocked": 3.919000000181608, + "dns": 0.006000000000000005, + "ssl": -1, + "connect": 0.8, + "send": 0.12399999999999989, + "wait": 28.34099999971036, + "receive": 0.24599999596830457, + "_blocked_queueing": 2.945000000181608 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_8", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=M", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "M" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "252" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:27 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 252, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Divorces\",\"description\":\"Delicieux Divorces pas calorique du tout\",\"size\":\"M\",\"price\":2.8,\"status\":\"available\"},{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 253, + "_transferSize": 510, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:27.244Z", + "time": 27.83599999248609, + "timings": { + "blocked": 14.010999996269122, + "dns": 0.0040000000000000036, + "ssl": -1, + "connect": 0.2999999999999998, + "send": 0.1200000000000001, + "wait": 12.966999994811601, + "receive": 0.43400000140536577, + "_blocked_queueing": 11.293999996269122 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_9", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Millefeuille", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 663, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "128" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:38 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 128, + "mimeType": "application/json", + "compression": -1, + "text": "{\"name\":\"Millefeuille\",\"description\":\"Delicieux Millefeuille pas calorique du tout\",\"size\":\"L\",\"price\":4.4,\"status\":\"available\"}" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 129, + "_transferSize": 386, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:38.913Z", + "time": 26.883999991696328, + "timings": { + "blocked": 11.270999997854233, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.06899999999999995, + "wait": 15.220000000571833, + "receive": 0.3239999932702631, + "_blocked_queueing": 10.395999997854233 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_10", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Eclair+Cafe", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 662, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "129" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:48 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 129, + "mimeType": "application/json", + "compression": -1, + "text": "{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"available\"}" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 130, + "_transferSize": 387, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:48.168Z", + "time": 22.693000006256625, + "timings": { + "blocked": 8.27700000374578, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.09899999999999998, + "wait": 14.092000006375834, + "receive": 0.22499999613501132, + "_blocked_queueing": 7.297000003745779 + } + }, + { + "time": 48.992874145507812, + "_isHTTPS": false, + "_webSocketMessages": null, + "_remoteDeviceIP": null, + "timings": { + "connect": 1, + "send": -1, + "dns": -1, + "ssl": -1, + "wait": -1, + "blocked": -1, + "receive": -1 + }, + "_serverAddress": "127.0.0.1", + "_isIntercepted": true, + "_id": "3764", + "serverIPAddress": "127.0.0.1", + "_name": "3764", + "_clientAddress": "127.0.0.1", + "_clientBundlePath": "\/Applications\/Postman.app", + "request": { + "method": "PATCH", + "bodySize": 13, + "headersSize": 296, + "postData": { + "params": [ + { + "name": "{\"price\":2.6}", + "value": "" + } + ], + "text": "{\"price\":2.6}", + "mimeType": "application\/json" + }, + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application\/json" + }, + { + "name": "User-Agent", + "value": "PostmanRuntime\/7.32.3" + }, + { + "name": "Accept", + "value": "*\/*" + }, + { + "name": "Postman-Token", + "value": "ecd3b816-8156-4409-8f94-56413aeab446" + }, + { + "name": "Host", + "value": "localhost.proxyman.io:8080" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "13" + } + ], + "queryString": [], + "httpVersion": "HTTP\/1.1", + "url": "http:\/\/localhost:8080\/rest\/API+Pastries\/0.0.1\/pastries\/Eclair+Cafe" + }, + "_serverPort": 8080, + "_clientName": "Postman", + "_clientPort": 56468, + "response": { + "status": 200, + "content": { + "size": 129, + "mimeType": "application\/json;charset=UTF-8", + "encoding": "base64", + "text": "eyJuYW1lIjoiRWNsYWlyIENhZmUiLCJkZXNjcmlwdGlvbiI6IkRlbGljaWV1eCBFY2xhaXIgYXUgQ2FmZSBwYXMgY2Fsb3JpcXVlIGR1IHRvdXQiLCJzaXplIjoiTSIsInByaWNlIjoyLjYsInN0YXR1cyI6ImF2YWlsYWJsZSJ9" + }, + "bodySize": 129, + "headersSize": 260, + "cookies": [], + "statusText": "OK", + "headers": [ + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + }, + { + "name": "Content-Type", + "value": "application\/json;charset=UTF-8" + }, + { + "name": "Content-Length", + "value": "129" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 09:05:53 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "httpVersion": "HTTP\/1.1", + "redirectURL": "" + }, + "comment": "", + "startedDateTime": "2023-08-17T11:05:53.434+02:00", + "cache": {} + } + ] + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastries-0.0.1.har b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastries-0.0.1.har new file mode 100644 index 000000000..cf7e0e91b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastries-0.0.1.har @@ -0,0 +1,929 @@ +{ + "log": { + "version": "1.2", + "comment": "microcksId: API Pastries:0.0.1", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [ + { + "startedDateTime": "2023-08-16T13:03:11.877Z", + "id": "page_6", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=L", + "pageTimings": { + "onContentLoad": 39.11599999992177, + "onLoad": 38.55900000780821 + } + }, + { + "startedDateTime": "2023-08-17T08:42:25.550Z", + "id": "page_7", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=S", + "pageTimings": { + "onContentLoad": 47.222999986843206, + "onLoad": 47.03399998834357 + } + }, + { + "startedDateTime": "2023-08-17T08:43:27.255Z", + "id": "page_8", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=M", + "pageTimings": { + "onContentLoad": 32.441999996080995, + "onLoad": 32.24200000113342 + } + }, + { + "startedDateTime": "2023-08-17T08:43:38.923Z", + "id": "page_9", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Millefeuille", + "pageTimings": { + "onContentLoad": 32.896999997319654, + "onLoad": 32.69799999543466 + } + }, + { + "startedDateTime": "2023-08-17T08:43:48.176Z", + "id": "page_10", + "title": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Eclair+Cafe", + "pageTimings": { + "onContentLoad": 30.78599998843856, + "onLoad": 30.63899998960551 + } + } + ], + "entries": [ + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "275560", + "pageref": "page_6", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=L", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "L" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "256" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Wed, 16 Aug 2023 13:03:11 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 256, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Baba Rhum\",\"description\":\"Delicieux Baba au Rhum pas calorique du tout\",\"size\":\"L\",\"price\":3.2,\"status\":\"available\"},{\"name\":\"Millefeuille\",\"description\":\"Delicieux Millefeuille pas calorique du tout\",\"size\":\"L\",\"price\":4.4,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 257, + "_transferSize": 514, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T13:03:11.873Z", + "time": 25.34499999912642, + "timings": { + "blocked": 6.360000002148562, + "dns": 0.008000000000000007, + "ssl": -1, + "connect": 0.43100000000000005, + "send": 0.10299999999999976, + "wait": 17.997999999124556, + "receive": 0.44499999785330147, + "_blocked_queueing": 3.832000002148561 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "394649", + "pageref": "page_7", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=S", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "S" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "131" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:42:25 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 131, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Tartelette Fraise\",\"description\":\"Delicieuse Tartelette aux Fraises fraiches\",\"size\":\"S\",\"price\":2,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 132, + "_transferSize": 389, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:42:25.547Z", + "time": 33.43599999586027, + "timings": { + "blocked": 3.919000000181608, + "dns": 0.006000000000000005, + "ssl": -1, + "connect": 0.8, + "send": 0.12399999999999989, + "wait": 28.34099999971036, + "receive": 0.24599999596830457, + "_blocked_queueing": 2.945000000181608 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_8", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries?size=M", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "size", + "value": "M" + } + ], + "cookies": [], + "headersSize": 657, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "252" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:27 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 252, + "mimeType": "application/json", + "compression": -1, + "text": "[{\"name\":\"Divorces\",\"description\":\"Delicieux Divorces pas calorique du tout\",\"size\":\"M\",\"price\":2.8,\"status\":\"available\"},{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"available\"}]" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 253, + "_transferSize": 510, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:27.244Z", + "time": 27.83599999248609, + "timings": { + "blocked": 14.010999996269122, + "dns": 0.0040000000000000036, + "ssl": -1, + "connect": 0.2999999999999998, + "send": 0.1200000000000001, + "wait": 12.966999994811601, + "receive": 0.43400000140536577, + "_blocked_queueing": 11.293999996269122 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_9", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Millefeuille", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 663, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "128" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:38 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 128, + "mimeType": "application/json", + "compression": -1, + "text": "{\"name\":\"Millefeuille\",\"description\":\"Delicieux Millefeuille pas calorique du tout\",\"size\":\"L\",\"price\":4.4,\"status\":\"available\"}" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 129, + "_transferSize": 386, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:38.913Z", + "time": 26.883999991696328, + "timings": { + "blocked": 11.270999997854233, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.06899999999999995, + "wait": 15.220000000571833, + "receive": 0.3239999932702631, + "_blocked_queueing": 10.395999997854233 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "395028", + "pageref": "page_10", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastries/0.0.1/pastries/Eclair+Cafe", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 662, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "129" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 08:43:48 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 129, + "mimeType": "application/json", + "compression": -1, + "text": "{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"available\"}" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 130, + "_transferSize": 387, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-17T08:43:48.168Z", + "time": 22.693000006256625, + "timings": { + "blocked": 8.27700000374578, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.09899999999999998, + "wait": 14.092000006375834, + "receive": 0.22499999613501132, + "_blocked_queueing": 7.297000003745779 + } + }, + { + "time": 48.992874145507812, + "_isHTTPS": false, + "_webSocketMessages": null, + "_remoteDeviceIP": null, + "timings": { + "connect": 1, + "send": -1, + "dns": -1, + "ssl": -1, + "wait": -1, + "blocked": -1, + "receive": -1 + }, + "_serverAddress": "127.0.0.1", + "_isIntercepted": true, + "_id": "3764", + "serverIPAddress": "127.0.0.1", + "_name": "3764", + "_clientAddress": "127.0.0.1", + "_clientBundlePath": "\/Applications\/Postman.app", + "request": { + "method": "PATCH", + "bodySize": 13, + "headersSize": 296, + "postData": { + "params": [ + { + "name": "{\"price\":2.6}", + "value": "" + } + ], + "text": "{\"price\":2.6}", + "mimeType": "application\/json" + }, + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application\/json" + }, + { + "name": "User-Agent", + "value": "PostmanRuntime\/7.32.3" + }, + { + "name": "Accept", + "value": "*\/*" + }, + { + "name": "Postman-Token", + "value": "ecd3b816-8156-4409-8f94-56413aeab446" + }, + { + "name": "Host", + "value": "localhost.proxyman.io:8080" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "13" + } + ], + "queryString": [], + "httpVersion": "HTTP\/1.1", + "url": "http:\/\/localhost:8080\/rest\/API+Pastries\/0.0.1\/pastries\/Eclair+Cafe" + }, + "_serverPort": 8080, + "_clientName": "Postman", + "_clientPort": 56468, + "response": { + "status": 200, + "content": { + "size": 129, + "mimeType": "application\/json;charset=UTF-8", + "encoding": "base64", + "text": "eyJuYW1lIjoiRWNsYWlyIENhZmUiLCJkZXNjcmlwdGlvbiI6IkRlbGljaWV1eCBFY2xhaXIgYXUgQ2FmZSBwYXMgY2Fsb3JpcXVlIGR1IHRvdXQiLCJzaXplIjoiTSIsInByaWNlIjoyLjYsInN0YXR1cyI6ImF2YWlsYWJsZSJ9" + }, + "bodySize": 129, + "headersSize": 260, + "cookies": [], + "statusText": "OK", + "headers": [ + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + }, + { + "name": "Content-Type", + "value": "application\/json;charset=UTF-8" + }, + { + "name": "Content-Length", + "value": "129" + }, + { + "name": "Date", + "value": "Thu, 17 Aug 2023 09:05:53 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "httpVersion": "HTTP\/1.1", + "redirectURL": "" + }, + "comment": "", + "startedDateTime": "2023-08-17T11:05:53.434+02:00", + "cache": {} + } + ] + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastry-2.0.har b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastry-2.0.har new file mode 100644 index 000000000..1e53a149a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/api-pastry-2.0.har @@ -0,0 +1,322 @@ +{ + "log": { + "version": "1.2", + "comment": "microcksId: API Pastry - 2.0:2.0.0", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [ + { + "startedDateTime": "2023-08-16T10:38:35.589Z", + "id": "page_1", + "title": "http://localhost:8080/rest/API+Pastry+-+2.0/2.0.0/pastry/Millefeuille", + "pageTimings": { + "onContentLoad": 61.84999999823049, + "onLoad": 59.80699999781791 + } + }, + { + "startedDateTime": "2023-08-16T10:38:45.456Z", + "id": "page_2", + "title": "http://localhost:8080/rest/API+Pastry+-+2.0/2.0.0/pastry/Eclair+Cafe", + "pageTimings": { + "onContentLoad": 31.186000007437542, + "onLoad": 31.021999995573424 + } + } + ], + "entries": [ + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "231605", + "pageref": "page_1", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastry+-+2.0/2.0.0/pastry/Millefeuille", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 665, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "128" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Wed, 16 Aug 2023 10:38:35 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 128, + "mimeType": "application/json", + "compression": -1, + "text": "{\"name\":\"Millefeuille\",\"description\":\"Delicieux Millefeuille pas calorique du tout\",\"size\":\"L\",\"price\":4.4,\"status\":\"available\"}" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 129, + "_transferSize": 386, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T10:38:35.580Z", + "time": 52.53700000151992, + "timings": { + "blocked": 12.658999998418615, + "dns": 0.0050000000000003375, + "ssl": -1, + "connect": 0.42700000000000005, + "send": 0.12199999999999989, + "wait": 38.886000000341795, + "receive": 0.4380000027595088, + "_blocked_queueing": 9.451999998418614 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "connection": "231605", + "pageref": "page_2", + "request": { + "method": "GET", + "url": "http://localhost:8080/rest/API+Pastry+-+2.0/2.0.0/pastry/Eclair+Cafe", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:8080" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 664, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "129" + }, + { + "name": "Content-Type", + "value": "application/json;charset=UTF-8" + }, + { + "name": "Date", + "value": "Wed, 16 Aug 2023 10:38:45 GMT" + }, + { + "name": "Keep-Alive", + "value": "timeout=60" + }, + { + "name": "Vary", + "value": "Origin" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Method" + }, + { + "name": "Vary", + "value": "Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 129, + "mimeType": "application/json", + "compression": -1, + "text": "{\"name\":\"Eclair Cafe\",\"description\":\"Delicieux Eclair au Cafe pas calorique du tout\",\"size\":\"M\",\"price\":2.5,\"status\":\"available\"}" + }, + "redirectURL": "", + "headersSize": 257, + "bodySize": 130, + "_transferSize": 387, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T10:38:45.450Z", + "time": 22.293999994872138, + "timings": { + "blocked": 7.325999991945922, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.10099999999999998, + "wait": 14.626000001374631, + "receive": 0.2410000015515834, + "_blocked_queueing": 6.251999991945922 + } + } + ] + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/microcks.har b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/microcks.har new file mode 100644 index 000000000..61d641aa1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/microcks.har @@ -0,0 +1,4335 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [], + "entries": [ + { + "_initiator": { + "type": "script", + "stack": { + "callFrames": [ + { + "functionName": "scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 6376, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3414, + "columnNumber": 25 + }, + { + "functionName": "onScheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3305, + "columnNumber": 28 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3408, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3242, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleMacroTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3265, + "columnNumber": 24 + }, + { + "functionName": "scheduleMacroTaskWithCurrentZone", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4198, + "columnNumber": 24 + }, + { + "functionName": "", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 6409, + "columnNumber": 27 + }, + { + "functionName": "proto.", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4522, + "columnNumber": 23 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 14521, + "columnNumber": 16 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175575, + "columnNumber": 89 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._tryNext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175569, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175552, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 172194, + "columnNumber": 19 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175530, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/filter.js.FilterOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 174717, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177636, + "columnNumber": 114 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177626, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175301, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/BehaviorSubject.js.BehaviorSubject._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169174, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169821, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177604, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../src/app/pages/services/{serviceId}/service-detail.page.ts.ServiceDetailPageComponent.ngOnInit", + "scriptId": "1838", + "url": "http://localhost:4200/main.js", + "lineNumber": 6204, + "columnNumber": 25 + }, + { + "functionName": "checkAndUpdateDirectiveInline", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 65133, + "columnNumber": 18 + }, + { + "functionName": "checkAndUpdateNodeInline", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73531, + "columnNumber": 19 + }, + { + "functionName": "checkAndUpdateNode", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73493, + "columnNumber": 15 + }, + { + "functionName": "debugCheckAndUpdateNode", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74127, + "columnNumber": 37 + }, + { + "functionName": "debugCheckDirectivesFn", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74087, + "columnNumber": 12 + }, + { + "functionName": "eval", + "scriptId": "2001", + "url": "ng:///AppModule/ServiceDetailPageComponent_Host.ngfactory.js", + "lineNumber": 12, + "columnNumber": 8 + }, + { + "functionName": "debugUpdateDirectives", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74079, + "columnNumber": 20 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73475, + "columnNumber": 13 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execEmbeddedViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73679, + "columnNumber": 16 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73476, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callWithDebugContext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74345, + "columnNumber": 24 + }, + { + "functionName": "debugCheckAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74047, + "columnNumber": 11 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ViewRef_.detectChanges", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 64722, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ApplicationRef.tick", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71145, + "columnNumber": 25 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 104 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3395, + "columnNumber": 25 + }, + { + "functionName": "onInvoke", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70292, + "columnNumber": 32 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3394, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.run", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3154, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.NgZone.run", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70206, + "columnNumber": 27 + }, + { + "functionName": "next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 80 + }, + { + "functionName": "schedulerFn", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67771, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.__tryOrUnsub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170154, + "columnNumber": 15 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170092, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170036, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169779, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.EventEmitter.emit", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67755, + "columnNumber": 75 + }, + { + "functionName": "checkStable", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70261, + "columnNumber": 34 + }, + { + "functionName": "onHasTask", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70305, + "columnNumber": 20 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.hasTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3447, + "columnNumber": 36 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate._updateTaskCount", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3467, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone._updateTaskCount", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3295, + "columnNumber": 33 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.runTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3216, + "columnNumber": 29 + }, + { + "functionName": "drainMicroTaskQueue", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3605, + "columnNumber": 34 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3506, + "columnNumber": 20 + }, + { + "functionName": "invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4697, + "columnNumber": 13 + }, + { + "functionName": "globalZoneAwareCallback", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4723, + "columnNumber": 16 + } + ] + } + }, + "_priority": "High", + "_resourceType": "xhr", + "cache": {}, + "connection": "233178", + "request": { + "method": "GET", + "url": "http://localhost:4200/api/services/64c10bc25a36604b5a414077?messages=true", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "application/json, text/plain, */*" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:4200" + }, + { + "name": "Referer", + "value": "http://localhost:4200/" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "messages", + "value": "true" + } + ], + "cookies": [], + "headersSize": 589, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "X-Powered-By", + "value": "Express" + }, + { + "name": "access-control-allow-headers", + "value": "*" + }, + { + "name": "access-control-allow-methods", + "value": "POST, PUT, GET, OPTIONS, DELETE" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "access-control-max-age", + "value": "3600" + }, + { + "name": "connection", + "value": "close" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "date", + "value": "Wed, 16 Aug 2023 10:43:17 GMT" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "vary", + "value": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 6663, + "mimeType": "application/json", + "compression": -13, + "text": "{\"service\":{\"id\":\"64c10bc25a36604b5a414077\",\"name\":\"API Pastry - 2.0\",\"version\":\"2.0.0\",\"type\":\"REST\",\"metadata\":{\"createdOn\":1690373058238,\"lastUpdate\":1690373058238},\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"operations\":[{\"name\":\"GET /pastry\",\"method\":\"GET\",\"resourcePaths\":[\"/pastry\"]},{\"name\":\"GET /pastry/{name}\",\"method\":\"GET\",\"dispatcher\":\"URI_PARTS\",\"dispatcherRules\":\"name\",\"resourcePaths\":[\"/pastry/Eclair%20Cafe\",\"/pastry/Millefeuille\"]},{\"name\":\"PATCH /pastry/{name}\",\"method\":\"PATCH\",\"dispatcher\":\"URI_PARTS\",\"dispatcherRules\":\"name\",\"resourcePaths\":[\"/pastry/Eclair%20Cafe\"]}]},\"messagesMap\":{\"PATCH /pastry/{name}\":[{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Eclair Cafe\",\"content\":\"{\\\"price\\\":2.6}\",\"operationId\":\"64c10bc25a36604b5a414077-PATCH /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"application/json\"]},{\"name\":\"Content-Type\",\"values\":[\"application/json\"]}],\"id\":\"64c10bc25a36604b5a414082\",\"responseId\":\"64c10bc25a36604b5a414081\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Eclair Cafe\"}]},\"response\":{\"name\":\"Eclair Cafe\",\"content\":\"{\\\"name\\\":\\\"Eclair Cafe\\\",\\\"description\\\":\\\"Delicieux Eclair au Cafe pas calorique du tout\\\",\\\"size\\\":\\\"M\\\",\\\"price\\\":2.6,\\\"status\\\":\\\"available\\\"}\",\"operationId\":\"64c10bc25a36604b5a414077-PATCH /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a414081\",\"status\":\"200\",\"mediaType\":\"application/json\",\"dispatchCriteria\":\"/name=Eclair Cafe\",\"fault\":false}},{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Eclair Cafe Xml\",\"content\":\"\\n\\t2.6\\n\",\"operationId\":\"64c10bc25a36604b5a414077-PATCH /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Content-Type\",\"values\":[\"text/xml\"]},{\"name\":\"Accept\",\"values\":[\"text/xml\"]}],\"id\":\"64c10bc25a36604b5a414084\",\"responseId\":\"64c10bc25a36604b5a414083\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Eclair Cafe\"}]},\"response\":{\"name\":\"Eclair Cafe Xml\",\"content\":\"\\n Eclair Cafe\\n Delicieux Eclair au Cafe pas calorique du tout\\n M\\n 2.6\\n available\\n\",\"operationId\":\"64c10bc25a36604b5a414077-PATCH /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a414083\",\"status\":\"200\",\"mediaType\":\"text/xml\",\"dispatchCriteria\":\"/name=Eclair Cafe\",\"fault\":false}}],\"GET /pastry/{name}\":[{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Millefeuille\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"application/json\"]}],\"id\":\"64c10bc25a36604b5a41407c\",\"responseId\":\"64c10bc25a36604b5a41407b\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Millefeuille\"}]},\"response\":{\"name\":\"Millefeuille\",\"content\":\"{\\\"name\\\":\\\"Millefeuille\\\",\\\"description\\\":\\\"Delicieux Millefeuille pas calorique du tout\\\",\\\"size\\\":\\\"L\\\",\\\"price\\\":4.4,\\\"status\\\":\\\"available\\\"}\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a41407b\",\"status\":\"200\",\"mediaType\":\"application/json\",\"dispatchCriteria\":\"/name=Millefeuille\",\"fault\":false}},{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Eclair Cafe\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"application/json\"]}],\"id\":\"64c10bc25a36604b5a41407e\",\"responseId\":\"64c10bc25a36604b5a41407d\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Eclair Cafe\"}]},\"response\":{\"name\":\"Eclair Cafe\",\"content\":\"{\\\"name\\\":\\\"Eclair Cafe\\\",\\\"description\\\":\\\"Delicieux Eclair au Cafe pas calorique du tout\\\",\\\"size\\\":\\\"M\\\",\\\"price\\\":2.5,\\\"status\\\":\\\"available\\\"}\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a41407d\",\"status\":\"200\",\"mediaType\":\"application/json\",\"dispatchCriteria\":\"/name=Eclair Cafe\",\"fault\":false}},{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Eclair Cafe Xml\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"text/xml\"]}],\"id\":\"64c10bc25a36604b5a414080\",\"responseId\":\"64c10bc25a36604b5a41407f\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Eclair Cafe\"}]},\"response\":{\"name\":\"Eclair Cafe Xml\",\"content\":\"\\n Eclair Cafe\\n Delicieux Eclair au Cafe pas calorique du tout\\n M\\n 2.5\\n available\\n\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a41407f\",\"status\":\"200\",\"mediaType\":\"text/xml\",\"dispatchCriteria\":\"/name=Eclair Cafe\",\"fault\":false}}],\"GET /pastry\":[{\"type\":\"reqRespPair\",\"request\":{\"name\":\"pastries_json\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"application/json\"]}],\"id\":\"64c10bc25a36604b5a41407a\",\"responseId\":\"64c10bc25a36604b5a414079\"},\"response\":{\"name\":\"pastries_json\",\"content\":\"[{\\\"name\\\":\\\"Baba Rhum\\\",\\\"description\\\":\\\"Delicieux Baba au Rhum pas calorique du tout\\\",\\\"size\\\":\\\"L\\\",\\\"price\\\":3.2,\\\"status\\\":\\\"available\\\"},{\\\"name\\\":\\\"Divorces\\\",\\\"description\\\":\\\"Delicieux Divorces pas calorique du tout\\\",\\\"size\\\":\\\"M\\\",\\\"price\\\":2.8,\\\"status\\\":\\\"available\\\"},{\\\"name\\\":\\\"Tartelette Fraise\\\",\\\"description\\\":\\\"Delicieuse Tartelette aux Fraises fraiches\\\",\\\"size\\\":\\\"S\\\",\\\"price\\\":2,\\\"status\\\":\\\"available\\\"}]\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a414079\",\"status\":\"200\",\"mediaType\":\"application/json\",\"fault\":false}}]}}" + }, + "redirectURL": "", + "headersSize": 393, + "bodySize": 6676, + "_transferSize": 7069, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T10:43:17.603Z", + "time": 73.77899999711289, + "timings": { + "blocked": 24.260999999686145, + "dns": 0.0039999999999995595, + "ssl": -1, + "connect": 0.24299999999999988, + "send": 0.16999999999999993, + "wait": 48.423000000730156, + "receive": 0.6779999966965988, + "_blocked_queueing": 21.512999999686144 + } + }, + { + "_initiator": { + "type": "script", + "stack": { + "callFrames": [ + { + "functionName": "scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 6376, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3414, + "columnNumber": 25 + }, + { + "functionName": "onScheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3305, + "columnNumber": 28 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3408, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3242, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleMacroTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3265, + "columnNumber": 24 + }, + { + "functionName": "scheduleMacroTaskWithCurrentZone", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4198, + "columnNumber": 24 + }, + { + "functionName": "", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 6409, + "columnNumber": 27 + }, + { + "functionName": "proto.", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4522, + "columnNumber": 23 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 14521, + "columnNumber": 16 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175575, + "columnNumber": 89 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._tryNext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175569, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175552, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 172194, + "columnNumber": 19 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175530, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/filter.js.FilterOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 174717, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177636, + "columnNumber": 114 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177626, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175301, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/BehaviorSubject.js.BehaviorSubject._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169174, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169821, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177604, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.ObservableStrategy.createSubscription", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11354, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11436, + "columnNumber": 44 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe.transform", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11417, + "columnNumber": 21 + }, + { + "functionName": "eval", + "scriptId": "1953", + "url": "ng:///AppModule/ServiceDetailPageComponent.ngfactory.js", + "lineNumber": 1845, + "columnNumber": 72 + }, + { + "functionName": "debugUpdateDirectives", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74079, + "columnNumber": 20 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73475, + "columnNumber": 13 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execEmbeddedViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73679, + "columnNumber": 16 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73476, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callWithDebugContext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74345, + "columnNumber": 24 + }, + { + "functionName": "debugCheckAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74047, + "columnNumber": 11 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ViewRef_.detectChanges", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 64722, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ApplicationRef.tick", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71145, + "columnNumber": 25 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 104 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3395, + "columnNumber": 25 + }, + { + "functionName": "onInvoke", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70292, + "columnNumber": 32 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3394, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.run", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3154, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.NgZone.run", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70206, + "columnNumber": 27 + }, + { + "functionName": "next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 80 + }, + { + "functionName": "schedulerFn", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67771, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.__tryOrUnsub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170154, + "columnNumber": 15 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170092, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170036, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169779, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.EventEmitter.emit", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67755, + "columnNumber": 75 + }, + { + "functionName": "checkStable", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70261, + "columnNumber": 34 + }, + { + "functionName": "onHasTask", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70305, + "columnNumber": 20 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.hasTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3447, + "columnNumber": 36 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate._updateTaskCount", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3467, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone._updateTaskCount", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3295, + "columnNumber": 33 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.runTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3216, + "columnNumber": 29 + }, + { + "functionName": "drainMicroTaskQueue", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3605, + "columnNumber": 34 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3506, + "columnNumber": 20 + }, + { + "functionName": "invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4697, + "columnNumber": 13 + }, + { + "functionName": "globalZoneAwareCallback", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4723, + "columnNumber": 16 + } + ] + } + }, + "_priority": "High", + "_resourceType": "xhr", + "cache": {}, + "connection": "233186", + "request": { + "method": "GET", + "url": "http://localhost:4200/api/services/64c10bc25a36604b5a414077?messages=true", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "application/json, text/plain, */*" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:4200" + }, + { + "name": "Referer", + "value": "http://localhost:4200/" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [ + { + "name": "messages", + "value": "true" + } + ], + "cookies": [], + "headersSize": 589, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "X-Powered-By", + "value": "Express" + }, + { + "name": "access-control-allow-headers", + "value": "*" + }, + { + "name": "access-control-allow-methods", + "value": "POST, PUT, GET, OPTIONS, DELETE" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "access-control-max-age", + "value": "3600" + }, + { + "name": "connection", + "value": "close" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "date", + "value": "Wed, 16 Aug 2023 10:43:17 GMT" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "vary", + "value": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 6663, + "mimeType": "application/json", + "compression": -13, + "text": "{\"service\":{\"id\":\"64c10bc25a36604b5a414077\",\"name\":\"API Pastry - 2.0\",\"version\":\"2.0.0\",\"type\":\"REST\",\"metadata\":{\"createdOn\":1690373058238,\"lastUpdate\":1690373058238},\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"operations\":[{\"name\":\"GET /pastry\",\"method\":\"GET\",\"resourcePaths\":[\"/pastry\"]},{\"name\":\"GET /pastry/{name}\",\"method\":\"GET\",\"dispatcher\":\"URI_PARTS\",\"dispatcherRules\":\"name\",\"resourcePaths\":[\"/pastry/Eclair%20Cafe\",\"/pastry/Millefeuille\"]},{\"name\":\"PATCH /pastry/{name}\",\"method\":\"PATCH\",\"dispatcher\":\"URI_PARTS\",\"dispatcherRules\":\"name\",\"resourcePaths\":[\"/pastry/Eclair%20Cafe\"]}]},\"messagesMap\":{\"PATCH /pastry/{name}\":[{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Eclair Cafe\",\"content\":\"{\\\"price\\\":2.6}\",\"operationId\":\"64c10bc25a36604b5a414077-PATCH /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"application/json\"]},{\"name\":\"Content-Type\",\"values\":[\"application/json\"]}],\"id\":\"64c10bc25a36604b5a414082\",\"responseId\":\"64c10bc25a36604b5a414081\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Eclair Cafe\"}]},\"response\":{\"name\":\"Eclair Cafe\",\"content\":\"{\\\"name\\\":\\\"Eclair Cafe\\\",\\\"description\\\":\\\"Delicieux Eclair au Cafe pas calorique du tout\\\",\\\"size\\\":\\\"M\\\",\\\"price\\\":2.6,\\\"status\\\":\\\"available\\\"}\",\"operationId\":\"64c10bc25a36604b5a414077-PATCH /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a414081\",\"status\":\"200\",\"mediaType\":\"application/json\",\"dispatchCriteria\":\"/name=Eclair Cafe\",\"fault\":false}},{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Eclair Cafe Xml\",\"content\":\"\\n\\t2.6\\n\",\"operationId\":\"64c10bc25a36604b5a414077-PATCH /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Content-Type\",\"values\":[\"text/xml\"]},{\"name\":\"Accept\",\"values\":[\"text/xml\"]}],\"id\":\"64c10bc25a36604b5a414084\",\"responseId\":\"64c10bc25a36604b5a414083\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Eclair Cafe\"}]},\"response\":{\"name\":\"Eclair Cafe Xml\",\"content\":\"\\n Eclair Cafe\\n Delicieux Eclair au Cafe pas calorique du tout\\n M\\n 2.6\\n available\\n\",\"operationId\":\"64c10bc25a36604b5a414077-PATCH /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a414083\",\"status\":\"200\",\"mediaType\":\"text/xml\",\"dispatchCriteria\":\"/name=Eclair Cafe\",\"fault\":false}}],\"GET /pastry/{name}\":[{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Millefeuille\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"application/json\"]}],\"id\":\"64c10bc25a36604b5a41407c\",\"responseId\":\"64c10bc25a36604b5a41407b\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Millefeuille\"}]},\"response\":{\"name\":\"Millefeuille\",\"content\":\"{\\\"name\\\":\\\"Millefeuille\\\",\\\"description\\\":\\\"Delicieux Millefeuille pas calorique du tout\\\",\\\"size\\\":\\\"L\\\",\\\"price\\\":4.4,\\\"status\\\":\\\"available\\\"}\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a41407b\",\"status\":\"200\",\"mediaType\":\"application/json\",\"dispatchCriteria\":\"/name=Millefeuille\",\"fault\":false}},{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Eclair Cafe\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"application/json\"]}],\"id\":\"64c10bc25a36604b5a41407e\",\"responseId\":\"64c10bc25a36604b5a41407d\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Eclair Cafe\"}]},\"response\":{\"name\":\"Eclair Cafe\",\"content\":\"{\\\"name\\\":\\\"Eclair Cafe\\\",\\\"description\\\":\\\"Delicieux Eclair au Cafe pas calorique du tout\\\",\\\"size\\\":\\\"M\\\",\\\"price\\\":2.5,\\\"status\\\":\\\"available\\\"}\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a41407d\",\"status\":\"200\",\"mediaType\":\"application/json\",\"dispatchCriteria\":\"/name=Eclair Cafe\",\"fault\":false}},{\"type\":\"reqRespPair\",\"request\":{\"name\":\"Eclair Cafe Xml\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"text/xml\"]}],\"id\":\"64c10bc25a36604b5a414080\",\"responseId\":\"64c10bc25a36604b5a41407f\",\"queryParameters\":[{\"name\":\"name\",\"value\":\"Eclair Cafe\"}]},\"response\":{\"name\":\"Eclair Cafe Xml\",\"content\":\"\\n Eclair Cafe\\n Delicieux Eclair au Cafe pas calorique du tout\\n M\\n 2.5\\n available\\n\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry/{name}\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a41407f\",\"status\":\"200\",\"mediaType\":\"text/xml\",\"dispatchCriteria\":\"/name=Eclair Cafe\",\"fault\":false}}],\"GET /pastry\":[{\"type\":\"reqRespPair\",\"request\":{\"name\":\"pastries_json\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"headers\":[{\"name\":\"Accept\",\"values\":[\"application/json\"]}],\"id\":\"64c10bc25a36604b5a41407a\",\"responseId\":\"64c10bc25a36604b5a414079\"},\"response\":{\"name\":\"pastries_json\",\"content\":\"[{\\\"name\\\":\\\"Baba Rhum\\\",\\\"description\\\":\\\"Delicieux Baba au Rhum pas calorique du tout\\\",\\\"size\\\":\\\"L\\\",\\\"price\\\":3.2,\\\"status\\\":\\\"available\\\"},{\\\"name\\\":\\\"Divorces\\\",\\\"description\\\":\\\"Delicieux Divorces pas calorique du tout\\\",\\\"size\\\":\\\"M\\\",\\\"price\\\":2.8,\\\"status\\\":\\\"available\\\"},{\\\"name\\\":\\\"Tartelette Fraise\\\",\\\"description\\\":\\\"Delicieuse Tartelette aux Fraises fraiches\\\",\\\"size\\\":\\\"S\\\",\\\"price\\\":2,\\\"status\\\":\\\"available\\\"}]\",\"operationId\":\"64c10bc25a36604b5a414077-GET /pastry\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\",\"id\":\"64c10bc25a36604b5a414079\",\"status\":\"200\",\"mediaType\":\"application/json\",\"fault\":false}}]}}" + }, + "redirectURL": "", + "headersSize": 393, + "bodySize": 6676, + "_transferSize": 7069, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T10:43:17.604Z", + "time": 90.35399999993388, + "timings": { + "blocked": 72.30200000343285, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.04700000000000415, + "wait": 17.555999997293576, + "receive": 0.4489999992074445, + "_blocked_queueing": 23.136000003432855 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "High", + "_resourceType": "other", + "cache": {}, + "connection": "233186", + "request": { + "method": "GET", + "url": "http://localhost:4200/assets/favicon.ico", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:4200" + }, + { + "name": "If-None-Match", + "value": "W/\"3aee-pO9uV1CVpAzMSmU9llwegB/abqk\"" + }, + { + "name": "Referer", + "value": "http://localhost:4200/" + }, + { + "name": "Sec-Fetch-Dest", + "value": "image" + }, + { + "name": "Sec-Fetch-Mode", + "value": "no-cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 643, + "bodySize": 0 + }, + "response": { + "status": 304, + "statusText": "Not Modified", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept-Ranges", + "value": "bytes" + }, + { + "name": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Date", + "value": "Wed, 16 Aug 2023 10:43:17 GMT" + }, + { + "name": "ETag", + "value": "W/\"3aee-pO9uV1CVpAzMSmU9llwegB/abqk\"" + }, + { + "name": "Keep-Alive", + "value": "timeout=5" + }, + { + "name": "X-Powered-By", + "value": "Express" + } + ], + "cookies": [], + "content": { + "size": 15086, + "mimeType": "image/vnd.microsoft.icon", + "text": "\u0000\u0000\u0001\u0000\u0003\u000000\u0000\u0000\u0001\u0000 \u0000�%\u0000\u00006\u0000\u0000\u0000 \u0000\u0000\u0001\u0000 \u0000�\u0010\u0000\u0000�%\u0000\u0000\u0010\u0010\u0000\u0000\u0001\u0000 \u0000h\u0004\u0000\u0000�6\u0000\u0000(\u0000\u0000\u00000\u0000\u0000\u0000`\u0000\u0000\u0000\u0001\u0000 \u0000\u0000\u0000\u0000\u0000\u0000$\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0001�Ѓ\u0017�Ѓ=�ЃK�Ѓ4�Ѓ\u000b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Û\u0005�)?�!}�!Z�!\u000f\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0003�Ѓ+�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Є��З9���\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\t�n��&��!��!��!��!\"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0006�Ѓ9�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Џ��ϸJ\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�ɷS�L��\u001f��!��!��!��!��!7\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\f�ЃI�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��ф��Ѓ��Ѓ��Ѓ��Ѓ��І��Ϯ�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\u0003�����1�� ��!��!��!��!��!��!P�!\u0002\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0013�ЃZ�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Є���{��S��n��Љ��Ѓ��Ѓ��Є��Ϫ�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\u0011ݼ���'��!��!��!��!��!��!��!��!m�\u001f\u0007���\u0002�Ѓ\u0003\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0001�Ѓ\u001d�Ѓm�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Є��S���\u001d��P��Φ��Ђ��Ѓ��Є��ϭ�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\u001aݼ���&��!��!��!��!��!��!��!�� ���C��Ѝ��Ѓ��Ѓ\u001f\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0003�Ѓ)�Ѓ�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��ς��A���\u001b��c��Ϻ��Ђ��Ѓ��Ї��ϵ�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\u0016ܾ���(��!��!��!��!��!��!��!��!��w��Ѝ��Ѓ��Ѓ��Ѓ\u0005\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0006�Ѓ6�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Є��c��T��Ħ��ћ��Ђ��Ђ��Б�����\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\n�Ī��4�� ��!��!��!��!�� ��\u001f��\"�ຆ��А��Ђ��Ѓ��ф&\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u000b�ЃF�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��ϊ��Ѝ��Ѓ��Ѓ��Ѓ��ϧ����]\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�̿��N��\u001f��!��!��!��&��_��P��!��f��Ϣ��Ѓ��΀��V`\u0000\u0000\u0000\u0000�ф\u0012�ЃW�Ѓ��Ѓ��Ѓ��Ѓ��ф��х��ф��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ж��������\u000e\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���N�{�� ��!��!��!��#{���\u0017�ʻ��V��-�߽���̕��b��'��O��ф��Ѓ��Ѓ��Ѓ��Ѓ��ς���h��T���p��Ѕ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Е��ϻ����'\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\u0014�ŭ��9��\u001f��!��!��!e\u0000\u0000\u0000\u0000���+ݽ���%��.��D��(��\u001f��S��ф��Ѓ��Ѓ��Ѓ��Ѓ��Z��&��\u001f��5��ƌ��х��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ѓ��Ћ��Т��Ͼ����$\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���`�{��\"��!��!��!���\u0000\u0004���\f�����-�� ��\u001f��!�� ��R��ф��Ѓ��Ѓ��Є���v��.�� ��!��\"�ἇ��ј��І��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Љ��Л��Ф��϶����P���\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\f�ʺ��V��\u001f��!��!��!h�{K�j��$��!��!��!�� ��R��ф��Ѓ��Ѓ��х���h��#��!��!��'�ܾ�����U�Џ]�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Џ��������\u0015���\u0005\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���%�Ŭ��I��\u001f��!��!��.��'��!��!��!��!�� ��Q��ф��Ѓ��Ѓ��ф���n��&��\u001f���\u001e��O��ɷ����\u0003\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ў��Ͻ�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�ѵG�Ǥ��S��!��!�� ��!��!��!��!��!�� ��Q��ф��Ѓ��Ѓ��Ѓ��΀��P��3��V��¢����$\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ}�Ѓ��Ѓ��Ѓ��Ѓ��Ђ��В���ƒ\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0002�Ѓ$�Ѓy�Ђ��ϕ��_�� ��!��!��!��!��!��!��!�� ��P��ф��Ѓ��Ѓ��Ѓ��Ѓ��ρ�����ͧ����1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u000b�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Н����^\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u001b�Ѓ��Ѓ��Ѓ��Ѕ��Ȕ��4�� ��!��!��!��!��!��!��!�� ��O��ф��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ў��ϧ_��*\u0001�Ѓ\u0010�Ѓ{�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Є��Ϯ����,\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�\u001d#�7\\��|��Ѓ��Ѓ��Ѓ��щ��‘��(��!��!��!��!��!��!��!��!��\u001f��O��ф��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѕ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Б��������\u0006\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�!A�!��#��l��ш��Ѓ��Ѓ��ы������'��!��!��!��!��!��!��!��!��\u001f��N��ф��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Є��ϫ����H\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�!\u0014�!��!�� ��w��ю��Ї��Є��щ��Ě��-�� ��!��!��!��!��!��!��!��\u001f��N��ф��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��И���Û���\u0007\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�\u001f:� ���\u001e��:��ǟ��Ь��ϲ��Њ��Ѕ��ʣ��<��\u001f��!��!��!��!��!��!��!��\u001f��M��ф��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ў��Ϲ����$\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�SI�7��C�࿏��м����Y\u0000\u0000\u0000\u0000�ЃL�Ђ��ϣ��\\���\u001e��!��!��!��!��!��!��!��\u001f��M��ф��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ѝ��д����@\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ђ��ˁ��˒��Л��Ͼ����\u0006\u0000\u0000\u0000\u0000�Ѓt�Ђ��ѕ�້��$��!��!��!��!��!��!��!��\u001f��L��ф��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��А��ɣ��ŬU\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ђ��Ѝ��Ϲ�\u0000\u0000\u0000\u0000�Ѓ8�Ѓ��Ѓ��Ї��˦��D��\u001f��!��!��!��!��!��!��\u001f��K��Є��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ђ��Ѓ��Љ��Κ�ྌ��B��\u001e��\u001f\u0012\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Є��Ѝ��Є��Ѓ��Ѓ��Ѓ��Ђ��К�່��%�� ��!��!��!��!��!��\u001f��K��Є��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��φ��͋��͍��ʓ�⿉��]��+��\u001f��!��!��!\t\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѕ��Φ��d��!��!��!��!��!��!��\u001f��J��Є��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Њ����H��=��(��\u001f�� ��!��!��!��!e\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Њ��˥��Q��\u001f��!��!��!��!��\u001f��J��Є��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Є��ċ��/���\u001e��!��!��!��!��!��!��!��!\u001b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ѓ��Ѓ��Ѓ��Ђ��ї��r��\u001f��!��!��!��!��\u001f��I��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��̖��H��\u001f��!��!��!��!��!��!��!��![\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0016�Ѓ��Ѓ��Ѓ��Ђ��Ѓ��Њ��Ў��Ѓ��Ѓ��Ѓ��Ђ��ѕ��x��\u001f��!��!��!��!��\u001f��I��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Θ��T���\u001e��!��!��!��!��!��!��!��!�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u000e�Ѝe�Д��Й��У��ϳ��Д��Ѓ��Ѓ��Ѓ��Ђ��ђ�Ẅ��\"��!��!��!��!��\u001f��H��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Λ��R���\u001e��!��!��!��!��!��!��!��!��!\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\f���\u001c���\u0015�Џ.�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��э������(��!��!��!��!��\u001f��H��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��˝��E���\u001e��-��C�� ��!��!��!��!��!�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�ЃP�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��І��ɣ��:��\u001f��!��!��!��\u001e��F��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ї��ř��1��\u001f��,�ݼ���O��!��\u001f��!��!��!-\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ_�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ϥ��X���\u001e��!��#��\"��4��?��\\��Ђ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��В�Ṃ��\"��!�� �ݼ�>�ȳ��q��E��/�� 9\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�ЃF�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��ј�ṃ��\"��!��[��|��u��I��+���w��Є��Ѓ��Ѓ��Ѓ��Ѓ��͠��S���\u001e��!��!�\u0000\u0000\u0000\u0000���\t���-�ȴ0ۿ�\u000b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0010�Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Ђ��Љ��Ȣ��7��\"���k��Ϩ�ݽ���u��4���v��Є��Ѓ��Ѓ��Ђ��Ў��˴��i��D��+o��\u001d\u000e\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ#�І��Љ��Љ��Ѝ��Е��Ї��Т��l��\u001f��K��ɏ��V��o���}��ρ��Ѓ��Ѓ��Ѓ��Є��ϩ����[��� ���\u0015���\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�С\b�Ϩ9�Ϯg�ϸm�ϺY�І��Ѝ��ş��4��#��4��\"��C��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Й���Ú���\u0007\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0002�Ѓ��Ђ��С��v��!�� ��\u001f��D��Ѓ��Ѓ��Ѓ��Ђ��Ϗ��Ϻ����!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ_�Ѓ��Ј��˦��O��\u001f��\u001f��C��Ђ��Ѓ��Ђ��ό��γ����:\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0013�Ѓ��Ђ��Б��ş���?���\u001d��C��Ђ��Ђ��Ϗ��ɤ��ƯN\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ/�Ѓ��Ђ��Е��Ě��E��B��σ��Ζ��Ù��ns��\u0001\u0004\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Є\u001c�Љ��Ќ��О��ɬ�⿊��Ω��Ȯ��gF�y\u0000\u0004\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\u0002�ϵ\u001c�ϳ?���K���<��� ���\u0007\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000������\u0000\u0000������\u0000\u0000������\u0000\u0000�����\u0003\u0000\u0000�\u001f���\u0001\u0000\u0000�\u000f���\u0000\u0000\u0000�\u0007���\u0000\u0000\u0000�\u0003��\u0000\u0000\u0000\u0000�\u0000?�\u0000\u0000\u0000\u0000�\u0000\u001f�\u0000\u0000\u0000\u0000�\u0000\u001f�\u0000\u0001\u0000\u0000�\u0000\u001e\u0000\u0000\u0001\u0000\u0000�\u0018\u0000\u0000\u0000\u0003\u0000\u0000�\u001c\u0000\u0000\u0000\u0007\u0000\u0000�\f\u0000\u0000\u0000\u001f\u0000\u0000�\f\u0000\u0001�\u0000\u0000�\u0000\u0000\u0001�\u0000\u0000��\u0000\u0003�\u0000\u0000�\u0000\u0000\u0007��\u0000\u0000�\u0000\u0000\u0007��\u0000\u0000�\u0000\u0000\u0000\u0000�\u0000\u0000�\u0000\u0000\u0000\u0001�\u0000\u0000�\u0000\u0000\u0000\u0001�\u0000\u0000�\u0000\u0000\u0000\u0003�\u0000\u0000�\u0000\u0000\u0000\u0007�\u0000\u0000\u0007\u0000\u0000\u0000\u000f�\u0000\u0000\u0006\u0000\u0000\u0000\u0007�\u0000\u0000\u0000\u0000\u0000\u0000\u0003�\u0000\u0000\u0000\u0000\u0000\u0000\u0003�\u0000\u0000\u0000\u0000\u0000\u0000\u0001�\u0000\u0000\u0000\u0000\u0000\u0000\u0001�\u0000\u0000�\u0000\u0000\u0000\u0000�\u0000\u0000�\u0000\u0000\u0000\u0000�\u0000\u0000�\u0000\u0000\u0000\u0000�\u0000\u0000�\u0000\u0000\u0000\u0001�\u0000\u0000�\u0000\u0000\u0000C�\u0000\u0000�\u0000\u0000\u0000�\u0000\u0000�\u0000\u0000\u0001��\u0000\u0000�\u0000\u0000\u000f��\u0000\u0000��\u0000\u000f��\u0000\u0000��\u0000\u001f��\u0000\u0000��\u0000?��\u0000\u0000��\u0000��\u0000\u0000��\u0000���\u0000\u0000��\u0001���\u0000\u0000������\u0000\u0000������\u0000\u0000������\u0000\u0000(\u0000\u0000\u0000 \u0000\u0000\u0000@\u0000\u0000\u0000\u0001\u0000 \u0000\u0000\u0000\u0000\u0000\u0000\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�!\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0013�Ѓ4�Ѓ/�Є\u000b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\u0001�J;�\"��!u�!\u0013\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0001�Ѓ!�Ѓu�Ѓ��Ѓ��Ѓ��І��У.\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�ħ0��B�� ��!��!��!\"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0004�Ѓ.�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Є��Н�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000ݻ�z�0�� ��!��!��!��!6\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\b�Ѓ=�Ѓ��Ѓ��Ѓ��Ѓ�����Q��q��ш��Ђ��М�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000޺���+��!��!��!��!��!��Zn�шg�Ѓ\u0015\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\r�ЃM�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��ф���r��(��q��ѐ��Ђ��С�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000ܽ���/�� ��!��!��\u001f��%��}��ц��Ѓ~\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0015�Ѓ_�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ���|���h��Ȗ��Ѕ��Ї��Ϯ�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ŭm��@��\u001f��!��$��E��0��{��ю���z��<\u0016�х\u001e�Ѓq�Ѓ��Є��Є��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��х��Є��Ђ��Л����W\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���2�e�� ��!��\"����2�q��D��z��E��@��Ѓ��Ѓ��Є���z��P��X��̈́��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��І��Й��Ϻs���\u0005\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���\u0005ۿ���6��\u001f��!��_\u0006ܾ���.��$�� ��D��Ѓ��Ѓ��Ѓ��M��\u001f��)��‡��щ��Ѓ��Ѓ��Ѓ��Ѓ��Ћ��Ж��ϧ��ϿI���\u0004\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���+�y��(�� ��+��Q��%��!��\u001f��C��Ђ��Ѓ�����8��\u001d��2�ܿ���қF�Ѓ��Ѓ��Ѓ��Ђ��Й����8���\u0006\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�ͱW�r��)��!��\"��!��!��\u001f��C��Ђ��Ѓ��Ѓ��U��6��n��˿3\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Ђ��М����\"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u001a�Ѓn�ц��ƒ��,�� ��!��!��!��\u001f��B��Ђ��Ѓ��Ѓ��ρ��˅��ίu\u0000\u0000\u0000\u0000�Ѓ!�Ѓ��Ѓ��Ѓ��Є��Ϧ����\u000f\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�\u0017\u0016�UY�ς��Ѓ��ъ��o�� ��!��!��!��!��\u001f��B��ς��Ѓ��Ѓ��Ѓ��Є��Љ��Є��Ѓ��Ѓ��Ѓ��Ђ��Ѝ��Ϸ�\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�!!�\u001f��9��˅��Ѓ��ь��p�� ��!��!��!��!��\u001f��A��ς��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Т����,\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000� p�\u001e��R��͢��Ж��ъ��~��#��!��!��!��!��\u001f��A��ς��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��Е��ϼn\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�X��Y��ƛ����n�Д\u0014�х��ǐ��2�� ��!��!��!��\u001f��@��ς��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Г��з����\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Є��Ї��К����(�ЁS�Ђ��ϔ��[��\u001f��!��!��!��\u001f��@��ς��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��х��щ��ʏ��l��5e\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��І��І��Ѓ��Ѓ��ш��Î��3�� ��!��!��\u001f��?��ρ��Ѓ��Ѓ��Ѓ��Є��ɇ���z��m��D��\"�� ��!J\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ѓ��Ђ��А��{��(��!��!��\u001f��?��ρ��Ѓ��Ѓ��Ѓ��ф��Ă��4��!��\u001f��!��!��!��!\u0014\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Ђ��Є��Є��Ѓ��Ѓ��ɑ��6�� ��!��\u001f��>��ρ��Ѓ��Ѓ��Ѓ��Ѓ��̌���A��\u001f��!��!��!��!��!M\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ђ\u0012�Їy�Ў��Е��М��Ї��Ѓ��Ѓ��˒��=��\u001f��!��\u001f��>��ρ��Ѓ��Ѓ��Ѓ��Ђ��͏��F��\u001f��!��!��!��!��!p\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ͽ\u0002�ϻ\u0014���\u0016�Њs�Ѓ��Ѓ��Ђ��Δ��O��\u001e��!��\u001f��=��ς��Ѓ��Ѓ��Ѓ��Ѓ��ʏ��;��+��H��\"�� ��!��!O\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ��Ѓ��Ѓ��Ђ��Џ��l��\u001f��$��(��>���n��Є��Ѓ��Ѓ��ц������)��%�޺���b��5��#�\u001f\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓm�Ѓ��Ѓ��Ђ��ч������(��M�⻂��a��E��Ђ��Ѓ��Ђ��Б��r��&��!�ݻ�\u0006��� ݽ�\"�O\u0003\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0015�Є��Ї��Ћ��Ќ��͗��L��E������j���o��Ђ��Ѓ��Ѕ��Ϩ����d�\\>� \u0007\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Е\b�Т6�ϰL�Йk�ь�㽃��+��+��<��ς��Ѓ��Ђ��Й����Y\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u001b�Ѓ��Ε��]��\u001d��;��΀��Ѓ��ϒ��Ϸ����\u0006\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓn�І��ɔ��L��9��΀��͐��ś����\r\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ђ\b�Єd�ю��ɛ��{��̛������W\u0019\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ϥ\u0015�Ѵ5���3���\u001aѼ�\u0003\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000���������������\u0000��\u0000�?�\u0000�\u000f�\u0000�\u0007\u0000\u0001��\u0000\u0003��\u0000\u0007�\u0000\u0004\u001f�\u0000\f\u001f�\u0000\u001c\u001f�\u0000\u0000\u001f�\u0000\u0000?�\u0000\u0000\u0018\u0000\u0000\u0018\u0000\u0000�\u0000\u0000\u0000\u0000\u0000\u0000?\u0000\u0000\u0000?�\u0000\u0000?�\u0000\u0000?�\u0000\u0000�\u0000\u0003��\u0000\u001f���?���?����������������(\u0000\u0000\u0000\u0010\u0000\u0000\u0000 \u0000\u0000\u0000\u0001\u0000 \u0000\u0000\u0000\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�\"\u0006�!\u0004\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�х\u0012�Ѓ*�І\r\u0000\u0000\u0000\u0000�}\u0016�/�� ��!\u0012\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ѓ\u0019�Ѓf�ρ��ς��Ќ�\u0000\u0000\u0000\u0000�uL�.�� ��\"��j@�׍\f\u0000\u0000\u0000\u0000�҆\u0002�ф$�Ѓy�Ѓ��Є��d���x��ѐ�\u0000\u0000\u0000\u0000සH�2��!��5���x���xa�΀/�Ѓ��ρ��Ѓ��Ѓ��Ѓ���y��̅��К�\u0000\u0000\u0000\u0000�Dz\u001a�J��!��c���I��C��΀���o��H��ɀ��Є��Ѓ��Њ��З��ϲ1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�f�<��,��\"��6��΀��c���A����`�Ѓ��Ѕ��Н����\u0006\u0000\u0000\u0000\u0000�\u0014\u000f��nX�Љ���P��\u001f��\u001f��6���~��ρ��̅��Їl�Ђ��Ј��Ϧ\\\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�'��k��Ў���N��\u001f�� ��6���~��Ѓ��Ѓ��Ѓ��х��З��Ͽ\u0017\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000��p��ȕ��Ҏ��i��\"�� ��5���~��Ѓ��Ѓ��υ���~��yU\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�ф��Ѕ��Ѓ��ʅ��B��\u001e��5���}��ф��ʁ���T��0�� ��!\u000e\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Є��Љ��Љ��ч��b��\u001f��4���}��у��ʂ��7��\u001f��!��!9\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�З\u0005�С\u001d�І��х��q��&��9���w��ф���~��9��C��(��\u001f!\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000��|\u0001�Ѓ��Ї��Ȅ���L��c���p��ч��ć��4i�{\u0018�a\u0014\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ќ\t�М*�Ў��g��=���}��Д��и%\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�҄(�ʈ��i��Ȉ��Ƙ=\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000�Ӛ\u0012�ΰ-�ƭ\u0016\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000��\u0000\u0000��\u0000\u0000��\u0000\u0000�\u0000\u0000\u0000�\u0001\u0000\u0000�#\u0000\u0000�'\u0000\u0000\u0000\u0007\u0000\u0000\u0000\u000f\u0000\u0000\u0000\u0007\u0000\u0000\u0000\u0007\u0000\u0000�\u0007\u0000\u0000�?\u0000\u0000�\u0000\u0000��\u0000\u0000��\u0000\u0000" + }, + "redirectURL": "", + "headersSize": 234, + "bodySize": 0, + "_transferSize": 234, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T10:43:17.623Z", + "time": 7.215000004412606, + "timings": { + "blocked": 4.98400000166893, + "dns": 0.0020000000000000018, + "ssl": -1, + "connect": 0.16799999999999993, + "send": 0.052999999999999936, + "wait": 1.8199999973066152, + "receive": 0.1880000054370612, + "_blocked_queueing": 4.06700000166893 + } + }, + { + "_initiator": { + "type": "script", + "stack": { + "callFrames": [ + { + "functionName": "scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 6376, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3414, + "columnNumber": 25 + }, + { + "functionName": "onScheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3305, + "columnNumber": 28 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3408, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3242, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleMacroTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3265, + "columnNumber": 24 + }, + { + "functionName": "scheduleMacroTaskWithCurrentZone", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4198, + "columnNumber": 24 + }, + { + "functionName": "", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 6409, + "columnNumber": 27 + }, + { + "functionName": "proto.", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4522, + "columnNumber": 23 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 14521, + "columnNumber": 16 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175575, + "columnNumber": 89 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._tryNext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175569, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175552, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 172194, + "columnNumber": 19 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175530, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/filter.js.FilterOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 174717, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177636, + "columnNumber": 114 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177626, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175301, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/BehaviorSubject.js.BehaviorSubject._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169174, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169821, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177604, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.ObservableStrategy.createSubscription", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11354, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11436, + "columnNumber": 44 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe.transform", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11417, + "columnNumber": 21 + }, + { + "functionName": "eval", + "scriptId": "1953", + "url": "ng:///AppModule/ServiceDetailPageComponent.ngfactory.js", + "lineNumber": 1786, + "columnNumber": 75 + }, + { + "functionName": "debugUpdateDirectives", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74079, + "columnNumber": 20 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73475, + "columnNumber": 13 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execEmbeddedViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73679, + "columnNumber": 16 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73476, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execEmbeddedViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73679, + "columnNumber": 16 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73476, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callWithDebugContext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74345, + "columnNumber": 24 + }, + { + "functionName": "debugCheckAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74047, + "columnNumber": 11 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ViewRef_.detectChanges", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 64722, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ApplicationRef.tick", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71145, + "columnNumber": 25 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 104 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3395, + "columnNumber": 25 + }, + { + "functionName": "onInvoke", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70292, + "columnNumber": 32 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3394, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.run", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3154, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.NgZone.run", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70206, + "columnNumber": 27 + }, + { + "functionName": "next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 80 + }, + { + "functionName": "schedulerFn", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67771, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.__tryOrUnsub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170154, + "columnNumber": 15 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170092, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170036, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169779, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.EventEmitter.emit", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67755, + "columnNumber": 75 + }, + { + "functionName": "checkStable", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70261, + "columnNumber": 34 + }, + { + "functionName": "onLeave", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70328, + "columnNumber": 4 + }, + { + "functionName": "onInvokeTask", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70286, + "columnNumber": 16 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3426, + "columnNumber": 59 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.runTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3199, + "columnNumber": 46 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3502, + "columnNumber": 33 + }, + { + "functionName": "invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4697, + "columnNumber": 13 + }, + { + "functionName": "globalZoneAwareCallback", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4734, + "columnNumber": 20 + } + ], + "parent": { + "description": "load", + "callFrames": [ + { + "functionName": "customScheduleGlobal", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4836, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3414, + "columnNumber": 25 + }, + { + "functionName": "onScheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3305, + "columnNumber": 28 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3408, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3242, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleEventTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3268, + "columnNumber": 24 + }, + { + "functionName": "", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 5007, + "columnNumber": 32 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 14509, + "columnNumber": 16 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175575, + "columnNumber": 89 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._tryNext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175569, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175552, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 172194, + "columnNumber": 19 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175530, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/filter.js.FilterOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 174717, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177636, + "columnNumber": 114 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177626, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175301, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/BehaviorSubject.js.BehaviorSubject._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169174, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169821, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177604, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.ObservableStrategy.createSubscription", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11354, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11436, + "columnNumber": 44 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe.transform", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11417, + "columnNumber": 21 + }, + { + "functionName": "eval", + "scriptId": "1953", + "url": "ng:///AppModule/ServiceDetailPageComponent.ngfactory.js", + "lineNumber": 1845, + "columnNumber": 72 + }, + { + "functionName": "debugUpdateDirectives", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74079, + "columnNumber": 20 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73475, + "columnNumber": 13 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execEmbeddedViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73679, + "columnNumber": 16 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73476, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callWithDebugContext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74345, + "columnNumber": 24 + }, + { + "functionName": "debugCheckAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74047, + "columnNumber": 11 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ViewRef_.detectChanges", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 64722, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ApplicationRef.tick", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71145, + "columnNumber": 25 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 104 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3395, + "columnNumber": 25 + }, + { + "functionName": "onInvoke", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70292, + "columnNumber": 32 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3394, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.run", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3154, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.NgZone.run", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70206, + "columnNumber": 27 + }, + { + "functionName": "next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 80 + }, + { + "functionName": "schedulerFn", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67771, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.__tryOrUnsub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170154, + "columnNumber": 15 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170092, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170036, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169779, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.EventEmitter.emit", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67755, + "columnNumber": 75 + }, + { + "functionName": "checkStable", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70261, + "columnNumber": 34 + }, + { + "functionName": "onHasTask", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70305, + "columnNumber": 20 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.hasTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3447, + "columnNumber": 36 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate._updateTaskCount", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3467, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone._updateTaskCount", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3295, + "columnNumber": 33 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.runTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3216, + "columnNumber": 29 + }, + { + "functionName": "drainMicroTaskQueue", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3605, + "columnNumber": 34 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3506, + "columnNumber": 20 + }, + { + "functionName": "invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4697, + "columnNumber": 13 + }, + { + "functionName": "globalZoneAwareCallback", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4723, + "columnNumber": 16 + } + ] + } + } + }, + "_priority": "High", + "_resourceType": "xhr", + "cache": {}, + "connection": "233195", + "request": { + "method": "GET", + "url": "http://localhost:4200/api/resources/service/64c10bc25a36604b5a414077", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "application/json, text/plain, */*" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:4200" + }, + { + "name": "Referer", + "value": "http://localhost:4200/" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 584, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "X-Powered-By", + "value": "Express" + }, + { + "name": "access-control-allow-headers", + "value": "*" + }, + { + "name": "access-control-allow-methods", + "value": "POST, PUT, GET, OPTIONS, DELETE" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "access-control-max-age", + "value": "3600" + }, + { + "name": "connection", + "value": "close" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "date", + "value": "Wed, 16 Aug 2023 10:43:17 GMT" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "vary", + "value": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 6531, + "mimeType": "application/json", + "compression": -13, + "text": "[{\"id\":\"64c10bc25a36604b5a414078\",\"name\":\"API Pastry - 2.0-2.0.0.yaml\",\"content\":\"---\\nopenapi: 3.0.2\\ninfo:\\n title: API Pastry - 2.0\\n version: 2.0.0\\n description: API definition of API Pastry sample app\\n contact:\\n name: Laurent Broudoux\\n url: http://github.com/lbroudoux\\n email: laurent.broudoux@gmail.com\\n license:\\n name: MIT License\\n url: https://opensource.org/licenses/MIT\\npaths:\\n /pastry:\\n summary: Global operations on pastries\\n get:\\n tags:\\n - pastry\\n responses:\\n \\\"200\\\":\\n content:\\n application/json:\\n schema:\\n type: array\\n items:\\n $ref: '#/components/schemas/Pastry'\\n examples:\\n pastries_json:\\n value:\\n - name: Baba Rhum\\n description: Delicieux Baba au Rhum pas calorique du tout\\n size: L\\n price: 3.2\\n status: available\\n - name: Divorces\\n description: Delicieux Divorces pas calorique du tout\\n size: M\\n price: 2.8\\n status: available\\n - name: Tartelette Fraise\\n description: Delicieuse Tartelette aux Fraises fraiches\\n size: S\\n price: 2\\n status: available\\n description: Get list of pastries\\n operationId: GetPastries\\n summary: Get list of pastries\\n /pastry/{name}:\\n summary: Specific operation on pastry\\n get:\\n parameters:\\n - examples:\\n Eclair Cafe:\\n value: Eclair Cafe\\n Eclair Cafe Xml:\\n value: Eclair Cafe\\n Millefeuille:\\n value: Millefeuille\\n name: name\\n description: pastry name\\n schema:\\n type: string\\n in: path\\n required: true\\n responses:\\n \\\"200\\\":\\n content:\\n application/json:\\n schema:\\n $ref: '#/components/schemas/Pastry'\\n examples:\\n Eclair Cafe:\\n value:\\n name: Eclair Cafe\\n description: Delicieux Eclair au Cafe pas calorique du tout\\n size: M\\n price: 2.5\\n status: available\\n Millefeuille:\\n value:\\n name: Millefeuille\\n description: Delicieux Millefeuille pas calorique du tout\\n size: L\\n price: 4.4\\n status: available\\n text/xml:\\n schema:\\n $ref: '#/components/schemas/Pastry'\\n examples:\\n Eclair Cafe Xml:\\n value: |-\\n \\n Eclair Cafe\\n Delicieux Eclair au Cafe pas calorique du tout\\n M\\n 2.5\\n available\\n \\n description: Pastry with specified name\\n operationId: GetPastryByName\\n summary: Get Pastry by name\\n description: Get Pastry by name\\n patch:\\n requestBody:\\n content:\\n application/json:\\n schema:\\n $ref: '#/components/schemas/Pastry'\\n examples:\\n Eclair Cafe:\\n value:\\n price: 2.6\\n text/xml:\\n schema:\\n $ref: '#/components/schemas/Pastry'\\n examples:\\n Eclair Cafe Xml:\\n value: \\\"\\\\n\\\\t2.6\\\\n\\\"\\n required: true\\n parameters:\\n - examples:\\n Eclair Cafe:\\n value: Eclair Cafe\\n Eclair Cafe Xml:\\n value: Eclair Cafe\\n name: name\\n description: pastry name\\n schema:\\n type: string\\n in: path\\n required: true\\n responses:\\n \\\"200\\\":\\n content:\\n application/json:\\n schema:\\n $ref: '#/components/schemas/Pastry'\\n examples:\\n Eclair Cafe:\\n value:\\n name: Eclair Cafe\\n description: Delicieux Eclair au Cafe pas calorique du tout\\n size: M\\n price: 2.6\\n status: available\\n text/xml:\\n schema:\\n $ref: '#/components/schemas/Pastry'\\n examples:\\n Eclair Cafe Xml:\\n value: |-\\n \\n Eclair Cafe\\n Delicieux Eclair au Cafe pas calorique du tout\\n M\\n 2.6\\n available\\n \\n description: Changed pastry\\n operationId: PatchPastry\\n summary: Patch existing pastry\\n parameters:\\n - name: name\\n description: pastry name\\n schema:\\n type: string\\n in: path\\n required: true\\ncomponents:\\n schemas:\\n Pastry:\\n title: Root Type for Pastry\\n description: The root of the Pastry type's schema.\\n type: object\\n properties:\\n name:\\n description: Name of this pastry\\n type: string\\n description:\\n description: A short description of this pastry\\n type: string\\n size:\\n description: Size of pastry (S, M, L)\\n type: string\\n price:\\n format: double\\n description: Price (in USD) of this pastry\\n type: number\\n status:\\n description: Status in stock (available, out_of_stock)\\n type: string\\n example:\\n name: My Pastry\\n description: A short description os my pastry\\n size: M\\n price: 4.5\\n status: available\\ntags:\\n- name: pastry\\n description: Pastry resource\\n\",\"type\":\"OPEN_API_SPEC\",\"serviceId\":\"64c10bc25a36604b5a414077\",\"sourceArtifact\":\"https://raw.githubusercontent.com/microcks/microcks/master/samples/APIPastry-openapi.yaml\"}]" + }, + "redirectURL": "", + "headersSize": 393, + "bodySize": 6544, + "_transferSize": 6937, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T10:43:17.696Z", + "time": 8.809000004940666, + "timings": { + "blocked": 0.8890000046445057, + "dns": 0.0040000000000000036, + "ssl": -1, + "connect": 0.29200000000000004, + "send": 0.07799999999999996, + "wait": 6.897999999203719, + "receive": 0.6480000010924414, + "_blocked_queueing": 0.6370000046445057 + } + }, + { + "_initiator": { + "type": "script", + "stack": { + "callFrames": [ + { + "functionName": "scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 6376, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3414, + "columnNumber": 25 + }, + { + "functionName": "onScheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3305, + "columnNumber": 28 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3408, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3242, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleMacroTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3265, + "columnNumber": 24 + }, + { + "functionName": "scheduleMacroTaskWithCurrentZone", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4198, + "columnNumber": 24 + }, + { + "functionName": "", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 6409, + "columnNumber": 27 + }, + { + "functionName": "proto.", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4522, + "columnNumber": 23 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 14521, + "columnNumber": 16 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175575, + "columnNumber": 89 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._tryNext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175569, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175552, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 172194, + "columnNumber": 19 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175530, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/filter.js.FilterOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 174717, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177636, + "columnNumber": 114 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177626, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175301, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/BehaviorSubject.js.BehaviorSubject._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169174, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169821, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177604, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.ObservableStrategy.createSubscription", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11354, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11436, + "columnNumber": 44 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe.transform", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11417, + "columnNumber": 21 + }, + { + "functionName": "eval", + "scriptId": "1953", + "url": "ng:///AppModule/ServiceDetailPageComponent.ngfactory.js", + "lineNumber": 1794, + "columnNumber": 75 + }, + { + "functionName": "debugUpdateDirectives", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74079, + "columnNumber": 20 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73475, + "columnNumber": 13 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execEmbeddedViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73679, + "columnNumber": 16 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73476, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execEmbeddedViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73679, + "columnNumber": 16 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73476, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callWithDebugContext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74345, + "columnNumber": 24 + }, + { + "functionName": "debugCheckAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74047, + "columnNumber": 11 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ViewRef_.detectChanges", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 64722, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ApplicationRef.tick", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71145, + "columnNumber": 25 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 104 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3395, + "columnNumber": 25 + }, + { + "functionName": "onInvoke", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70292, + "columnNumber": 32 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3394, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.run", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3154, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.NgZone.run", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70206, + "columnNumber": 27 + }, + { + "functionName": "next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 80 + }, + { + "functionName": "schedulerFn", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67771, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.__tryOrUnsub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170154, + "columnNumber": 15 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170092, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170036, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169779, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.EventEmitter.emit", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67755, + "columnNumber": 75 + }, + { + "functionName": "checkStable", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70261, + "columnNumber": 34 + }, + { + "functionName": "onLeave", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70328, + "columnNumber": 4 + }, + { + "functionName": "onInvokeTask", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70286, + "columnNumber": 16 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3426, + "columnNumber": 59 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.runTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3199, + "columnNumber": 46 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3502, + "columnNumber": 33 + }, + { + "functionName": "invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4697, + "columnNumber": 13 + }, + { + "functionName": "globalZoneAwareCallback", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4734, + "columnNumber": 20 + } + ], + "parent": { + "description": "load", + "callFrames": [ + { + "functionName": "customScheduleGlobal", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4836, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3414, + "columnNumber": 25 + }, + { + "functionName": "onScheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3305, + "columnNumber": 28 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3408, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3242, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.scheduleEventTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3268, + "columnNumber": 24 + }, + { + "functionName": "", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 5007, + "columnNumber": 32 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 14509, + "columnNumber": 16 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175575, + "columnNumber": 89 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._tryNext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175569, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175552, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 172194, + "columnNumber": 19 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175530, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/filter.js.FilterOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 174717, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180631, + "columnNumber": 30 + }, + { + "functionName": "subscribeToResult", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 180808, + "columnNumber": 83 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._innerSub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177636, + "columnNumber": 114 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177626, + "columnNumber": 13 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapSubscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175301, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/BehaviorSubject.js.BehaviorSubject._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169174, + "columnNumber": 23 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169395, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject._trySubscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169821, + "columnNumber": 50 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169381, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 175278, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/operators/switchMap.js.SwitchMapOperator.call", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 177604, + "columnNumber": 22 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169376, + "columnNumber": 30 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.ObservableStrategy.createSubscription", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11354, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe._subscribe", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11436, + "columnNumber": 44 + }, + { + "functionName": "push../node_modules/@angular/common/fesm5/common.js.AsyncPipe.transform", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 11417, + "columnNumber": 21 + }, + { + "functionName": "eval", + "scriptId": "1953", + "url": "ng:///AppModule/ServiceDetailPageComponent.ngfactory.js", + "lineNumber": 1845, + "columnNumber": 72 + }, + { + "functionName": "debugUpdateDirectives", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74079, + "columnNumber": 20 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73475, + "columnNumber": 13 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execEmbeddedViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73679, + "columnNumber": 16 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73476, + "columnNumber": 4 + }, + { + "functionName": "callViewAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73716, + "columnNumber": 20 + }, + { + "functionName": "execComponentViewsAction", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73658, + "columnNumber": 12 + }, + { + "functionName": "checkAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 73481, + "columnNumber": 4 + }, + { + "functionName": "callWithDebugContext", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74345, + "columnNumber": 24 + }, + { + "functionName": "debugCheckAndUpdateView", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 74047, + "columnNumber": 11 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ViewRef_.detectChanges", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 64722, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.ApplicationRef.tick", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71145, + "columnNumber": 25 + }, + { + "functionName": "", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 104 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3395, + "columnNumber": 25 + }, + { + "functionName": "onInvoke", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70292, + "columnNumber": 32 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3394, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.run", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3154, + "columnNumber": 42 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.NgZone.run", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70206, + "columnNumber": 27 + }, + { + "functionName": "next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 71034, + "columnNumber": 80 + }, + { + "functionName": "schedulerFn", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67771, + "columnNumber": 51 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.__tryOrUnsub", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170154, + "columnNumber": 15 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170092, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170036, + "columnNumber": 25 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 170013, + "columnNumber": 17 + }, + { + "functionName": "push../node_modules/rxjs/_esm5/internal/Subject.js.Subject.next", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 169779, + "columnNumber": 24 + }, + { + "functionName": "push../node_modules/@angular/core/fesm5/core.js.EventEmitter.emit", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 67755, + "columnNumber": 75 + }, + { + "functionName": "checkStable", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70261, + "columnNumber": 34 + }, + { + "functionName": "onHasTask", + "scriptId": "274", + "url": "http://localhost:4200/vendor.js", + "lineNumber": 70305, + "columnNumber": 20 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate.hasTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3447, + "columnNumber": 36 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneDelegate._updateTaskCount", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3467, + "columnNumber": 21 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone._updateTaskCount", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3295, + "columnNumber": 33 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.Zone.runTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3216, + "columnNumber": 29 + }, + { + "functionName": "drainMicroTaskQueue", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3605, + "columnNumber": 34 + }, + { + "functionName": "push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 3506, + "columnNumber": 20 + }, + { + "functionName": "invokeTask", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4697, + "columnNumber": 13 + }, + { + "functionName": "globalZoneAwareCallback", + "scriptId": "18", + "url": "http://localhost:4200/polyfills.js", + "lineNumber": 4723, + "columnNumber": 16 + } + ] + } + } + }, + "_priority": "High", + "_resourceType": "xhr", + "cache": {}, + "connection": "233202", + "request": { + "method": "GET", + "url": "http://localhost:4200/api/metrics/conformance/service/64c10bc25a36604b5a414077", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Accept", + "value": "application/json, text/plain, */*" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "localhost:4200" + }, + { + "name": "Referer", + "value": "http://localhost:4200/" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + }, + { + "name": "Sec-GPC", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Brave\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 594, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "X-Powered-By", + "value": "Express" + }, + { + "name": "access-control-allow-headers", + "value": "*" + }, + { + "name": "access-control-allow-methods", + "value": "POST, PUT, GET, OPTIONS, DELETE" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "access-control-max-age", + "value": "3600" + }, + { + "name": "connection", + "value": "close" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "date", + "value": "Wed, 16 Aug 2023 10:43:17 GMT" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "vary", + "value": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" + } + ], + "cookies": [], + "content": { + "size": 183, + "mimeType": "application/json", + "compression": -11, + "text": "{\"id\":\"64c10bc25a36604b5a414085\",\"serviceId\":\"64c10bc25a36604b5a414077\",\"maxPossibleScore\":82.5,\"currentScore\":0.0,\"lastUpdateDay\":\"20230727\",\"latestTrend\":\"STABLE\",\"latestScores\":{}}" + }, + "redirectURL": "", + "headersSize": 393, + "bodySize": 194, + "_transferSize": 587, + "_error": null + }, + "serverIPAddress": "[::1]", + "startedDateTime": "2023-08-16T10:43:17.697Z", + "time": 7.263999999172985, + "timings": { + "blocked": 0.78999999313429, + "dns": 0.0020000000000000018, + "ssl": -1, + "connect": 0.179, + "send": 0.09700000000000003, + "wait": 5.985999993153848, + "receive": 0.21000001288484782, + "_blocked_queueing": 0.60299999313429 + } + } + ] + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/movie-graph-api-1.0.har b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/movie-graph-api-1.0.har new file mode 100644 index 000000000..5f6aefe62 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/har/movie-graph-api-1.0.har @@ -0,0 +1 @@ +{"log":{"version":"1.2","creator":{"name":"Proxyman","version":"4.9.1"},"comment": "microcksId: Movie Graph API:1.0 \n apiPrefix: /graphql/Movie+Graph+API/1.0","entries":[{"time":66.929100036621094,"_isHTTPS":false,"_webSocketMessages":null,"_remoteDeviceIP":null,"timings":{"connect":1,"send":-1,"dns":-1,"ssl":-1,"wait":-1,"blocked":-1,"receive":-1},"_serverAddress":"127.0.0.1","_isIntercepted":true,"_id":"59375","serverIPAddress":"127.0.0.1","_name":"59375","_clientAddress":"127.0.0.1","_clientBundlePath":"\/Applications\/Postman.app","request":{"method":"POST","bodySize":203,"headersSize":281,"postData":{"params":[{"name":"{\"query\":\"query film ($id: String) {\\n film (id: $id) {\\n id\\n title\\n director\\n episodeID\\n rating\\n starCount \\n }\\n}\",\"variables\":{\n \"id\": \"3\"\n}}","value":""}],"text":"{\"query\":\"query film ($id: String) {\\n film (id: $id) {\\n id\\n title\\n director\\n episodeID\\n rating\\n starCount \\n }\\n}\",\"variables\":{\n \"id\": \"3\"\n}}","mimeType":"application\/json"},"cookies":[],"headers":[{"name":"Content-Type","value":"application\/json"},{"name":"User-Agent","value":"PostmanRuntime\/7.32.3"},{"name":"Accept","value":"*\/*"},{"name":"Postman-Token","value":"48d0eb1b-682a-40dc-8a70-094d9ab78749"},{"name":"Host","value":"localhost.proxyman.io:8080"},{"name":"Accept-Encoding","value":"gzip, deflate, br"},{"name":"Connection","value":"keep-alive"},{"name":"Content-Length","value":"203"}],"queryString":[],"httpVersion":"HTTP\/1.1","url":"http:\/\/localhost.proxyman.io:8080\/graphql\/Movie+Graph+API\/1.0\/"},"_serverPort":8080,"_clientName":"Postman","_clientPort":60287,"response":{"status":200,"content":{"size":128,"mimeType":"application\/json","encoding":"base64","text":"eyJkYXRhIjp7ImZpbG0iOnsiaWQiOiIzIiwidGl0bGUiOiJSZXR1cm4gb2YgdGhlIEplZGkiLCJlcGlzb2RlSUQiOjYsImRpcmVjdG9yIjoiUmljaGFyZCBNYXJxdWFuZCIsInN0YXJDb3VudCI6NSwicmF0aW5nIjo4LjN9fX0="},"bodySize":128,"headersSize":246,"cookies":[],"statusText":"OK","headers":[{"name":"Vary","value":"Origin"},{"name":"Vary","value":"Access-Control-Request-Method"},{"name":"Vary","value":"Access-Control-Request-Headers"},{"name":"Content-Type","value":"application\/json"},{"name":"Content-Length","value":"128"},{"name":"Date","value":"Wed, 23 Aug 2023 09:15:01 GMT"},{"name":"Keep-Alive","value":"timeout=60"},{"name":"Connection","value":"keep-alive"}],"httpVersion":"HTTP\/1.1","redirectURL":""},"comment":"","startedDateTime":"2023-08-23T11:15:01.184+02:00","cache":{}}]}} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/APIPastry-2.0-examples.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/APIPastry-2.0-examples.yml new file mode 100644 index 000000000..56f8ad444 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/APIPastry-2.0-examples.yml @@ -0,0 +1,44 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: API Pastry - 2.0 + version: 2.0.0 +operations: + 'GET /pastry/{name}': + Eclair Chocolat: + request: + parameters: + name: Eclair Chocolat + response: + mediaType: application/json + body: + name: Eclair Chocolat + description: Delicieux Eclair Chocolat pas calorique du tout + size: M + price: 2.5 + status: unknown + Eclair Chocolat Xml: + request: + parameters: + name: Eclair Chocolat Xml + response: + mediaType: text/xml + body: |- + + Eclair Cafe + Delicieux Eclair au Chocolat pas calorique du tout + M + 2.5 + unknown + + Eclair Chocolat Empty Status: + request: + parameters: + name: Eclair Chocolat + response: + mediaType: application/json + body: + name: Eclair Chocolat + description: Delicieux Eclair Chocolat pas calorique du tout + size: M + price: 2.5 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/APIPastry-2.0-metadata.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/APIPastry-2.0-metadata.yml new file mode 100644 index 000000000..c0791508b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/APIPastry-2.0-metadata.yml @@ -0,0 +1,18 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIMetadata +metadata: + name: API Pastry - 2.0 + version: 2.0.0 +operations: + 'GET /pastry/{name}': + delay: 100 + parameterConstraints: + - name: Authorization + in: header + required: true + recopy: false + mustMatchRegexp: "^Bearer\\s[a-zA-Z0-9\\._-]+$" + - name: x-request-id + in: header + required: true + recopy: true \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/hello-grpc-v1-examples.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/hello-grpc-v1-examples.yml new file mode 100644 index 000000000..6185b60b4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/hello-grpc-v1-examples.yml @@ -0,0 +1,22 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: io.github.microcks.grpc.hello.v1.HelloService + version: v1 +operations: + 'greeting': + Laurent: + request: + body: + firstname: Laurent + lastname: Broudoux + response: + body: + greeting: Hello Laurent Broudoux ! + John: + request: + body: |- + {"firstname": "John", "lastname": "Doe"} + response: + body: + greeting: Hello John Doe ! diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/hello-grpc-v1-metadata.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/hello-grpc-v1-metadata.yml new file mode 100644 index 000000000..e22d7b71d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/hello-grpc-v1-metadata.yml @@ -0,0 +1,22 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIMetadata +metadata: + name: HelloService + version: v1 + labels: + domain: greeting + status: stable + team: Team A +operations: + 'POST /greeting': + delay: 100 + dispatcher: JSON_BODY + dispatcherRules: |- + { + "exp": "/firstname", + "operator": "equals", + "cases": { + "Laurent": "Laurent", + "default": "Philippe" + } + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/user-signedup-0.1.0-examples.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/user-signedup-0.1.0-examples.yml new file mode 100644 index 000000000..5c9b7e7c2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/user-signedup-0.1.0-examples.yml @@ -0,0 +1,16 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: User signed-up API + version: 0.1.0 +operations: + 'SUBSCRIBE /user/signedup': + jane: + eventMessage: + headers: + my-app-header: 123 + sentAt: "2024-07-14T18:01:28Z" + payload: + fullName: Jane Doe + email: jane@microcks.io + age: 35 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/weather-forecast-examples.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/weather-forecast-examples.yml new file mode 100644 index 000000000..9c8908633 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/weather-forecast-examples.yml @@ -0,0 +1,22 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: WeatherForecast API + version: 1.0.0 +operations: + 'GET /forecast/{region}': + north: + request: + parameters: + region: north + apiKey: 123456 + response: + status: 200 + mediaType: application/json + body: |- + { + "region": "north", + "temp": -1.5, + "weather": "snowy", + "visibility": 25 + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/weather-forecast-headers-examples.yml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/weather-forecast-headers-examples.yml new file mode 100644 index 000000000..5a9342979 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/metadata/weather-forecast-headers-examples.yml @@ -0,0 +1,21 @@ +apiVersion: mocks.microcks.io/v1alpha1 +kind: APIExamples +metadata: + name: WeatherForecast API + version: 1.0.0 +operations: + 'GET /forecast': + north: + request: + headers: + region: north + response: + status: 200 + mediaType: application/json + body: |- + { + "region": "north", + "temp": -1.5, + "weather": "snowy", + "visibility": 25 + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-collection.json new file mode 100644 index 000000000..88afdbf89 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-collection.json @@ -0,0 +1,369 @@ +{ + "variables": [], + "info": { + "name": "Beer Catalog API", + "_postman_id": "7194f912-d5f5-3ca0-cf75-8a0b912abc4e", + "description": "version=0.9 - An API for querying beer catalog of Acme Inc.", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "beer", + "description": "Folder for beer", + "item": [ + { + "name": "Get beer having name", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var expectedName = globals[\"name\"];", + "var jsonData = JSON.parse(responseBody);", + "", + "var schema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": { \"type\": \"string\", \"enum\": [expectedName] },", + " \"country\": { \"type\": \"string\" },", + " \"type\": { \"type\": \"string\" },", + " \"rating\": { \"type\": \"number\" },", + " \"status\": { \"type\": \"string\" }", + " }", + "};", + "", + "tests[\"Valid name in response\"] = tv4.validate(jsonData, schema);" + ] + } + } + ], + "request": { + "url": { + "raw": "http:///beer/:name", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + ":name" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "name", + "value": "" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Get beer having name" + }, + "response": [ + { + "id": "809e4ade-2462-454b-b8de-880f520e8c79", + "name": "Rodenbach", + "originalRequest": { + "url": { + "raw": "http:///beer/:name", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + ":name" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "name", + "value": "Rodenbach" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\"name\": \"Rodenbach\", \"country\": \"Belgium\", \"type\": \"Brown ale\", \"rating\": 4.2, \"status\": \"available\"}" + }, + { + "id": "b205add4-5386-4c79-a38c-61cd75a94435", + "name": "Weissbier", + "originalRequest": { + "url": { + "raw": "http:///beer/:name", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + ":name" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "name", + "value": "Weissbier" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"name\": \"Weissbier\",\n \"country\": \"Germany\",\n \"type\": \"Wheat\",\n \"rating\": 4.1,\n \"status\": \"out_of_stock\"\n}" + } + ] + }, + { + "name": "Get beers having status", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var expectedStatus = globals[\"status\"];", + "var jsonData = JSON.parse(responseBody);", + "", + "var schema = {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": { \"type\": \"string\" },", + " \"country\": { \"type\": \"string\" },", + " \"type\": { \"type\": \"string\" },", + " \"rating\": { \"type\": \"number\" },", + " \"status\": { \"type\": \"string\", \"enum\": [expectedStatus] }", + " }", + " }", + "};", + "", + "tests[\"Valid response\"] = tv4.validate(jsonData, schema);" + ] + } + } + ], + "request": { + "url": { + "raw": "http:///beer/findByStatus/:status", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + "findByStatus", + ":status" + ], + "query": [], + "variable": [ + { + "key": "status", + "value": "" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Get beers having status" + }, + "response": [ + { + "id": "2cef0eba-abe8-468e-8dc9-0731f5758b52", + "name": "Get available beers", + "originalRequest": { + "url": { + "raw": "http:///beer/findByStatus/:status", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + "findByStatus", + ":status" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "status", + "value": "available" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[{\"name\": \"Rodenbach\", \"country\": \"Belgium\", \"type\": \"Brown ale\", \"rating\": 4.2, \"status\": \"available\"},\n{\"name\": \"Westmalle Triple\", \"country\": \"Belgium\", \"type\": \"Trappist\", \"rating\": 3.8, \"status\": \"available\"}]" + }, + { + "id": "5b0ccc56-539d-428c-8334-d641a900b60e", + "name": "Get out_of_stock beers", + "originalRequest": { + "url": { + "raw": "http:///beer/findByStatus/:status", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer", + "findByStatus", + ":status" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "status", + "value": "out_of_stock" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[{\"name\": \"Weissbier\", \"country\": \"Germany\", \"type\": \"Wheat\", \"rating\": 4.1, \"status\": \"out_of_stock\"}]" + } + ] + }, + { + "name": "List beers within catalog", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "tests[\"Status code is OK\"] = (responseCode.code === 200 || responseCode.code === 404);" + ] + } + } + ], + "request": { + "url": { + "raw": "http:///beer?page={{page}}", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer" + ], + "query": [ + { + "key": "page", + "value": "{{page}}", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "List beers within catalog" + }, + "response": [ + { + "id": "6941bfe0-0e27-4ee6-a244-f03dfca6fe25", + "name": "List page 0", + "originalRequest": { + "url": { + "raw": "http:///beer?page=0", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "beer" + ], + "query": [ + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[\n {\n \"name\": \"Rodenbach\",\n \"country\": \"Belgium\",\n \"type\": \"Brown ale\",\n \"rating\": 4.2,\n \"status\": \"available\"\n },\n {\n \"name\": \"Westmalle Triple\",\n \"country\": \"Belgium\",\n \"type\": \"Trappist\",\n \"rating\": 3.8,\n \"status\": \"available\"\n },\n {\n \"name\": \"Weissbier\",\n \"country\": \"Germany\",\n \"type\": \"Wheat\",\n \"rating\": 4.1,\n \"status\": \"out_of_stock\"\n }\n]" + } + ] + } + ] + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.json new file mode 100644 index 000000000..ca944e3ae --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.json @@ -0,0 +1,171 @@ +{ + "swagger": "2.0", + "info": { + "title": "Beer Catalog API", + "version": "0.9", + "description": "An API for querying beer catalog of Acme Inc.", + "contact": { + "name": "Laurent Broudoux", + "url": "http://github.com/lbroudoux", + "email": "laurent.broudoux@gmail.com" + }, + "license": { + "name": "MIT License", + "url": "https://opensource.org/licenses/MIT" + }, + "x-microcks": { + "labels": { + "domain": "beers", + "status": "beta", + "team": "Team A" + } + } + }, + "paths": { + "/beer/{name}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "beer" + ], + "responses": { + "200": { + "description": "Beer having requested name", + "schema": { + "$ref": "#/definitions/Beer" + }, + "examples": { + "application/json": "{\n \"name\": \"Rodenbach\",\n \"country\": \"Belgium\",\n \"type\": \"Fruit\",\n \"rating\": 4.3,\n \"status\": \"available\"\n}" + } + } + }, + "operationId": "GetBeer", + "summary": "Get beer having name", + "description": "Get beer having name" + }, + "parameters": [ + { + "name": "name", + "description": "Name of beer to retrieve", + "in": "path", + "required": true, + "type": "string" + } + ] + }, + "/beer/findByStatus/{status}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "beer" + ], + "responses": { + "200": { + "description": "List of beers having requested status", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Beer" + } + }, + "examples": { + "application/json": "[\n {\n \"name\": \"Rodenbach,\n \"country\": \"Belgium\",\n \"type\": \"Fruit\",\n \"rating\": 4.2,\n \"status\": \"available\"\n },\n {\n \"name\": \"Weissbier\",\n \"country\": \"Germany\",\n \"type\": \"Wheat\",\n \"rating\": 4.1,\n \"status\": \"available\"\n }\n]" + } + } + }, + "operationId": "FindBeersByStatus", + "summary": "Get beers having status", + "description": "Get beers having status" + }, + "parameters": [ + { + "name": "status", + "description": "Status of beers to retrieve", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "page", + "description": "Number of page to retrieve", + "in": "query", + "type": "number" + } + ] + }, + "/beer": { + "get": { + "tags": [ + "beer" + ], + "parameters": [ + { + "name": "page", + "description": "Description", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Array of beers", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Beer" + } + } + } + }, + "operationId": "ListBeers", + "summary": "List beers within catalog", + "description": "List beers within catalog" + }, + "parameters": [ + { + "name": "page", + "description": "Number of page to retrieve", + "in": "query", + "type": "number" + } + ] + } + }, + "definitions": { + "Beer": { + "properties": { + "name": { + "description": "Name of Beer", + "type": "string" + }, + "country": { + "description": "Origin country of Beer", + "type": "string" + }, + "type": { + "description": "Type of Beer", + "type": "string" + }, + "rating": { + "description": "Rating from customers", + "type": "number" + }, + "status": { + "description": "Stock status", + "type": "string" + } + } + } + }, + "tags": [ + { + "name": "beer", + "description": "Beer resource" + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml new file mode 100644 index 000000000..37c3e0864 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/beer-catalog-api-swagger.yaml @@ -0,0 +1,143 @@ +swagger: '2.0' +info: + title: Beer Catalog API + version: '0.9' + description: An API for querying beer catalog of Acme Inc. + contact: + name: Laurent Broudoux + url: 'http://github.com/lbroudoux' + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: 'https://opensource.org/licenses/MIT' + x-microcks: + labels: + domain: beers + status: beta + team: Team A +paths: + '/beer/{name}': + get: + produces: + - application/json + tags: + - beer + responses: + '200': + description: Beer having requested name + schema: + $ref: '#/definitions/Beer' + examples: + application/json: |- + { + "name": "Rodenbach", + "country": "Belgium", + "type": "Fruit", + "rating": 4.3, + "status": "available" + } + operationId: GetBeer + summary: Get beer having name + description: Get beer having name + parameters: + - + name: name + description: Name of beer to retrieve + in: path + required: true + type: string + '/beer/findByStatus/{status}': + get: + produces: + - application/json + tags: + - beer + responses: + '200': + description: List of beers having requested status + schema: + type: array + items: + $ref: '#/definitions/Beer' + examples: + application/json: |- + [ + { + "name": "Rodenbach, + "country": "Belgium", + "type": "Fruit", + "rating": 4.2, + "status": "available" + }, + { + "name": "Weissbier", + "country": "Germany", + "type": "Wheat", + "rating": 4.1, + "status": "available" + } + ] + operationId: FindBeersByStatus + summary: Get beers having status + description: Get beers having status + parameters: + - + name: status + description: Status of beers to retrieve + in: path + required: true + type: string + - + name: page + description: Number of page to retrieve + in: query + type: number + /beer: + get: + tags: + - beer + parameters: + - + name: myparam + description: Description + in: query + required: true + type: string + responses: + '200': + description: Array of beers + schema: + type: array + items: + $ref: '#/definitions/Beer' + operationId: ListBeers + summary: List beers within catalog + description: List beers within catalog + parameters: + - + name: page + description: Number of page to retrieve + in: query + type: number +definitions: + Beer: + properties: + name: + description: Name of Beer + type: string + country: + description: Origin country of Beer + type: string + type: + description: Type of Beer + type: string + rating: + description: Rating from customers + type: number + status: + description: Stock status + type: string +tags: + - + name: beer + description: Beer resource \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-3.1-complete.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-3.1-complete.yaml new file mode 100644 index 000000000..f9359758c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-3.1-complete.yaml @@ -0,0 +1,169 @@ +--- +openapi: 3.1.0 +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.1.0 +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + post: + summary: Add a car to current owner + description: Add a car to current owner description + operationId: addCarOp + requestBody: + description: Car body + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car + description: Should return 201 + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + required: true + responses: + 201: + description: Car created + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car response + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + 400: {} + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + value: laurent + laurent_307: + $ref: '#/components/examples/param_laurent' + /owner/{owner}/car/{car}/passenger: + get: + summary: 'Get the passengers of a car' + description: 'Get all the passengers of a car' + operationId: getPassengersOp + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Person' + examples: + laurent_307_passengers: + value: "[\n {\"firstname\": \"Nicolas\", \"lastname\": \"Masse\"},\n {\"firstname\": \"Eric\", \"lastname\": \"Wittmann\"}\n]" + post: + summary: Add some passengers to the car + description: Add some passengers to the car description + operationId: addPassengerOp + responses: + 200: + content: + application/json: {} + parameters: + - name: owner + in: path + description: Owner of the car + required: true + schema: + type: string + examples: + laurent_307_passengers: + $ref: '#/components/examples/param_laurent' + - name: car + in: path + description: The car to manage passengers for + required: true + schema: + type: string + examples: + laurent_307_passengers: + value: 307 +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + Person: + type: object + properties: + firstname: + description: 'Firstname of person' + type: string + lastname: + description: 'Lastname of person' + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-complete.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-complete.yaml new file mode 100644 index 000000000..b17f1d5c4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-complete.yaml @@ -0,0 +1,169 @@ +--- +openapi: 3.0.0 +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.1.0 +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + post: + summary: Add a car to current owner + description: Add a car to current owner description + operationId: addCarOp + requestBody: + description: Car body + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car + description: Should return 201 + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + required: true + responses: + 201: + description: Car created + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car response + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + 400: {} + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + value: laurent + laurent_307: + $ref: '#/components/examples/param_laurent' + /owner/{owner}/car/{car}/passenger: + get: + summary: 'Get the passengers of a car' + description: 'Get all the passengers of a car' + operationId: getPassengersOp + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Person' + examples: + laurent_307_passengers: + value: "[\n {\"firstname\": \"Nicolas\", \"lastname\": \"Masse\"},\n {\"firstname\": \"Eric\", \"lastname\": \"Wittmann\"}\n]" + post: + summary: Add some passengers to the car + description: Add some passengers to the car description + operationId: addPassengerOp + responses: + 200: + content: + application/json: {} + parameters: + - name: owner + in: path + description: Owner of the car + required: true + schema: + type: string + examples: + laurent_307_passengers: + $ref: '#/components/examples/param_laurent' + - name: car + in: path + description: The car to manage passengers for + required: true + schema: + type: string + examples: + laurent_307_passengers: + value: 307 +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + Person: + type: object + properties: + firstname: + description: 'Firstname of person' + type: string + lastname: + description: 'Lastname of person' + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-complex-refs.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-complex-refs.yaml new file mode 100644 index 000000000..c8fe1fed5 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-complex-refs.yaml @@ -0,0 +1,133 @@ +--- +openapi: 3.0.0 +info: + title: OpenAPI Car API with Refs + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + unknown: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + unknown: + value: 20 + - name: x-user-id + in: header + description: Token to be passed as a header + required: true + schema: + type: string + style: simple + examples: + laurent_cars: + value: poiuytrezamlkjhgfdsq + unknown: + value: '' + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + headers: + x-result-count: + description: Total number of results + schema: + type: string + examples: + laurent_cars: + value: 2 + 404: + description: Unknown + $ref: '#/components/responses/UnknownOwner' + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + $ref: '#/components/examples/param_laurent' + unknown: + summary: Value for unknown owner + value: unknown +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + examples: + param_laurent: + value: laurent + responses: + UnknownOwner: + description: Response in case of unknown owner + headers: + my-custom-header: + description: + schema: + type: string + examples: + unknown: + value: 'unknown' + content: + application/json: + examples: + unknown: + value: '{"reason": "owner not found"}' +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-extensions.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-extensions.json new file mode 100644 index 000000000..3aac007e7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-extensions.json @@ -0,0 +1,219 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "OpenAPI Car API", + "description": "Sample OpenAPI API using cars", + "contact": { + "name": "Laurent Broudoux", + "url": "https://github.com/lbroudoux", + "email": "laurent.broudoux@gmail.com" + }, + "license": { + "name": "MIT License", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "1.0.0", + "x-microcks": { + "labels": { + "domain": "cars", + "status": "beta", + "team": "Team A" + } + } + }, + "paths": { + "/owner/{owner}/car": { + "get": { + "summary": "List all cars of owner", + "description": "List all cars of owner description", + "operationId": "getCarsOp", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Result page wanted", + "required": false, + "schema": { + "type": "integer" + }, + "examples": { + "laurent_cars": { + "value": 0 + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of result in page", + "required": false, + "schema": { + "type": "integer" + }, + "examples": { + "laurent_cars": { + "value": 20 + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Car" + } + }, + "examples": { + "laurent_cars": { + "value": "[\n {\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003},\n {\"name\": \"jean-pierre\", \"model\": \"Peugeot Traveler\", \"year\": 2017}\n]" + } + } + } + } + } + } + }, + "post": { + "summary": "Add a car to current owner", + "description": "Add a car to current owner description", + "operationId": "addCarOp", + "requestBody": { + "description": "Car body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Car" + }, + "examples": { + "laurent_307": { + "summary": "Creation of a valid car", + "description": "Should return 201", + "value": "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}" + } + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Car created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Car" + }, + "examples": { + "laurent_307": { + "summary": "Creation of a valid car response", + "value": "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}" + } + } + } + } + }, + "400": {} + }, + "x-microcks-operation": { + "delay": 100, + "dispatcher": "SCRIPT", + "dispatcherRules": "def path = mockRequest.getRequest().getRequestURI();\nif (!path.contains(\"/laurent/car\")) {return \"Not Accepted\";}\ndef jsonSlurper = new groovy.json.JsonSlurper();\ndef car = jsonSlurper.parseText(mockRequest.getRequestContent());\nif (car.name == null) {return \"Not Accepted\"}\nreturn \"laurent_307\";" + } + }, + "parameters": [ + { + "name": "owner", + "in": "path", + "description": "Owner of the cars", + "required": true, + "schema": { + "format": "string", + "type": "string" + }, + "examples": { + "laurent_cars": { + "summary": "Value for laurent related examples", + "value": "laurent" + }, + "laurent_307": { + "$ref": "#/components/examples/param_laurent" + } + } + } + ] + }, + "/owner/{owner}/car/{car}/passenger": { + "post": { + "summary": "Add some passengers to the car", + "description": "Add some passengers to the car description", + "operationId": "addPassengerOp", + "responses": { + "200": { + "content": { + "application/json": {} + } + } + } + }, + "parameters": [ + { + "name": "owner", + "in": "path", + "description": "Owner of the car", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "car", + "in": "path", + "description": "The car to manage passengers for", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "components": { + "schemas": { + "Car": { + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name of the car", + "type": "string" + }, + "year": { + "description": "Build year", + "type": "integer" + }, + "model": { + "description": "Model of the car", + "type": "string" + } + } + } + }, + "examples": { + "param_laurent": { + "value": "laurent" + } + } + }, + "tags": [ + { + "name": "car", + "description": "" + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-extensions.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-extensions.yaml new file mode 100644 index 000000000..6ff570036 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-extensions.yaml @@ -0,0 +1,158 @@ +--- +openapi: 3.0.0 +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 + x-microcks: + labels: + domain: cars + status: beta + team: Team A +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + post: + summary: Add a car to current owner + description: Add a car to current owner description + operationId: addCarOp + requestBody: + description: Car body + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car + description: Should return 201 + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + required: true + responses: + 201: + description: Car created + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car response + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + 400: {} + x-microcks-operation: + delay: 100 + dispatcher: SCRIPT + dispatcherRules: |- + def path = mockRequest.getRequest().getRequestURI(); + if (!path.contains("/laurent/car")) { + return "Not Accepted" + } + def jsonSlurper = new groovy.json.JsonSlurper(); + def car = jsonSlurper.parseText(mockRequest.getRequestContent()); + if (car.name == null) { + return "Not Accepted" + } + return "laurent_307" + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + value: laurent + laurent_307: + $ref: '#/components/examples/param_laurent' + /owner/{owner}/car/{car}/passenger: + post: + summary: Add some passengers to the car + description: Add some passengers to the car description + operationId: addPassengerOp + responses: + 200: + content: + application/json: {} + parameters: + - name: owner + in: path + description: Owner of the car + required: true + schema: + type: string + - name: car + in: path + description: The car to manage passengers for + required: true + schema: + type: string +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-headers.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-headers.yaml new file mode 100644 index 000000000..e7852d6ff --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-headers.yaml @@ -0,0 +1,107 @@ +--- +openapi: 3.0.0 +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + - name: x-user-id + in: header + description: Token to be passed as a header + required: true + schema: + type: string + style: simple + examples: + laurent_cars: + value: poiuytrezamlkjhgfdsq + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + headers: + x-result-count: + description: Total number of results + schema: + type: string + examples: + laurent_cars: + value: 2 + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + value: laurent + laurent_307: + $ref: '#/components/examples/param_laurent' +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-quoted.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-quoted.yaml new file mode 100644 index 000000000..21cecd5a7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-quoted.yaml @@ -0,0 +1,139 @@ +--- +openapi: '3.0.0' +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: '1.0.0' +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + post: + summary: Add a car to current owner + description: Add a car to current owner description + operationId: addCarOp + requestBody: + description: Car body + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car + description: Should return 201 + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + required: true + responses: + 201: + description: Car created + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car response + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + 400: {} + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + value: laurent + laurent_307: + $ref: '#/components/examples/param_laurent' + /owner/{owner}/car/{car}/passenger: + post: + summary: Add some passengers to the car + description: Add some passengers to the car description + operationId: addPassengerOp + responses: + 200: + content: + application/json: {} + parameters: + - name: owner + in: path + description: Owner of the car + required: true + schema: + type: string + - name: car + in: path + description: The car to manage passengers for + required: true + schema: + type: string +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-spacesops.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-spacesops.yaml new file mode 100644 index 000000000..581be7c57 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-spacesops.yaml @@ -0,0 +1,139 @@ +--- +openapi: 3.0.0 +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 +paths: + ' /owner/{owner}/car': + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + post: + summary: Add a car to current owner + description: Add a car to current owner description + operationId: addCarOp + requestBody: + description: Car body + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car + description: Should return 201 + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + required: true + responses: + 201: + description: Car created + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car response + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + 400: {} + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + value: laurent + laurent_307: + $ref: '#/components/examples/param_laurent' + '/owner/{owner}/car/{car}/passenger ': + post: + summary: Add some passengers to the car + description: Add some passengers to the car description + operationId: addPassengerOp + responses: + 200: + content: + application/json: {} + parameters: + - name: owner + in: path + description: Owner of the car + required: true + schema: + type: string + - name: car + in: path + description: The car to manage passengers for + required: true + schema: + type: string +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-uncomplete-params.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-uncomplete-params.yaml new file mode 100644 index 000000000..3d8fa3092 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-uncomplete-params.yaml @@ -0,0 +1,143 @@ +--- +openapi: 3.0.0 +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + post: + summary: Add a car to current owner + description: Add a car to current owner description + operationId: addCarOp + requestBody: + description: Car body + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car + description: Should return 201 + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + required: true + responses: + 201: + description: Car created + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car response + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + 400: {} + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_307: + $ref: '#/components/examples/param_laurent' + /owner/{owner}/car/{car}/passenger: + get: + summary: Get some passengers from the car + description: Get some passengers from the car description + operationId: getPassengerOp + responses: + 200: + content: + application/json: + examples: + laurent_307_passengers: + summary: Passengers of laurent 307 + value: '["name": "Arthur", "name": "Fanette", "name": "Felix", "name": "Abel"]' + parameters: + - name: owner + in: path + description: Owner of the car + required: true + schema: + type: string + examples: + laurent_307_passengers: + value: laurent + - name: car + in: path + description: The car to manage passengers for + required: true + schema: + type: string +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-with-json.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-with-json.yaml new file mode 100644 index 000000000..fc49c2eb4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi-with-json.yaml @@ -0,0 +1,88 @@ +openapi: 3.0.0 +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + value: laurent + laurent_307: + $ref: '#/components/examples/param_laurent' +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: Car \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi.json new file mode 100644 index 000000000..2c3147900 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi.json @@ -0,0 +1,207 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "OpenAPI Car API", + "description": "Sample OpenAPI API using cars", + "contact": { + "name": "Laurent Broudoux", + "url": "https://github.com/lbroudoux", + "email": "laurent.broudoux@gmail.com" + }, + "license": { + "name": "MIT License", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "1.0.0" + }, + "paths": { + "/owner/{owner}/car": { + "get": { + "summary": "List all cars of owner", + "description": "List all cars of owner description", + "operationId": "getCarsOp", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Result page wanted", + "required": false, + "schema": { + "type": "integer" + }, + "examples": { + "laurent_cars": { + "value": 0 + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of result in page", + "required": false, + "schema": { + "type": "integer" + }, + "examples": { + "laurent_cars": { + "value": 20 + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Car" + } + }, + "examples": { + "laurent_cars": { + "value": "[\n {\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003},\n {\"name\": \"jean-pierre\", \"model\": \"Peugeot Traveler\", \"year\": 2017}\n]" + } + } + } + } + } + } + }, + "post": { + "summary": "Add a car to current owner", + "description": "Add a car to current owner description", + "operationId": "addCarOp", + "requestBody": { + "description": "Car body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Car" + }, + "examples": { + "laurent_307": { + "summary": "Creation of a valid car", + "description": "Should return 201", + "value": "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}" + } + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Car created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Car" + }, + "examples": { + "laurent_307": { + "summary": "Creation of a valid car response", + "value": "{\"name\": \"307\", \"model\": \"Peugeot 307\", \"year\": 2003}" + } + } + } + } + }, + "400": {} + } + }, + "parameters": [ + { + "name": "owner", + "in": "path", + "description": "Owner of the cars", + "required": true, + "schema": { + "format": "string", + "type": "string" + }, + "examples": { + "laurent_cars": { + "summary": "Value for laurent related examples", + "value": "laurent" + }, + "laurent_307": { + "$ref": "#/components/examples/param_laurent" + } + } + } + ] + }, + "/owner/{owner}/car/{car}/passenger": { + "post": { + "summary": "Add some passengers to the car", + "description": "Add some passengers to the car description", + "operationId": "addPassengerOp", + "responses": { + "200": { + "content": { + "application/json": {} + } + } + } + }, + "parameters": [ + { + "name": "owner", + "in": "path", + "description": "Owner of the car", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "car", + "in": "path", + "description": "The car to manage passengers for", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "components": { + "schemas": { + "Car": { + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name of the car", + "type": "string" + }, + "year": { + "description": "Build year", + "type": "integer" + }, + "model": { + "description": "Model of the car", + "type": "string" + } + } + } + }, + "examples": { + "param_laurent": { + "value": "laurent" + } + } + }, + "tags": [ + { + "name": "car", + "description": "" + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi.yaml new file mode 100644 index 000000000..50d2f804f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/cars-openapi.yaml @@ -0,0 +1,139 @@ +--- +openapi: 3.0.0 +info: + title: OpenAPI Car API + description: Sample OpenAPI API using cars + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 +paths: + /owner/{owner}/car: + get: + summary: List all cars of owner + description: List all cars of owner description + operationId: getCarsOp + parameters: + - name: page + in: query + description: Result page wanted + required: false + schema: + type: integer + examples: + laurent_cars: + value: 0 + - name: limit + in: query + description: Number of result in page + required: false + schema: + type: integer + examples: + laurent_cars: + value: 20 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Car' + examples: + laurent_cars: + value: |- + [ + {"name": "307", "model": "Peugeot 307", "year": 2003}, + {"name": "jean-pierre", "model": "Peugeot Traveler", "year": 2017} + ] + post: + summary: Add a car to current owner + description: Add a car to current owner description + operationId: addCarOp + requestBody: + description: Car body + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car + description: Should return 201 + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + required: true + responses: + 201: + description: Car created + content: + application/json: + schema: + $ref: '#/components/schemas/Car' + examples: + laurent_307: + summary: Creation of a valid car response + value: '{"name": "307", "model": "Peugeot 307", "year": 2003}' + 400: {} + parameters: + - name: owner + in: path + description: Owner of the cars + required: true + schema: + format: string + type: string + examples: + laurent_cars: + summary: Value for laurent related examples + value: laurent + laurent_307: + $ref: '#/components/examples/param_laurent' + /owner/{owner}/car/{car}/passenger: + post: + summary: Add some passengers to the car + description: Add some passengers to the car description + operationId: addPassengerOp + responses: + 200: + content: + application/json: {} + parameters: + - name: owner + in: path + description: Owner of the car + required: true + schema: + type: string + - name: car + in: path + description: The car to manage passengers for + required: true + schema: + type: string +components: + schemas: + Car: + required: + - name + properties: + name: + description: Name of the car + type: string + year: + description: Build year + type: integer + model: + description: Model of the car + type: string + examples: + param_laurent: + value: laurent +tags: +- name: car + description: \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/examples-ref-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/examples-ref-openapi.yaml new file mode 100644 index 000000000..223b3c16b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/examples-ref-openapi.yaml @@ -0,0 +1,23 @@ +openapi: "3.0.0" +info: + title: Broken Ref + version: 2.0.0 +paths: + /v1.0/endpoint: + get: + description: Broken Ref + + responses: + '200': + content: + application/json: + examples: + $ref: '#/paths/~1v1.0~1endpoint/get/responses/200/headers/x-Some-Header/examples' + description: redirect to pdf file + headers: + x-Some-Header: + examples: + example1: + value: 'someValue' + schema: + type: string \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/hello-dynamic-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/hello-dynamic-openapi.yaml new file mode 100644 index 000000000..39be7f6a4 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/hello-dynamic-openapi.yaml @@ -0,0 +1,65 @@ +--- +openapi: 3.0.2 +info: + title: Hello Dynamic API + description: Description for Hello Dynamic API + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 +paths: + /hello: + get: + summary: Say hello in a dynamic way + description: Use Microcks dynamic template in response + operationId: hello + requestBody: + description: Name + content: + application/json: + schema: + $ref: '#/components/schemas/Name' + examples: + dynamic: + value: |- + { "name": "Laurent" } + required: true + responses: + 200: + description: Greeting + content: + application/json: + schema: + $ref: '#/components/schemas/Greeting' + examples: + dynamic: + value: |- + { + "id": "{{ randomString(64) }}", + "date": "{{ now(dd/MM/yyyy) }}", + "message": "Hello {{ request.body/name }}!" + } +components: + schemas: + Name: + title: Root type for Name + description: The root of the Name type's schema + type: object + properties: + name: + type: string + Greeting: + title: Root type for Greeting + description: The root of the Greeting type's schema + type: object + properties: + id: + type: string + date: + type: string + message: + type: string \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/locations-openapi.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/locations-openapi.json new file mode 100644 index 000000000..996ab6351 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/locations-openapi.json @@ -0,0 +1,103 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "LocationById", + "description": "A brand new API with no content. Go nuts!", + "contact": {}, + "version": "1.0" + }, + "servers": [ + { + "url": "https://{defaultHost}", + "variables": { + "defaultHost": { + "default": "www.example.com" + } + } + } + ], + "paths": { + "/location/{id}": { + "get": { + "summary": "getLocationById", + "description": "Get location", + "operationId": "GetLocationById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Location identifier", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + }, + "examples": { + "location": { + "value": "83" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RootTypeforlocation" + }, + "examples": { + "location": { + "value": "{\n \"id\": 83,\n \"name\": \"some text\",\n \"location\": {\n \"lat\": 55.93,\n \"lng\": 97.05\n },\n \"type\": \"some text\",\n \"status\": \"some text\"\n}" + } + } + } + } + } + }, + "deprecated": false + } + } + }, + "components": { + "schemas": { + "RootTypeforlocation": { + "title": "RootTypeforlocation", + "description": "The root of the location type's schema.", + "type": "object", + "properties": { + "id": { + "format": "int32", + "type": "integer" + }, + "name": { + "type": "string" + }, + "location": { + "$ref": "#/components/schemas/Location" + }, + "type": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "Location": { + "title": "Location", + "type": "object", + "properties": { + "lat": { + "type": "number" + }, + "lng": { + "type": "number" + } + } + } + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/object-query-params.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/object-query-params.yaml new file mode 100644 index 000000000..98aa2e32f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/object-query-params.yaml @@ -0,0 +1,48 @@ +--- +openapi: 3.0.3 +info: + title: Object query param API + description: Description for Object query param API + license: + name: MIT License + url: https://opensource.org/licenses/MIT + version: 1.0.0 +paths: + /messiah: + get: + summary: Get the messiah name for a given person + description: Use object in query param + operationId: getMessiah + parameters: + - in: query + name: irrelevantHere + schema: + $ref: '#/components/schemas/Person' + examples: + dune: + value: + lastName: Atreides + firstName: Paul + responses: + 200: + description: The messiah name + content: + application/json: + schema: + type: object + properties: + messiahName: + type: string + examples: + dune: + value: + messiahName: Lisan Al Gaib +components: + schemas: + Person: + type: object + properties: + lastName: + type: string + firstName: + type: string diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/openapi-oneliner.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/openapi-oneliner.json new file mode 100644 index 000000000..d5dc37cea --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/openapi-oneliner.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"feature-flags-service","description":"Web service to enable feature flag lookup by application and environment.","version":"0.1"}} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/param-refs-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/param-refs-openapi.yaml new file mode 100644 index 000000000..83090d095 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/param-refs-openapi.yaml @@ -0,0 +1,60 @@ +openapi: 3.0.1 +info: + title: Sample API + version: '1.0' +paths: + /accounts/{accountId}: + get: + parameters: + - $ref: "#/components/parameters/accountId" + responses: + 200: + $ref: "#/components/responses/OK_200_AccountDetails" + +components: + parameters: + accountId: + name: accountId + in: path + required: true + schema: + $ref: "#/components/schemas/accountId" + examples: + "Example 1": + value: 396be545-e2d4-4497-a5b5-700e89ab99c0 + + responses: + OK_200_AccountDetails: + description: OK + content: + application/json: + schema: + type: object + required: + - account + properties: + account: + $ref: "#/components/schemas/accountDetails" + examples: + "Example 1": + $ref: "#/components/examples/accountDetailsRegularAccount" + + schemas: + accountId: + type: string + format: uuid + accountDetails: + type: object + properties: + resourceId: + type: string + + examples: + accountDetailsRegularAccount: + value: + { + "account": + { + "resourceId": "f377afb3-5c62-40cc-8f07-1f4749a780eb", + } + } \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-for-proxy-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-for-proxy-openapi.yaml new file mode 100644 index 000000000..022126623 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-for-proxy-openapi.yaml @@ -0,0 +1,91 @@ +--- +openapi: 3.0.2 +info: + title: pastry-real + version: 1.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /pastry: + summary: Global operations on pastries + get: + parameters: + - name: name + in: query + schema: + type: string + examples: + donut: + value: 'donut' + croissant: + value: 'croissant' + tags: + - pastry + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + donut: + value: + name: Real One + croissant: + value: + name: Croissant from Real One + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries + /pastry/{name}: + summary: Global operations on pastries + get: + parameters: + - name: name + in: path + schema: + type: string + examples: + donut: + value: 'donut' + croissant: + value: 'croissant' + tags: + - pastry + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + donut: + value: + name: Real One + croissant: + value: + name: Croissant from Real One + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries +components: + schemas: + Pastry: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + example: + name: My Pastry +tags: +- name: pastry + description: Pastry resource diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-details-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-details-openapi.yaml new file mode 100644 index 000000000..1e7492589 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-details-openapi.yaml @@ -0,0 +1,281 @@ +--- +openapi: 3.0.2 +info: + title: pastry-details + version: 1.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /pastry: + summary: Global operations on pastries + get: + tags: + - pastry + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pastry' + examples: + pastries_json: + value: + - name: Baba Rhum + description: Delicieux Baba au Rhum pas calorique du tout + size: L + price: 3.2 + status: available + - name: Divorces + description: Delicieux Divorces pas calorique du tout + size: M + price: 2.8 + status: available + - name: Tartelette Fraise + description: Delicieuse Tartelette aux Fraises fraiches + size: S + price: 2 + status: available + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries + /pastry/{name}: + summary: Specific operation on pastry + get: + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + Millefeuille: + value: Millefeuille + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.5 + status: available + Millefeuille: + value: + name: Millefeuille + description: Delicieux Millefeuille pas calorique du tout + size: L + price: 4.4 + status: available + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: |- + + Eclair Cafe + Delicieux Eclair au Cafe pas calorique du tout + M + 2.5 + available + + description: Pastry with specified name + operationId: GetPastryByName + summary: Get Pastry by name + description: Get Pastry by name + patch: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + price: 2.6 + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: "\n\t2.6\n" + required: true + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.6 + status: available + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: |- + + Eclair Cafe + Delicieux Eclair au Cafe pas calorique du tout + M + 2.6 + available + + description: Changed pastry + operationId: PatchPastry + summary: Patch existing pastry + parameters: + - name: name + description: pastry name + schema: + type: string + in: path + required: true + /pastry/{name}/details: + summary: Specific operation on pastry + get: + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + Millefeuille: + value: Millefeuille + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/PastryDetails' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Detail - Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.5 + status: available + street: somewhere + city: Earth + Millefeuille: + value: + name: Millefeuille + description: Detail - Delicieux Millefeuille pas calorique du tout + size: L + price: 4.4 + status: available + street: freestreet 3 + city: Paris + description: Pastry details with specified name + operationId: GetPastryDetailsByName + summary: Get Pastry Details by name + description: Get Pastry Details by name + parameters: + - name: name + description: pastry name + schema: + type: string + in: path + required: true +components: + schemas: + Pastry: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + description: + description: A short description of this pastry + type: string + size: + description: Size of pastry (S, M, L) + type: string + price: + format: double + description: Price (in USD) of this pastry + type: number + status: + description: Status in stock (available, out_of_stock) + type: string + example: + name: My Pastry + description: A short description os my pastry + size: M + price: 4.5 + status: available + PastryDetails: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + description: + description: A short description of this pastry + type: string + size: + description: Size of pastry (S, M, L) + type: string + price: + format: double + description: Price (in USD) of this pastry + type: number + status: + description: Status in stock (available, out_of_stock) + type: string + street: + description: the street + type: string + city: + description: the city + type: string +tags: +- name: pastry + description: Pastry resource diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-headers-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-headers-openapi.yaml new file mode 100644 index 000000000..94f3c7c2f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-headers-openapi.yaml @@ -0,0 +1,99 @@ +--- +openapi: 3.0.2 +info: + title: pastry-headers + version: 1.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /pastry: + summary: Global operations on pastries + get: + tags: + - pastry + responses: + "200": + headers: + x-some-generic-header: + schema: + type: string + examples: + pastries_json: + value: '{{ randomUUID() }}' + x-some-static-header: + schema: + type: string + examples: + pastries_json: + value: some-static-header + x-request-based-header: + schema: + type: string + examples: + pastries_json: + value: '{{ request.params[size] }} size' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pastry' + examples: + pastries_json: + value: + - name: Baba Rhum + description: Delicieux Baba au Rhum pas calorique du tout + size: L + price: 3.2 + status: available + - name: Divorces + description: Delicieux Divorces pas calorique du tout + size: M + price: 2.8 + status: available + - name: Tartelette Fraise + description: Delicieuse Tartelette aux Fraises fraiches + size: S + price: 2 + status: available + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries +components: + schemas: + Pastry: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + description: + description: A short description of this pastry + type: string + size: + description: Size of pastry (S, M, L) + type: string + price: + format: double + description: Price (in USD) of this pastry + type: number + status: + description: Status in stock (available, out_of_stock) + type: string + example: + name: My Pastry + description: A short description os my pastry + size: M + price: 4.5 + status: available +tags: +- name: pastry + description: Pastry resource diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-proxy-fallback-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-proxy-fallback-openapi.yaml new file mode 100644 index 000000000..9c6db8637 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-proxy-fallback-openapi.yaml @@ -0,0 +1,63 @@ +--- +openapi: 3.0.2 +info: + title: pastry-proxy + version: 1.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /pastry: + summary: Global operations on pastries + get: + x-microcks-operation: + dispatcher: PROXY_FALLBACK + dispatcherRules: | + { + "dispatcher": "URI_PARAMS", + "dispatcherRules": "name", + "proxyUrl": "http://localhost/rest/pastry-real/1.0.0/" + } + parameters: + - name: name + in: query + schema: + type: string + examples: + donut: + value: 'donut' + tags: + - pastry + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + donut: + value: + name: Mocked One + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries +components: + schemas: + Pastry: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + example: + name: My Pastry +tags: +- name: pastry + description: Pastry resource diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-proxy-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-proxy-openapi.yaml new file mode 100644 index 000000000..63cf175f1 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-proxy-openapi.yaml @@ -0,0 +1,58 @@ +--- +openapi: 3.0.2 +info: + title: pastry-proxy + version: 1.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /pastry/{name}: + summary: Global operations on pastries + get: + x-microcks-operation: + dispatcher: PROXY + dispatcherRules: http://localhost/rest/pastry-real/1.0.0 + parameters: + - name: name + in: path + schema: + type: string + examples: + donut: + value: 'donut' + tags: + - pastry + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + donut: + value: + name: Mocked One + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries +components: + schemas: + Pastry: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + example: + name: My Pastry +tags: +- name: pastry + description: Pastry resource diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-script-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-script-openapi.yaml new file mode 100644 index 000000000..2ef02706b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/pastry-with-script-openapi.yaml @@ -0,0 +1,52 @@ +--- +openapi: 3.0.2 +info: + title: pastry-script + version: 1.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /pastry/{name}/taste: + summary: Specific operation on pastry + get: + x-microcks-operation: + dispatcher: SCRIPT + dispatcherRules: | + def pastryName = mockRequest.getURIParameters().get("name"); + if (pastryName == "Eclair Cafe") { + return "Delicious" + } else if (pastryName == "Millefeuille") { + return "Awesome" + } + return "Ok" + responses: + "200": + content: + text/plain: + examples: + Delicious: + value: "Delicious" + Awesome: + value: "Awesome" + Ok: + value: "Ok" + description: Pastry taste for specified name + operationId: GetPastryTasteByName + summary: Get Pastry taste by name + description: Get Pastry taste by name + parameters: + - name: name + description: pastry name + schema: + type: string + in: path + required: true +tags: + - name: pastry + description: Pastry resource diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-1.0.0-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-1.0.0-openapi.yaml new file mode 100644 index 000000000..a61cbed14 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-1.0.0-openapi.yaml @@ -0,0 +1,131 @@ +openapi: 3.0.2 +info: + title: Petstore API + version: 1.0.0 + description: |- + A sample API that uses a petstore as an example to demonstrate features + in the OpenAPI 3.0 specification and Microcks + contact: + name: Microcks Team + url: 'https://microcks.io' + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +components: + schemas: + Pet: + allOf: + - $ref: '#/components/schemas/NewPet' + - properties: + id: + format: int64 + type: integer + required: + - id + NewPet: + properties: + name: + type: string + required: + - name +paths: + /my/pets: + get: + description: A list of pets owned by the user + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + examples: + my_pets: + value: + - id: 1 + name: Zaza + - id: 2 + name: Tigress + - id: 3 + name: Maki + - id: 4 + name: Toufik + /pets: + get: + description: A list of all pets filtered by name + parameters: + - name: filter + in: query + schema: + type: string + examples: + k_pets: + value: k + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + examples: + k_pets: + value: + - id: 3 + name: Maki + - id: 4 + name: Toufik + post: + summary: Add a new pet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + examples: + new_pet: + value: + name: Jojo + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + examples: + new_pet: + value: |- + { + "id": {{ randomInt(5,10) }}, + "name": "{{ request.body/name }}" + } + /pets/{id}: + get: + description: Get a pet by its ID + parameters: + - name: id + in: path + schema: + type: string + examples: + pet_1: + value: '1' + pet_2: + value: '2' + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + examples: + pet_1: + value: + id: 1 + name: Zaza + pet_2: + value: + id: 2 + name: Tigresse \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-2.0.0-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-2.0.0-openapi.yaml new file mode 100644 index 000000000..2393e3121 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-2.0.0-openapi.yaml @@ -0,0 +1,181 @@ +openapi: 3.0.2 +info: + title: Petstore API + version: 2.0.0 + description: |- + A sample API that uses a petstore as an example to demonstrate features + in the OpenAPI 3.0 specification and Microcks + contact: + name: Microcks Team + url: 'https://microcks.io' + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +components: + schemas: + Pet: + allOf: + - $ref: '#/components/schemas/NewPet' + - properties: + id: + format: int64 + type: integer + required: + - id + NewPet: + properties: + name: + type: string + coat: + $ref: '#/components/schemas/Coat' + bugs: + type: array + items: + $ref: '#/components/schemas/Bug' + required: + - name + Coat: + type: object + properties: + name: + type: string + tint: + type: string + enum: + - light + - dark + required: + - name + Bug: + type: string + enum: + - tick + - flea +paths: + /my/pets: + get: + description: A list of pets owned by the user + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + examples: + my_pets: + value: + - id: 1 + name: Zaza + coat: + name: merle + tint: light + - id: 2 + name: Tigresse + coat: + name: tabby + tint: dark + - id: 3 + name: Maki + coat: + name: calico + tint: dark + - id: 4 + name: Toufik + coat: + name: tabby + tint: dark + /pets: + get: + description: A list of all pets filtered by name + parameters: + - name: filter + in: query + schema: + type: string + examples: + k_pets: + value: k + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + examples: + k_pets: + value: + - id: 3 + name: Maki + coat: + name: calico + tint: dark + - id: 4 + name: Toufik + coat: + name: tabby + tint: dark + post: + summary: Add a new pet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + examples: + new_pet: + value: + name: Jojo + coat: + name: tabby + tint: light + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + examples: + new_pet: + value: |- + { + "id": {{ randomInt(5,10) }}, + "name": "{{ request.body/name }}" + } + /pets/{id}: + get: + description: Get a pet by its ID + parameters: + - name: id + in: path + schema: + type: string + examples: + pet_1: + value: '1' + pet_2: + value: '2' + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + examples: + pet_1: + value: + id: 1 + name: Zaza + coat: + name: merle + tint: light + pet_2: + value: + id: 2 + name: Tigresse + coat: + name: tabby + tint: dark \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-openapi.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-openapi.json new file mode 100644 index 000000000..e3eb91f1a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/petstore-openapi.json @@ -0,0 +1,307 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "PetStore API", + "version": "1.0.0", + "description": "Dummy API for PoC", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team", + "url": "http://swagger.io", + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/api" + } + ], + "paths": { + "/pets": { + "get": { + "parameters": [ + { + "style": "form", + "name": "tags", + "description": "tags to filter by", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "required": false + }, + { + "name": "limit", + "description": "maximum number of results to return", + "schema": { + "format": "int32", + "type": "integer" + }, + "in": "query", + "required": false + } + ], + "responses": { + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "unexpected error" + }, + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "examples": { + "laurent_cats": { + "value": [ + { + "id": 1, + "name": "Zaza", + "tag": "cat" + }, + { + "id": 2, + "name": "Tigresse", + "tag": "cat" + }, + { + "id": 3, + "name": "Maki", + "tag": "cat" + }, + { + "id": 4, + "name": "Toufik", + "tag": "cat" + } + ] + } + } + } + }, + "description": "pet response" + } + }, + "operationId": "findPets", + "description": "Returns all pets from the system that the user has access to\n" + }, + "post": { + "requestBody": { + "description": "Pet to add to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + }, + "examples": { + "tigresse": { + "value": { + "name": "Tigresse", + "tag": "cat" + } + } + } + } + }, + "required": true + }, + "responses": { + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "unexpected error" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + }, + "examples": { + "tigresse": { + "value": { + "id": 2, + "name": "Tigresse", + "tag": "cat" + } + } + } + } + }, + "description": "pet response" + } + }, + "operationId": "addPet", + "description": "Creates a new pet in the store. Duplicates are allowed" + } + }, + "/pets/{id}": { + "get": { + "parameters": [ + { + "examples": { + "zaza": { + "value": 1 + } + }, + "name": "id", + "description": "ID of pet to fetch", + "schema": { + "format": "int64", + "type": "integer" + }, + "in": "path", + "required": true + } + ], + "responses": { + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "unexpected error" + }, + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + }, + "examples": { + "zaza": { + "value": { + "id": 1, + "name": "Zaza", + "tag": "cat" + } + } + } + } + }, + "description": "pet response" + } + }, + "operationId": "findPetById", + "description": "Returns a user based on a single ID, if the user does not have\naccess to the pet" + }, + "delete": { + "parameters": [ + { + "name": "id", + "description": "ID of pet to delete", + "schema": { + "format": "int64", + "type": "integer" + }, + "in": "path", + "required": true + } + ], + "responses": { + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "unexpected error" + }, + "204": { + "description": "pet deleted" + } + }, + "operationId": "deletePet", + "description": "deletes a single pet based on the ID supplied" + }, + "parameters": [ + { + "name": "id", + "description": "Pet identifier", + "schema": { + "type": "integer" + }, + "in": "path", + "required": true + } + ] + } + }, + "components": { + "schemas": { + "Pet": { + "allOf": [ + { + "$ref": "#/components/schemas/NewPet" + }, + { + "required": [ + "id" + ], + "properties": { + "id": { + "format": "int64", + "type": "integer" + } + } + } + ] + }, + "NewPet": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Error": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/query-param-refs-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/query-param-refs-openapi.yaml new file mode 100644 index 000000000..fb5bdaa30 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/query-param-refs-openapi.yaml @@ -0,0 +1,130 @@ +openapi: "3.0.0" +info: + title: API-Template + version: 1.0.0 + +servers: + - url: https://dev-api.openfinance.es/api-name/v1 + description: Development server + - url: https://api.openfinance.es/api-name/v1 + description: Production server +tags: + - name: examples + description: 'Examples for Microcks.io' + +paths: + # Example with only one query parameter + /resources: + get: + tags: [examples] + summary: Get resources by query + description: Get a list of resources by query + operationId: queryOp + parameters: + - name: resourceType + in: query + description: Get a list of resources by type + schema: + type: string + enum: [standard, premium] + examples: + standard_example: + value: 'standard' + premium_example: + value: 'premium' + responses: + '200': + description: List of resources + content: + application/json: + schema: + properties: + name: + type: string + resourceType: + type: string + enum: [standard, premium] + examples: + standard_example: + value: + - name: 'Resource One' + resourceType: 'standard' + premium_example: + value: + - name: 'Resource Two' + resourceType: 'premium' + all_example: + value: + - name: 'Resource One' + resourceType: 'standard' + - name: 'Resource Two' + resourceType: 'premium' + # Example with problem in URI_PARAMS + /accounts: + get: + tags: [examples] + summary: Get resources by multiple query params + description: Get a list of resources by multiple query params + operationId: dobleQueryOp + parameters: + - $ref: '#/components/parameters/levelQueryParam' + responses: + '200': + description: List of accounts + content: + application/json: + schema: + properties: + accountID: + type: string + level: + type: string + enum: [bronze, golden] + examples: + bronze_example: + value: + - accountID: AC001 + level: bronze + region: national + - accountID: AC002 + level: bronze + region: international + golden_example: + value: + - accountID: AC003 + level: golden + region: national + - accountID: AC004 + level: golden + region: international + all_example: + value: + - accountID: AC001 + level: bronze + region: national + - accountID: AC002 + level: bronze + region: international + - accountID: AC003 + level: golden + region: national + - accountID: AC004 + level: golden + region: international + +components: + parameters: + levelQueryParam: + name: level + in: query + description: Account level + schema: + type: string + enum: [bronze, golden] + examples: + bronze_example: + value: 'bronze' + golden_example: + value: 'golden' + all_example: + value: 'all' diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/simple-oidc-redirect-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/simple-oidc-redirect-openapi.yaml new file mode 100644 index 000000000..ad579dfca --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/simple-oidc-redirect-openapi.yaml @@ -0,0 +1,70 @@ +openapi: 3.1.0 +info: + title: Simple OIDC + description: Specification of a simple OpenID Connect identity provider. + version: 1.0 +paths: + /login/oauth/authorize: + get: + x-microcks-operation: + dispatcher: FALLBACK + dispatcherRules: |- + { + "dispatcher": "URI_PARAMS", + "dispatcherRules": "response_type", + "fallback": "generic" + } + parameters: + - name: response_type + in: query + description: Expected response type + schema: + type: string + examples: + generic: + value: code + - name: client_id + in: query + description: The client identifier for the OAuth 2.0 client that the token was issued to. + schema: + type: string + examples: + generic: + value: GHCLIENT + - name: scope + in: query + description: String containing a plus-separated list of scope values + schema: + type: string + examples: + generic: + value: openid+user:email + - name: state + in: query + description: Client state that should appear in redirect directive + schema: + type: string + examples: + generic: + value: e956e017-5e13-4c9d-b83b-6dd6337a6a86 + - name: redirect_uri + in: query + description: Redirect to this URI after successfull authorization + schema: + type: string + format: uri + examples: + generic: + value: http://localhost:8080/Login/githubLoginSuccess + responses: + '302': + description: Redirect + x-microcks-refs: + - generic + headers: + 'Location': + schema: + type: string + examples: + generic: + value: "{{ request.params[redirect_uri] }}?state={{ request.params[state] }}&code={{ uuid() }}" diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-json.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-json.json new file mode 100644 index 000000000..044afe41d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-json.json @@ -0,0 +1,103 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Test OPENAPI", + "version": "1.0.0" + }, + "paths": { + "/tests": { + "get": { + "operationId": "ListTests", + "responses": { + "200": { + "description": "Get Tests", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Test" + } + }, + "examples": { + "simple": { + "value": [ + { + "foo": "some text", + "bar": 11 + }, + { + "foo": "some text", + "bar": 35 + } + ] + } + } + } + } + } + } + } + }, + "/tests/{id}": { + "get": { + "operationId": "GetTest", + "responses": { + "200": { + "description": "Get Test", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Test" + }, + "examples": { + "single": { + "value": { + "foo": "some text", + "bar": 55 + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Test id", + "required": true, + "schema": { + "type": "string" + }, + "examples": { + "single": { + "value": 55 + } + } + } + ] + } + }, + "components": { + "schemas": { + "Test": { + "title": "Root Type for Test", + "description": "The root of the Test type's schema.", + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "format": "int32", + "type": "integer" + } + }, + "example": "{\n \"foo\": \"Test string\",\n \"bar\": 123\n}" + } + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-nocontent.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-nocontent.yaml new file mode 100644 index 000000000..a52329850 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-nocontent.yaml @@ -0,0 +1,99 @@ +--- +openapi: 3.0.2 +info: + title: Test API + description: Description for Test API + version: 1.0.0 +paths: + /tests: + get: + operationId: ListTests + responses: + 200: + description: Get Tests + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Test' + examples: + simple: + value: |- + [ + { + "foo": "some text", + "bar": 11 + }, + { + "foo": "some text", + "bar": 35 + } + ] + /tests/{id}: + get: + operationId: GetTest + responses: + 200: + description: Get Test + content: + application/json: + schema: + $ref: '#/components/schemas/Test' + examples: + single: + value: |- + { + "foo": "some text", + "bar": 55 + } + delete: + operationId: DeleteTest + requestBody: + content: + application/json: + schema: + type: string + examples: + to-delete-2: + value: "some string that shouldn't be there" + responses: + 204: + description: No Content + x-microcks-refs: + - to-delete-1 + 418: + description: Not Authorized + x-microcks-refs: + - to-delete-2 + parameters: + - name: id + in: path + description: Test id + required: true + schema: + type: string + examples: + single: + value: 55 + to-delete-1: + value: 66 + to-delete-2: + value: 77 +components: + schemas: + Test: + title: Root Type for Test + description: The root of the Test type's schema. + type: object + properties: + foo: + type: string + bar: + format: int32 + type: integer + example: |- + { + "foo": "Test string", + "bar": 123 + } diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-yaml.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-yaml.yaml new file mode 100644 index 000000000..8c04ca254 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi-yaml.yaml @@ -0,0 +1,67 @@ +--- +openapi: 3.0.2 +info: + title: Test OPENAPI + version: 1.0.0 +paths: + /tests: + get: + operationId: ListTests + responses: + 200: + description: Get Tests + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Test' + examples: + simple: + value: + - foo: some text + bar: 11 + - foo: some text + bar: 35 + /tests/{id}: + get: + operationId: GetTest + responses: + 200: + description: Get Test + content: + application/json: + schema: + $ref: '#/components/schemas/Test' + examples: + single: + value: + foo: some text + bar: 55 + parameters: + - name: id + in: path + description: Test id + required: true + schema: + type: string + examples: + single: + value: 55 +components: + schemas: + Test: + title: Root Type for Test + description: The root of the Test type's schema. + type: object + properties: + foo: + type: string + bar: + format: int32 + type: integer + example: |- + { + "foo": "Test string", + "bar": 123 + } diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi.json new file mode 100644 index 000000000..a8f063ba6 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi.json @@ -0,0 +1,92 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Test API", + "description": "Description for Test API", + "version": "1.0.0" + }, + "paths": { + "/tests": { + "get": { + "operationId": "ListTests", + "responses": { + "200": { + "description": "Get Tests", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Test" + } + }, + "examples": { + "simple": { + "value": "[\n {\n \"foo\": \"some text\",\n \"bar\": 11\n },\n {\n \"foo\": \"some text\",\n \"bar\": 35\n }\n]" + } + } + } + } + } + } + } + }, + "/tests/{id}": { + "get": { + "operationId": "GetTest", + "responses": { + "200": { + "description": "Get Test", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Test" + }, + "examples": { + "single": { + "value": "{\n \"foo\": \"some text\",\n \"bar\": 55\n }" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Test id", + "required": true, + "schema": { + "type": "string" + }, + "examples": { + "single": { + "value": 55 + } + } + } + ] + } + }, + "components": { + "schemas": { + "Test": { + "title": "Root Type for Test", + "description": "The root of the Test type's schema.", + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "format": "int32", + "type": "integer" + } + }, + "example": "{\n \"foo\": \"Test string\",\n \"bar\": 123\n}" + } + } + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi.yaml new file mode 100644 index 000000000..7ddec81da --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/test-openapi.yaml @@ -0,0 +1,76 @@ +--- +openapi: 3.0.2 +info: + title: Test API + description: Description for Test API + version: 1.0.0 +paths: + /tests: + get: + operationId: ListTests + responses: + 200: + description: Get Tests + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Test' + examples: + simple: + value: |- + [ + { + "foo": "some text", + "bar": 11 + }, + { + "foo": "some text", + "bar": 35 + } + ] + /tests/{id}: + get: + operationId: GetTest + responses: + 200: + description: Get Test + content: + application/json: + schema: + $ref: '#/components/schemas/Test' + examples: + single: + value: |- + { + "foo": "some text", + "bar": 55 + } + parameters: + - name: id + in: path + description: Test id + required: true + schema: + type: string + examples: + single: + value: 55 +components: + schemas: + Test: + title: Root Type for Test + description: The root of the Test type's schema. + type: object + properties: + foo: + type: string + bar: + format: int32 + type: integer + example: |- + { + "foo": "Test string", + "bar": 123 + } diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-examples.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-examples.json new file mode 100644 index 000000000..73a164f0f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-examples.json @@ -0,0 +1,14 @@ +[ + { + "region": "east", + "temp": -6.6, + "weather": "frosty", + "visibility": 523 + }, + { + "region": "south", + "temp": 28.3, + "weather": "sunny", + "visibility": 1500 + } +] diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-common-regions.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-common-regions.yaml new file mode 100644 index 000000000..2b0c859b7 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-common-regions.yaml @@ -0,0 +1,24 @@ +info: + title: Common regions objects to reuse + description: Common regions objects to reuse +regions: + north: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-common.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-common.yaml new file mode 100644 index 000000000..e991cbedf --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-common.yaml @@ -0,0 +1,23 @@ +info: + title: Common objects to reuse + description: Common objects to reuse + type: object +parameters: + region: + name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-examples.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-examples.yaml new file mode 100644 index 000000000..3f48b132d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-examples.yaml @@ -0,0 +1,12 @@ +north: + value: + $ref: './weather-forecast-common-regions.yaml#/regions/north' +west: + value: + $ref: './weather-forecast-common-regions.yaml#/regions/west' +east: + value: + $ref: './weather-forecast-common-regions.yaml#/regions/east' +south: + value: + $ref: './weather-forecast-common-regions.yaml#/regions/south' diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref-pointers.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref-pointers.yaml new file mode 100644 index 000000000..7888c2125 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref-pointers.yaml @@ -0,0 +1,67 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - $ref: 'https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-common.yaml#/parameters/region' + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: 'https://raw.githubusercontent.com/microcks/microcks/1.8.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml' + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + value: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + value: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref.yaml new file mode 100644 index 000000000..c37b73823 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-absolute-ref.yaml @@ -0,0 +1,83 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: 'https://raw.githubusercontent.com/microcks/microcks/1.5.x/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml' + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + value: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + value: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-recursive-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-recursive-ref.yaml new file mode 100644 index 000000000..12981ae0c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-recursive-ref.yaml @@ -0,0 +1,60 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: './weather-forecast-schema.yaml' + examples: + $ref: './weather-forecast-examples.yaml' + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref-example.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref-example.yaml new file mode 100644 index 000000000..dc29c373e --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref-example.yaml @@ -0,0 +1,83 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: './weather-forecast-schema.yaml' + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + $ref: '#/components/examples/eastExample' + south: + $ref: '#/components/examples/southExample' + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." +components: + examples: + eastExample: + value: + $ref: './weather-examples.json#/0' + southExample: + value: + $ref: './weather-examples.json#/1' \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml new file mode 100644 index 000000000..839f5fbfb --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-openapi-relative-ref.yaml @@ -0,0 +1,83 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.0.0 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast/{region}: + get: + operationId: GetForecast + summary: Get forecast for region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: path + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + - name: apiKey + description: Client API key + schema: + type: string + in: query + required: true + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + $ref: './weather-forecast-schema.yaml' + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + value: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + value: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.json new file mode 100644 index 000000000..c28f58f0f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.json @@ -0,0 +1,27 @@ +{ + "title": "Root Type for Forecast", + "description": "A weather forecast for a requested region", + "type": "object", + "properties": { + "region": { + "type": "string" + }, + "temp": { + "format": "double", + "type": "number" + }, + "weather": { + "type": "string" + }, + "visibility": { + "format": "int32", + "type": "integer" + } + }, + "example": { + "region": "west", + "temp": 25.2, + "weather": "cloudy", + "visibility": 1000 + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml new file mode 100644 index 000000000..dabd98c85 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-schema.yaml @@ -0,0 +1,19 @@ +title: Root Type for Forecast +description: A weather forecast for a requested region +type: object +properties: + region: + type: string + temp: + format: double + type: number + weather: + type: string + visibility: + format: int32 + type: integer +example: + region: west + temp: 25.2 + weather: cloudy + visibility: 1000 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-with-header.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-with-header.yaml new file mode 100644 index 000000000..0c7699472 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/openapi/weather-forecast-with-header.yaml @@ -0,0 +1,98 @@ +--- +openapi: 3.0.2 +info: + title: WeatherForecast API + version: 1.1.12 + description: A simple API for demonstrating dispatching capabilities in Microcks + contact: + name: Laurent Broudoux + url: https://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /forecast: + get: + operationId: GetForecast + summary: Get forecast for region + x-microcks-operation: + dispatcher: QUERY_HEADER + dispatcherRules: region + parameters: + - name: region + description: The region to get forecast for + schema: + type: string + in: header + required: true + examples: + unknown: + value: other + north: + value: north + west: + value: west + east: + value: east + south: + value: south + responses: + "200": + description: Weather forecast for region + content: + application/json: + schema: + title: Root Type for Forecast + description: A weather forecast for a requested region + type: object + properties: + region: + type: string + temp: + format: double + type: number + weather: + type: string + visibility: + format: int32 + type: integer + example: + region: west + temp: 25.2 + weather: cloudy + visibility: 1000 + examples: + north: + value: + region: north + temp: -1.5 + weather: snowy + visibility: 25 + west: + value: + region: west + temp: 12.2 + weather: rainy + visibility: 300 + east: + value: + region: east + temp: -6.6 + weather: frosty + visibility: 523 + south: + value: + region: south + temp: 28.3 + weather: sunny + visibility: 1500 + "404": + description: Region is unknown + content: + application/json: + schema: + type: string + examples: + unknown: + value: "Region is unknown. Choose in north, west, east or south." \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/DBAPI.postman_collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/DBAPI.postman_collection.json new file mode 100644 index 000000000..0b603d90c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/DBAPI.postman_collection.json @@ -0,0 +1,1034 @@ +{ + "variables": [], + "info": { + "name": "dbapi", + "_postman_id": "7ab13fcd-bfbf-c316-4206-b0ac7bab9561", + "description": "version=1.0 - dbAPI", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "Reads all addresses of the current user.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/addresses", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": {}, + "description": "Reads all addresses of the current user. There might be several private and business addresses. AddressType can be BUSINESS_ADDRESS or PRIVATE_ADDRESS. Country format is ISO code." + }, + "response": [ + { + "id": "7c83bfb4-294a-4b2a-85fb-21dad6e96596", + "name": "Reads all addresses of the current user.", + "originalRequest": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/addresses", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTk3MTUsImlhdCI6MTUxMzExOTExNiwianRpIjoiMzA4Nzc3NWItMzc3Zi00MGVjLWE2NDYtNjdkOGU1ZDJkZWI2In0.ElB0hmtcAWpG325D6kQ6Efg_AgaGWZ70N6MQ39PiAX9DlEanFsNmXWQ6luwth0K7P7F2Acpy4aXNGs28WOI8Km7OuGScB6PWkk2ecZcS88ln5_yMLyjKdNF5mzv-egb2wILglnhOWK38ZsyH1m0pJXOQ07YmPTnY8ZOIzCOmTwoK86DzxHkoti3QxSstp_opUwhD4gyC88DdwVZghC6hL0qDA9yVidqQBiWdVRNYbpKzvCGpjhLL4Zsac81KUa91fj7hkftZnIjj7gB3Q0Wac3SEzKYJDVhlArIfo4nZTo2iGZZhBV81YgbCSjn6JwhMDGsxUsEShP-L7jx91Ad34Q", + "description": "" + } + ], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "Tells all caching mechanisms from server to client whether they may cache this object. It is measured in seconds" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "Options that are desired for the connection" + }, + { + "name": "content-length", + "key": "content-length", + "value": "305", + "description": "The length of the response body in octets (8-bit bytes)" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:52:22 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "Custom header" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "Gives the date/time after which the response is considered stale" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=100", + "description": "Custom header" + }, + { + "name": "message_id", + "key": "message_id", + "value": "b1aa6de3-d563-43bc-888e-91e3376e36c6", + "description": "Custom header" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "A name for the server" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "Custom header" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "Cross-site scripting (XSS) filter" + } + ], + "cookie": [], + "responseTime": 367, + "body": "[{\"street\":\"Am Sandtorkai\",\"houseNumber\":\"4\",\"zip\":\"20457\",\"city\":\"Hamburg\",\"country\":\"DEU\",\"addressType\":\"PRIVATE_ADDRESS\",\"registeredResidence\":true},{\"street\":\"Am Sandtorkai\",\"houseNumber\":\"4\",\"zip\":\"20457\",\"city\":\"Hamburg\",\"country\":\"DEU\",\"addressType\":\"BUSINESS_ADDRESS\",\"registeredResidence\":false}]" + } + ] + }, + { + "name": "Reads all addresses of the current user.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v2/addresses", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": {}, + "description": "Reads all addresses of the current user. There might be several private and business addresses. AddressType can be BUSINESS_ADDRESS or PRIVATE_ADDRESS. Country format is ISO code." + }, + "response": [] + }, + { + "name": "Retrieves personal information about the current partner.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v2/partners", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": {}, + "description": "Retrieves personal information (e.g. first name, last name, date of birth) about the current partner." + }, + "response": [] + }, + { + "name": "Reads all transactions of the current cash account.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v2/transactions?iban={{iban}}¤cyCode={{currencyCode}}&bookingDateFrom={{bookingDateFrom}}&bookingDateTo={{bookingDateTo}}&sortBy=bookingDate[ASC]&limit=10&offset=0", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": {}, + "description": "Reads all transactions for a specific account of the current user. If given IBAN is not valid or does not represent an account of the current user, an empty result is returned. It is not apparent who issued a transaction, only whether the user gained or lost money by it (based on whether the amount is positive or negative respectively). The maximum number of transactions returned is 200." + }, + "response": [] + }, + { + "name": "Reads all cash accounts of the current user.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v2/cashAccounts?iban={{iban}}¤cyCode={{currencyCode}}&limit=10&offset=0", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": {}, + "description": "Reads all cash accounts of the current user. Only current accounts and accounts in the currency EUR are returned. If given IBAN is not valid or does not represent an account of the current user, an empty result is returned." + }, + "response": [] + }, + { + "name": "Reads all cash accounts of the current user.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/cashAccounts?iban={{iban}}¤cyCode={{currencyCode}}", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": {}, + "description": "Reads all cash accounts of the current user. Only current accounts and accounts in the currency EUR are returned. If given IBAN is not valid or does not represent an account of the current user, an empty result is returned." + }, + "response": [ + { + "id": "c0a0e9c5-f026-4411-871e-fedcbd34fb3d", + "name": "Reads all cash accounts of the current user.", + "originalRequest": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/cashAccounts?iban=DE10000000000000002783", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "cashAccounts" + ], + "query": [ + { + "key": "iban", + "value": "DE10000000000000002783", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTk3MTUsImlhdCI6MTUxMzExOTExNiwianRpIjoiMzA4Nzc3NWItMzc3Zi00MGVjLWE2NDYtNjdkOGU1ZDJkZWI2In0.ElB0hmtcAWpG325D6kQ6Efg_AgaGWZ70N6MQ39PiAX9DlEanFsNmXWQ6luwth0K7P7F2Acpy4aXNGs28WOI8Km7OuGScB6PWkk2ecZcS88ln5_yMLyjKdNF5mzv-egb2wILglnhOWK38ZsyH1m0pJXOQ07YmPTnY8ZOIzCOmTwoK86DzxHkoti3QxSstp_opUwhD4gyC88DdwVZghC6hL0qDA9yVidqQBiWdVRNYbpKzvCGpjhLL4Zsac81KUa91fj7hkftZnIjj7gB3Q0Wac3SEzKYJDVhlArIfo4nZTo2iGZZhBV81YgbCSjn6JwhMDGsxUsEShP-L7jx91Ad34Q", + "description": "" + } + ], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "Tells all caching mechanisms from server to client whether they may cache this object. It is measured in seconds" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "Options that are desired for the connection" + }, + { + "name": "content-length", + "key": "content-length", + "value": "156", + "description": "The length of the response body in octets (8-bit bytes)" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:52:49 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "Custom header" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "Gives the date/time after which the response is considered stale" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=100", + "description": "Custom header" + }, + { + "name": "message_id", + "key": "message_id", + "value": "ee226b69-f578-4f8d-b159-551e5ff928e7", + "description": "Custom header" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "A name for the server" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "Custom header" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "Cross-site scripting (XSS) filter" + } + ], + "cookie": [], + "responseTime": 285, + "body": "[{\"iban\":\"DE10000000000000002783\",\"currencyCode\":\"EUR\",\"accountType\":\"CURRENT_ACCOUNT\",\"currentBalance\":1400.95,\"productDescription\":\"persönliches Konto\"}]" + } + ] + }, + { + "name": "Create a processing order", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/processingOrders", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "multipart/form-data", + "description": "" + }, + { + "key": "Idempotency-ID", + "value": "{{Idempotency-ID}}", + "description": "" + }, + { + "key": "Process-ID", + "value": "{{Process-ID}}", + "description": "" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "processingOrder", + "value": "{{processingOrder}}", + "type": "text" + }, + { + "key": "documentData", + "value": "{{documentData}}", + "type": "text" + } + ] + }, + "description": "Create a processing order. This endpoint has limited access for special consumers only. It’s possible to start manual processes only." + }, + "response": [] + }, + { + "name": "Reads all transactions of the current cash account.", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/transactions?iban={{iban}}¤cyCode={{currencyCode}}&bookingDateFrom={{bookingDateFrom}}&bookingDateTo={{bookingDateTo}}&sortBy={{sortBy}}", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "transactions" + ], + "query": [ + { + "key": "iban", + "value": "{{iban}}", + "equals": true, + "description": "" + }, + { + "key": "currencyCode", + "value": "{{currencyCode}}", + "equals": true, + "description": "" + }, + { + "key": "bookingDateFrom", + "value": "{{bookingDateFrom}}", + "equals": true, + "description": "" + }, + { + "key": "bookingDateTo", + "value": "{{bookingDateTo}}", + "equals": true, + "description": "" + }, + { + "key": "sortBy", + "value": "{{sortBy}}", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTI2NDg0NTcsImlhdCI6MTUxMjY0Nzg1NywianRpIjoiOGRjOTUzZjYtZGFhNS00NmYyLWFiMDYtMjUxNzQ0YzgxYmIxIn0.bhDoU7MPk5D9WaPIJHnYAnikR0DTquc6m9QygkmYm1PqoSBnJTL80A60X9Jv0Ylq4mwxykGx9XvlTuhzlHgaD1TyAgpQrCjJeUEr1cgscppFyLd7G71BJWoRHGx-AZFruN9hDZUMSyVh5i_iN5EJ4Zlsoc79xwYn-PNJyuC2v6tsnKHc7BS3su69hZCPKKUvLDeLkBmkc6NhtlQJ4dvJ9-NAnrR5HnESUDrWQqhbKWh_p4ra7wa66GmsaBAZVNzFKLB2mestDCfQAczmA2n745m6fq2XqHdPe8q02eZCM72_PfkfiI6vCkhg00C1EV-QWwcMskhanURsCHlQNIlHTw", + "description": "" + } + ], + "body": {}, + "description": "Reads all transactions for a specific account of the current user. If given IBAN is not valid or does not represent an account of the current user, an empty result is returned. It is not apparent who issued a transaction, only whether the user gained or lost money by it (based on whether the amount is positive or negative respectively). The maximum number of transactions returned is 200." + }, + "response": [ + { + "id": "cab9e78f-ee07-4880-8a4c-fe9d9e251080", + "name": "Reads all transactions of the current cash account.", + "originalRequest": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/transactions?iban=DE10000000000000002783", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "transactions" + ], + "query": [ + { + "key": "iban", + "value": "DE10000000000000002783", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "type": "text", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTk3MTUsImlhdCI6MTUxMzExOTExNiwianRpIjoiMzA4Nzc3NWItMzc3Zi00MGVjLWE2NDYtNjdkOGU1ZDJkZWI2In0.ElB0hmtcAWpG325D6kQ6Efg_AgaGWZ70N6MQ39PiAX9DlEanFsNmXWQ6luwth0K7P7F2Acpy4aXNGs28WOI8Km7OuGScB6PWkk2ecZcS88ln5_yMLyjKdNF5mzv-egb2wILglnhOWK38ZsyH1m0pJXOQ07YmPTnY8ZOIzCOmTwoK86DzxHkoti3QxSstp_opUwhD4gyC88DdwVZghC6hL0qDA9yVidqQBiWdVRNYbpKzvCGpjhLL4Zsac81KUa91fj7hkftZnIjj7gB3Q0Wac3SEzKYJDVhlArIfo4nZTo2iGZZhBV81YgbCSjn6JwhMDGsxUsEShP-L7jx91Ad34Q", + "description": "" + } + ], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "Tells all caching mechanisms from server to client whether they may cache this object. It is measured in seconds" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "Options that are desired for the connection" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:52:02 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "Custom header" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "Gives the date/time after which the response is considered stale" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=100", + "description": "Custom header" + }, + { + "name": "message_id", + "key": "message_id", + "value": "37ea2cfb-8718-4e1f-bbb1-d8a027264d6e", + "description": "Custom header" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "A name for the server" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." + }, + { + "name": "transfer-encoding", + "key": "transfer-encoding", + "value": "chunked", + "description": "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity." + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "Custom header" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "Cross-site scripting (XSS) filter" + } + ], + "cookie": [], + "responseTime": 345, + "body": "[{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-05-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-52.50,\"counterPartyName\":\"GEZ\",\"paymentReference\":\"02/2016\",\"bookingDate\":\"2017-05-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-4.99,\"counterPartyName\":\"Kaffeestand Hamburg HBF\",\"paymentReference\":\"POS MIT PIN. Rechnung 56791873\",\"bookingDate\":\"2017-05-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-16.24,\"counterPartyName\":\"L'Osteria Frankfurt HBF\",\"paymentReference\":\"POS MIT PIN. rechnungs Nr. 21665723123\",\"bookingDate\":\"2017-05-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-123.46,\"counterPartyName\":\"Deutsche Bahn AG\",\"paymentReference\":\"POS MIT PIN. Ticket Hamburg-Freiburg\",\"bookingDate\":\"2017-05-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-05-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.53,\"counterPartyName\":\"I do! Die Hochzeitsmesse\",\"paymentReference\":\"POS MIT PIN. Ticket für die I do! Freiburg\",\"bookingDate\":\"2017-05-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-10.23,\"counterPartyName\":\"Subway\",\"paymentReference\":\"POS MIT PIN. Eat Fresh!\",\"bookingDate\":\"2017-05-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-9.95,\"counterPartyName\":\"Deutsche Bahn Bordbistro\",\"paymentReference\":\"POS MIT PIN. 3453463567\",\"bookingDate\":\"2017-05-14\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.53,\"counterPartyName\":\"Deutsche Bahn AG\",\"paymentReference\":\"POS MIT PIN. Ticket Freiburg-Hamburg\",\"bookingDate\":\"2017-05-14\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-125.89,\"counterPartyName\":\"InterCityHotel Freiburg\",\"paymentReference\":\"POS MIT PIN. Rechnungsnummer 2353462462457\",\"bookingDate\":\"2017-05-14\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-8.86,\"counterPartyName\":\"Deutsche Telekom AG\",\"paymentReference\":\"Hotspot 1 Tag\",\"bookingDate\":\"2017-05-14\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.53,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-05-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.53,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-05-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-05-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.33,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-05-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-05-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-51.22,\"counterPartyName\":\"PayPal Europe\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Ebay 661165\",\"bookingDate\":\"2017-05-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-05-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-05-25\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-45.62,\"counterPartyName\":\"Netto\",\"paymentReference\":\"Einkauf\",\"bookingDate\":\"2017-05-25\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-31.05,\"counterPartyName\":\"REWE\",\"paymentReference\":\"POS MIT PIN. Innenstadt\",\"bookingDate\":\"2017-05-28\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-06-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-06-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-06-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-51.22,\"counterPartyName\":\"Netto\",\"paymentReference\":\"Einkauf\",\"bookingDate\":\"2017-06-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-300.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Altona\",\"bookingDate\":\"2017-06-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-06-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":50.00,\"counterPartyName\":\"hochzeitfoto.de\",\"paymentReference\":\"Storno Rechnung 76543\",\"bookingDate\":\"2017-06-25\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-32.68,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-06-25\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-21.25,\"counterPartyName\":\"REWE\",\"paymentReference\":\"POS MIT PIN. Innenstadt\",\"bookingDate\":\"2017-06-26\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-15.56,\"counterPartyName\":\"PayPal Europe\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Ebay 6658765\",\"bookingDate\":\"2017-07-06\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-07-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-07-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-12.67,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-07-16\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-07-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-150.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Altona\",\"bookingDate\":\"2017-07-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-15.85,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-07-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-29.92,\"counterPartyName\":\"REWE\",\"paymentReference\":\"POS MIT PIN. Innenstadt\",\"bookingDate\":\"2017-07-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-37.95,\"counterPartyName\":\"Aldi Nord\",\"paymentReference\":\"POS MIT PIN. Hamburg Mitte\",\"bookingDate\":\"2017-07-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-07-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-52.50,\"counterPartyName\":\"GEZ\",\"paymentReference\":\"03/2016\",\"bookingDate\":\"2017-08-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-08-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-13.45,\"counterPartyName\":\"Kaisers Tengelmann\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-08-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-12.67,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-08-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-08-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-300.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-08-17\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-22.24,\"counterPartyName\":\"PayPal Europe\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Ebay 667865\",\"bookingDate\":\"2017-08-18\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-08-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-300.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-08-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-08-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-09-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-13.45,\"counterPartyName\":\"Kaisers Tengelmann\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-37.95,\"counterPartyName\":\"Aldi Nord\",\"paymentReference\":\"POS MIT PIN. Hamburg Mitte\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-12.67,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-149.00,\"counterPartyName\":\"Zalando Online\",\"paymentReference\":\"RCHNR 4565570\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-09-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-09-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-300.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Altona\",\"bookingDate\":\"2017-10-02\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-72.23,\"counterPartyName\":\"hochzeitideal.de\",\"paymentReference\":\"REF 56778990\",\"bookingDate\":\"2017-10-04\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-36.96,\"counterPartyName\":\"Aldi Nord\",\"paymentReference\":\"POS MIT PIN. Hamburg Mitte\",\"bookingDate\":\"2017-10-05\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-10-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-10-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-851.00,\"counterPartyName\":\"Air China\",\"paymentReference\":\"HAM-HKG\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1069.00,\"counterPartyName\":\"Hainan Airlines\",\"paymentReference\":\"HKG-HAM\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-675.67,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Withdrawl HK, Wedding Market ATM\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-15.21,\"counterPartyName\":\"Netto\",\"paymentReference\":\"Einkauf\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-59.99,\"counterPartyName\":\"Douglas\",\"paymentReference\":\"POS mit PIN\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-33.85,\"counterPartyName\":\"Kaisers Tengelmann\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-10-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-175.98,\"counterPartyName\":\"Karstadt AG\",\"paymentReference\":\"POS mit PIN\",\"bookingDate\":\"2017-11-04\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-52.50,\"counterPartyName\":\"GEZ\",\"paymentReference\":\"04/2016\",\"bookingDate\":\"2017-11-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-11-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-11-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-155.23,\"counterPartyName\":\"hochzeitideal.de\",\"paymentReference\":\"REF 56981990\",\"bookingDate\":\"2017-11-18\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-50.00,\"counterPartyName\":\"hochzeitfoto.de\",\"paymentReference\":\"Rechnung 76543\",\"bookingDate\":\"2017-11-18\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-115.95,\"counterPartyName\":\"Netto\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-11-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-13.45,\"counterPartyName\":\"Kaisers Tengelmann\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-11-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-88.22,\"counterPartyName\":\"PayPal Europe\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Ebay 666665\",\"bookingDate\":\"2017-11-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-11-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-11-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-400.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-11-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-199.00,\"counterPartyName\":\"Zalando Online\",\"paymentReference\":\"RCHNR 4567890\",\"bookingDate\":\"2017-12-02\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-175.98,\"counterPartyName\":\"Karstadt AG\",\"paymentReference\":\"POS mit PIN\",\"bookingDate\":\"2017-12-06\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"}]" + } + ] + }, + { + "name": "Retrieves personal information about the current partner.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/partners", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTkyODEsImlhdCI6MTUxMzExODY4MiwianRpIjoiZTM0ZDJkNmYtNmNjZS00ZDhlLTkyOWQtMDVkMWMxNTlmNjdmIn0.dLChnDbiVellMCmsYfW01wvLJpfE5QTlFGiUPvigsGhWQtuC-UEgJzYUX03jOR97CBgTho9l-Zv8_-tu00wJLOWXT_H5mW4E2msI5IfwAho8yrmczCOXnHx2pWt6FZid7GoyF-WxPQdnr6f5-b9OuGTauuaGQpN3byzJPNyQhJQPPSYsdE2TsJFfvuoyFbAaf15abtxmJF7NOGEdLO9dZpzJqEigCPAvS__5juTB6hcooVluaCShaAbYo-WSEAzmfN8NDFrazbCvugvayk5b27NDMvCKJO9efy7vMp7itABENwpXmlTqH48AojyhOOVDa8Y618oFx0kmV0fXEpidxg" + } + ], + "body": {}, + "description": "Retrieves personal information (e.g. first name, last name, date of birth) about the current partner." + }, + "response": [ + { + "id": "bd7190c5-279f-4870-b432-36ddf5bca41a", + "name": "Retrieves personal information about the current partner.", + "originalRequest": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/partners", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTkyODEsImlhdCI6MTUxMzExODY4MiwianRpIjoiZTM0ZDJkNmYtNmNjZS00ZDhlLTkyOWQtMDVkMWMxNTlmNjdmIn0.dLChnDbiVellMCmsYfW01wvLJpfE5QTlFGiUPvigsGhWQtuC-UEgJzYUX03jOR97CBgTho9l-Zv8_-tu00wJLOWXT_H5mW4E2msI5IfwAho8yrmczCOXnHx2pWt6FZid7GoyF-WxPQdnr6f5-b9OuGTauuaGQpN3byzJPNyQhJQPPSYsdE2TsJFfvuoyFbAaf15abtxmJF7NOGEdLO9dZpzJqEigCPAvS__5juTB6hcooVluaCShaAbYo-WSEAzmfN8NDFrazbCvugvayk5b27NDMvCKJO9efy7vMp7itABENwpXmlTqH48AojyhOOVDa8Y618oFx0kmV0fXEpidxg", + "description": "" + } + ], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "" + }, + { + "name": "content-length", + "key": "content-length", + "value": "596", + "description": "" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:48:11 GMT", + "description": "" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=100", + "description": "" + }, + { + "name": "message_id", + "key": "message_id", + "value": "83dfbab6-052f-410f-8ca1-973111ca4529", + "description": "" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "" + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "" + } + ], + "cookie": [], + "responseTime": "329", + "body": "{\"partnerType\":\"NATURAL_PERSON\",\"naturalPerson\":{\"firstName\":\"Kim\",\"lastName\":\"Schmid\",\"dateOfBirth\":\"1986-08-05\",\"gender\":\"FEMALE\",\"nationality\":\"DEU\",\"birthName\":\"Müller\",\"birthPlace\":\"München\",\"legitimation\":{\"documentType\":1,\"documentNumber\":\"ee1234\",\"documentIssueDate\":\"2005-05-05\",\"documentIssuingAuthority\":\"EEE Authority\",\"documentExpirationDate\":\"2555-05-05\"}},\"emailAddresses\":[{\"emailAddressType\":\"BUSINESS_ADDRESS\",\"emailAddress\":\"KimSchmid@test.com\"}],\"phoneNumbers\":[{\"communicationType\":\"MOBILE_PHONE\",\"internationalAreaCode\":\"+49\",\"areaCode\":\"170\",\"telephoneNumber\":\"123456\"}]}" + } + ] + }, + { + "name": "Age Certificate", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/ageCertificate?certificationMethod={{certificationMethod}}", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": {}, + "description": "'AgeCertificate' validates whether your Deutsche Bank customer is compliant with given age restrictions - based on the bank's records. Please note: This endpoint is currently only available in our APIs simulator stack and ready for you to test it. Don't hesitate to submit us your feedback and stay tuned on our future product development." + }, + "response": [] + }, + { + "name": "Checks the solvency for the current customer for a given account.", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/customerSolvency?iban=DE10000000000000002783", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "customerSolvency" + ], + "query": [ + { + "key": "iban", + "value": "DE10000000000000002783", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJkZXZlbG9wZXJwb3J0YWwiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMjIxODIsImlhdCI6MTUxMzExODU4MiwianRpIjoiYTcxMmYxYmMtMDgxNC00ZTAxLWFkZDAtYTcyM2RlM2ZmNWI4In0.WyqhWSL1XurGQLzLg5OAO9wIHEJzMADjgh4QRq_mkcsWuFM1997i61UPwxA4FlUG3_SrtV7L9guTsY9x1DQjszqa1Q7k8GKyBSDUiVHS4fRpnQ47JCjNFBCn7_cGz91SkQ-XjObzB_HtnHqryDkXCdpfvIQbdF-0tvDNJ5Q2-ik6YzxsRag8KXx60S2xb59oRxW6j0P-QMI7g0nH0FCspKedpKTwKn2Wc20v3_yTML83S_eCHzy7oInuXv-iX5EwVXs_F2TyE2qkYzjhSjVZXt3HsxTdlTJivgUApiylo5Yr2vm-aeMI9Is1o4_xQcqnmaKVzxUVqGyGuF9sNIpjGQ", + "description": "" + } + ], + "body": {}, + "description": "CustomerSolvency does the background check of a customer’s creditworthiness based on past transactions and account data. A score of solvency is returned, providing you with more security that your customer is likely to pay his bill. Please note: This endpoint is currently only available in our APIs simulator stack and ready for you to test it. Don't hesitate to submit us your feedback and stay tuned on our future product development." + }, + "response": [ + { + "id": "7a6078a8-fc23-4e6f-9fc3-7a2eb7832ac9", + "name": "Checks the solvency for the current customer for a given account.", + "originalRequest": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/customerSolvency?iban=DE10000000000000002783", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "customerSolvency" + ], + "query": [ + { + "key": "iban", + "value": "DE10000000000000002783", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "warning": "" + }, + { + "key": "Authorization", + "type": "text", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTkyODEsImlhdCI6MTUxMzExODY4MiwianRpIjoiZTM0ZDJkNmYtNmNjZS00ZDhlLTkyOWQtMDVkMWMxNTlmNjdmIn0.dLChnDbiVellMCmsYfW01wvLJpfE5QTlFGiUPvigsGhWQtuC-UEgJzYUX03jOR97CBgTho9l-Zv8_-tu00wJLOWXT_H5mW4E2msI5IfwAho8yrmczCOXnHx2pWt6FZid7GoyF-WxPQdnr6f5-b9OuGTauuaGQpN3byzJPNyQhJQPPSYsdE2TsJFfvuoyFbAaf15abtxmJF7NOGEdLO9dZpzJqEigCPAvS__5juTB6hcooVluaCShaAbYo-WSEAzmfN8NDFrazbCvugvayk5b27NDMvCKJO9efy7vMp7itABENwpXmlTqH48AojyhOOVDa8Y618oFx0kmV0fXEpidxg", + "description": "" + } + ], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "" + }, + { + "name": "content-length", + "key": "content-length", + "value": "15", + "description": "" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:44:46 GMT", + "description": "" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=99", + "description": "" + }, + { + "name": "message_id", + "key": "message_id", + "value": "1243a611-fa53-42b3-9973-765ae3a5a2fd", + "description": "" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "" + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "" + } + ], + "cookie": [], + "responseTime": "3225", + "body": "{\"solvency\":21}" + } + ] + }, + { + "name": "Transaction Certificate", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/transactionCertificate?iban={{iban}}&amount={{amount}}¤cyCode={{currencyCode}}&certificationMethod={{certificationMethod}}", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": {}, + "description": "Signing new contracts such as a lease contract often require the customer to provide a salary certificate from their bank. Getting hold of such a proof can be time consuming and annoying. TransactionCertificate automates this process and provides all the necessary data instantly. All parties save time on concluding contracts. Please note: This endpoint is currently only available in our APIs simulator stack and ready for you to test it. Don't hesitate to submit us your feedback and stay tuned on our future product development." + }, + "response": [] + }, + { + "name": "3scale Reads all addresses of the current user.", + "request": { + "url": "https://db-api-gateway.app.test.openshift.es/gw/dbapi/v1/addresses", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMjI0MTEsImlhdCI6MTUxMzEyMTgxMSwianRpIjoiY2E3MzUzNDQtMmU0Ni00MGY1LTk2MjEtYjMyOGZkMTM4N2JlIn0.ODLmiiixM8BKZmNM869t08cpFVVavuWE9AsCNnGxwuHyoKcNDJaHDxYhdAnY1Q5AbzAcyIVUwmNHZ-QPhV8MUe3PD-UnEVWr0XMfw6jLuflTzg-Pur730djiVuM4hv5bZSSID98PPH2KKt2tkY437cZek0sSKeNRLeQ_SBREw49HKp84Y8F87JgUTRigWxYXpoXdAr-gjie9eNARp8CZXw_CLDeYv2cUH3tzXVTg5qhB0iJGqm0vRaNJennyYjv7eOWvN_o3ccojGSTtm06dFKl2kC1nDcq1GFKniVS1W0va3ZNWK--jAHpyG-MKitqbuh8u45KdJbAjsACpqd0-TQ", + "description": "" + }, + { + "key": "user-key", + "value": "dcd6b0554d1b38fff2b4bc43fad96048", + "description": "" + } + ], + "body": {}, + "description": "Reads all addresses of the current user. There might be several private and business addresses. AddressType can be BUSINESS_ADDRESS or PRIVATE_ADDRESS. Country format is ISO code." + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/DBAPI.reorganised.postman_collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/DBAPI.reorganised.postman_collection.json new file mode 100644 index 000000000..1bd33c287 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/DBAPI.reorganised.postman_collection.json @@ -0,0 +1,1266 @@ +{ + "variables": [], + "info": { + "name": "dbapi", + "_postman_id": "31b06080-0610-e694-527d-2002eab46fc3", + "description": "version=1.0 - dbAPI", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "v1", + "description": "", + "item": [ + { + "name": "Reads all addresses of the current user.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/addresses", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Reads all addresses of the current user. There might be several private and business addresses. AddressType can be BUSINESS_ADDRESS or PRIVATE_ADDRESS. Country format is ISO code." + }, + "response": [ + { + "id": "95ab2e32-e052-43d4-9ce5-3c6131d3d841", + "name": "Reads all addresses of the current user.", + "originalRequest": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/addresses", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTk3MTUsImlhdCI6MTUxMzExOTExNiwianRpIjoiMzA4Nzc3NWItMzc3Zi00MGVjLWE2NDYtNjdkOGU1ZDJkZWI2In0.ElB0hmtcAWpG325D6kQ6Efg_AgaGWZ70N6MQ39PiAX9DlEanFsNmXWQ6luwth0K7P7F2Acpy4aXNGs28WOI8Km7OuGScB6PWkk2ecZcS88ln5_yMLyjKdNF5mzv-egb2wILglnhOWK38ZsyH1m0pJXOQ07YmPTnY8ZOIzCOmTwoK86DzxHkoti3QxSstp_opUwhD4gyC88DdwVZghC6hL0qDA9yVidqQBiWdVRNYbpKzvCGpjhLL4Zsac81KUa91fj7hkftZnIjj7gB3Q0Wac3SEzKYJDVhlArIfo4nZTo2iGZZhBV81YgbCSjn6JwhMDGsxUsEShP-L7jx91Ad34Q", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "Tells all caching mechanisms from server to client whether they may cache this object. It is measured in seconds" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "Options that are desired for the connection" + }, + { + "name": "content-length", + "key": "content-length", + "value": "305", + "description": "The length of the response body in octets (8-bit bytes)" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:52:22 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "Custom header" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "Gives the date/time after which the response is considered stale" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=100", + "description": "Custom header" + }, + { + "name": "message_id", + "key": "message_id", + "value": "b1aa6de3-d563-43bc-888e-91e3376e36c6", + "description": "Custom header" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "A name for the server" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "Custom header" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "Cross-site scripting (XSS) filter" + } + ], + "cookie": [], + "responseTime": 367, + "body": "[{\"street\":\"Am Sandtorkai\",\"houseNumber\":\"4\",\"zip\":\"20457\",\"city\":\"Hamburg\",\"country\":\"DEU\",\"addressType\":\"PRIVATE_ADDRESS\",\"registeredResidence\":true},{\"street\":\"Am Sandtorkai\",\"houseNumber\":\"4\",\"zip\":\"20457\",\"city\":\"Hamburg\",\"country\":\"DEU\",\"addressType\":\"BUSINESS_ADDRESS\",\"registeredResidence\":false}]" + } + ] + }, + { + "name": "Reads all cash accounts of the current user.", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/cashAccounts?iban={{iban}}¤cyCode={{currencyCode}}", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "cashAccounts" + ], + "query": [ + { + "key": "iban", + "value": "{{iban}}" + }, + { + "key": "currencyCode", + "value": "{{currencyCode}}" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Reads all cash accounts of the current user. Only current accounts and accounts in the currency EUR are returned. If given IBAN is not valid or does not represent an account of the current user, an empty result is returned." + }, + "response": [ + { + "id": "08c4e35c-d639-4d75-9329-e2ab2d358b17", + "name": "Reads all cash accounts of the current user.", + "originalRequest": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/cashAccounts?iban=DE10000000000000002783", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "cashAccounts" + ], + "query": [ + { + "key": "iban", + "value": "DE10000000000000002783", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTk3MTUsImlhdCI6MTUxMzExOTExNiwianRpIjoiMzA4Nzc3NWItMzc3Zi00MGVjLWE2NDYtNjdkOGU1ZDJkZWI2In0.ElB0hmtcAWpG325D6kQ6Efg_AgaGWZ70N6MQ39PiAX9DlEanFsNmXWQ6luwth0K7P7F2Acpy4aXNGs28WOI8Km7OuGScB6PWkk2ecZcS88ln5_yMLyjKdNF5mzv-egb2wILglnhOWK38ZsyH1m0pJXOQ07YmPTnY8ZOIzCOmTwoK86DzxHkoti3QxSstp_opUwhD4gyC88DdwVZghC6hL0qDA9yVidqQBiWdVRNYbpKzvCGpjhLL4Zsac81KUa91fj7hkftZnIjj7gB3Q0Wac3SEzKYJDVhlArIfo4nZTo2iGZZhBV81YgbCSjn6JwhMDGsxUsEShP-L7jx91Ad34Q", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "Tells all caching mechanisms from server to client whether they may cache this object. It is measured in seconds" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "Options that are desired for the connection" + }, + { + "name": "content-length", + "key": "content-length", + "value": "156", + "description": "The length of the response body in octets (8-bit bytes)" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:52:49 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "Custom header" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "Gives the date/time after which the response is considered stale" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=100", + "description": "Custom header" + }, + { + "name": "message_id", + "key": "message_id", + "value": "ee226b69-f578-4f8d-b159-551e5ff928e7", + "description": "Custom header" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "A name for the server" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "Custom header" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "Cross-site scripting (XSS) filter" + } + ], + "cookie": [], + "responseTime": 285, + "body": "[{\"iban\":\"DE10000000000000002783\",\"currencyCode\":\"EUR\",\"accountType\":\"CURRENT_ACCOUNT\",\"currentBalance\":1400.95,\"productDescription\":\"persönliches Konto\"}]" + } + ] + }, + { + "name": "Create a processing order", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/processingOrders", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "multipart/form-data", + "description": "" + }, + { + "key": "Idempotency-ID", + "value": "{{Idempotency-ID}}", + "description": "" + }, + { + "key": "Process-ID", + "value": "{{Process-ID}}", + "description": "" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "processingOrder", + "value": "{{processingOrder}}", + "type": "text" + }, + { + "key": "documentData", + "value": "{{documentData}}", + "type": "text" + } + ] + }, + "description": "Create a processing order. This endpoint has limited access for special consumers only. It’s possible to start manual processes only." + }, + "response": [] + }, + { + "name": "Reads all transactions of the current cash account.", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/transactions?iban={{iban}}¤cyCode={{currencyCode}}&bookingDateFrom={{bookingDateFrom}}&bookingDateTo={{bookingDateTo}}&sortBy={{sortBy}}", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "transactions" + ], + "query": [ + { + "key": "iban", + "value": "{{iban}}", + "equals": true, + "description": "" + }, + { + "key": "currencyCode", + "value": "{{currencyCode}}", + "equals": true, + "description": "" + }, + { + "key": "bookingDateFrom", + "value": "{{bookingDateFrom}}", + "equals": true, + "description": "" + }, + { + "key": "bookingDateTo", + "value": "{{bookingDateTo}}", + "equals": true, + "description": "" + }, + { + "key": "sortBy", + "value": "{{sortBy}}", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTI2NDg0NTcsImlhdCI6MTUxMjY0Nzg1NywianRpIjoiOGRjOTUzZjYtZGFhNS00NmYyLWFiMDYtMjUxNzQ0YzgxYmIxIn0.bhDoU7MPk5D9WaPIJHnYAnikR0DTquc6m9QygkmYm1PqoSBnJTL80A60X9Jv0Ylq4mwxykGx9XvlTuhzlHgaD1TyAgpQrCjJeUEr1cgscppFyLd7G71BJWoRHGx-AZFruN9hDZUMSyVh5i_iN5EJ4Zlsoc79xwYn-PNJyuC2v6tsnKHc7BS3su69hZCPKKUvLDeLkBmkc6NhtlQJ4dvJ9-NAnrR5HnESUDrWQqhbKWh_p4ra7wa66GmsaBAZVNzFKLB2mestDCfQAczmA2n745m6fq2XqHdPe8q02eZCM72_PfkfiI6vCkhg00C1EV-QWwcMskhanURsCHlQNIlHTw", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Reads all transactions for a specific account of the current user. If given IBAN is not valid or does not represent an account of the current user, an empty result is returned. It is not apparent who issued a transaction, only whether the user gained or lost money by it (based on whether the amount is positive or negative respectively). The maximum number of transactions returned is 200." + }, + "response": [ + { + "id": "a44be83e-3ce0-4a87-adeb-fc563628b003", + "name": "Reads all transactions of the current cash account.", + "originalRequest": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/transactions?iban=DE10000000000000002783", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "transactions" + ], + "query": [ + { + "key": "iban", + "value": "DE10000000000000002783", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "type": "text", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTk3MTUsImlhdCI6MTUxMzExOTExNiwianRpIjoiMzA4Nzc3NWItMzc3Zi00MGVjLWE2NDYtNjdkOGU1ZDJkZWI2In0.ElB0hmtcAWpG325D6kQ6Efg_AgaGWZ70N6MQ39PiAX9DlEanFsNmXWQ6luwth0K7P7F2Acpy4aXNGs28WOI8Km7OuGScB6PWkk2ecZcS88ln5_yMLyjKdNF5mzv-egb2wILglnhOWK38ZsyH1m0pJXOQ07YmPTnY8ZOIzCOmTwoK86DzxHkoti3QxSstp_opUwhD4gyC88DdwVZghC6hL0qDA9yVidqQBiWdVRNYbpKzvCGpjhLL4Zsac81KUa91fj7hkftZnIjj7gB3Q0Wac3SEzKYJDVhlArIfo4nZTo2iGZZhBV81YgbCSjn6JwhMDGsxUsEShP-L7jx91Ad34Q", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "Tells all caching mechanisms from server to client whether they may cache this object. It is measured in seconds" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "Options that are desired for the connection" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:52:02 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "Custom header" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "Gives the date/time after which the response is considered stale" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=100", + "description": "Custom header" + }, + { + "name": "message_id", + "key": "message_id", + "value": "37ea2cfb-8718-4e1f-bbb1-d8a027264d6e", + "description": "Custom header" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "A name for the server" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." + }, + { + "name": "transfer-encoding", + "key": "transfer-encoding", + "value": "chunked", + "description": "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity." + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "Custom header" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "Cross-site scripting (XSS) filter" + } + ], + "cookie": [], + "responseTime": 345, + "body": "[{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-05-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-52.50,\"counterPartyName\":\"GEZ\",\"paymentReference\":\"02/2016\",\"bookingDate\":\"2017-05-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-4.99,\"counterPartyName\":\"Kaffeestand Hamburg HBF\",\"paymentReference\":\"POS MIT PIN. Rechnung 56791873\",\"bookingDate\":\"2017-05-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-16.24,\"counterPartyName\":\"L'Osteria Frankfurt HBF\",\"paymentReference\":\"POS MIT PIN. rechnungs Nr. 21665723123\",\"bookingDate\":\"2017-05-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-123.46,\"counterPartyName\":\"Deutsche Bahn AG\",\"paymentReference\":\"POS MIT PIN. Ticket Hamburg-Freiburg\",\"bookingDate\":\"2017-05-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-05-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.53,\"counterPartyName\":\"I do! Die Hochzeitsmesse\",\"paymentReference\":\"POS MIT PIN. Ticket für die I do! Freiburg\",\"bookingDate\":\"2017-05-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-10.23,\"counterPartyName\":\"Subway\",\"paymentReference\":\"POS MIT PIN. Eat Fresh!\",\"bookingDate\":\"2017-05-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-9.95,\"counterPartyName\":\"Deutsche Bahn Bordbistro\",\"paymentReference\":\"POS MIT PIN. 3453463567\",\"bookingDate\":\"2017-05-14\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.53,\"counterPartyName\":\"Deutsche Bahn AG\",\"paymentReference\":\"POS MIT PIN. Ticket Freiburg-Hamburg\",\"bookingDate\":\"2017-05-14\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-125.89,\"counterPartyName\":\"InterCityHotel Freiburg\",\"paymentReference\":\"POS MIT PIN. Rechnungsnummer 2353462462457\",\"bookingDate\":\"2017-05-14\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-8.86,\"counterPartyName\":\"Deutsche Telekom AG\",\"paymentReference\":\"Hotspot 1 Tag\",\"bookingDate\":\"2017-05-14\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.53,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-05-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.53,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-05-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-05-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-34.33,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-05-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-05-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-51.22,\"counterPartyName\":\"PayPal Europe\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Ebay 661165\",\"bookingDate\":\"2017-05-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-05-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-05-25\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-45.62,\"counterPartyName\":\"Netto\",\"paymentReference\":\"Einkauf\",\"bookingDate\":\"2017-05-25\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-31.05,\"counterPartyName\":\"REWE\",\"paymentReference\":\"POS MIT PIN. Innenstadt\",\"bookingDate\":\"2017-05-28\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-06-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-06-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-06-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-51.22,\"counterPartyName\":\"Netto\",\"paymentReference\":\"Einkauf\",\"bookingDate\":\"2017-06-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-300.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Altona\",\"bookingDate\":\"2017-06-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-06-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":50.00,\"counterPartyName\":\"hochzeitfoto.de\",\"paymentReference\":\"Storno Rechnung 76543\",\"bookingDate\":\"2017-06-25\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-32.68,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-06-25\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-21.25,\"counterPartyName\":\"REWE\",\"paymentReference\":\"POS MIT PIN. Innenstadt\",\"bookingDate\":\"2017-06-26\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-15.56,\"counterPartyName\":\"PayPal Europe\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Ebay 6658765\",\"bookingDate\":\"2017-07-06\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-07-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-07-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-12.67,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-07-16\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-07-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-150.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Altona\",\"bookingDate\":\"2017-07-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-15.85,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-07-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-29.92,\"counterPartyName\":\"REWE\",\"paymentReference\":\"POS MIT PIN. Innenstadt\",\"bookingDate\":\"2017-07-22\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-37.95,\"counterPartyName\":\"Aldi Nord\",\"paymentReference\":\"POS MIT PIN. Hamburg Mitte\",\"bookingDate\":\"2017-07-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-07-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-52.50,\"counterPartyName\":\"GEZ\",\"paymentReference\":\"03/2016\",\"bookingDate\":\"2017-08-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-08-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-13.45,\"counterPartyName\":\"Kaisers Tengelmann\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-08-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-12.67,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-08-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-08-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-300.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-08-17\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-22.24,\"counterPartyName\":\"PayPal Europe\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Ebay 667865\",\"bookingDate\":\"2017-08-18\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-08-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-300.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-08-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-08-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-09-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-13.45,\"counterPartyName\":\"Kaisers Tengelmann\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-37.95,\"counterPartyName\":\"Aldi Nord\",\"paymentReference\":\"POS MIT PIN. Hamburg Mitte\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-12.67,\"counterPartyName\":\"Penny-Markt\",\"paymentReference\":\"POS MIT PIN. Besuchen Sie uns wieder.\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-149.00,\"counterPartyName\":\"Zalando Online\",\"paymentReference\":\"RCHNR 4565570\",\"bookingDate\":\"2017-09-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-09-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-09-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-300.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Altona\",\"bookingDate\":\"2017-10-02\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-72.23,\"counterPartyName\":\"hochzeitideal.de\",\"paymentReference\":\"REF 56778990\",\"bookingDate\":\"2017-10-04\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-36.96,\"counterPartyName\":\"Aldi Nord\",\"paymentReference\":\"POS MIT PIN. Hamburg Mitte\",\"bookingDate\":\"2017-10-05\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-10-11\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-10-13\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-851.00,\"counterPartyName\":\"Air China\",\"paymentReference\":\"HAM-HKG\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1069.00,\"counterPartyName\":\"Hainan Airlines\",\"paymentReference\":\"HKG-HAM\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-675.67,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Withdrawl HK, Wedding Market ATM\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-15.21,\"counterPartyName\":\"Netto\",\"paymentReference\":\"Einkauf\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-59.99,\"counterPartyName\":\"Douglas\",\"paymentReference\":\"POS mit PIN\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-33.85,\"counterPartyName\":\"Kaisers Tengelmann\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-10-21\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-10-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-175.98,\"counterPartyName\":\"Karstadt AG\",\"paymentReference\":\"POS mit PIN\",\"bookingDate\":\"2017-11-04\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-52.50,\"counterPartyName\":\"GEZ\",\"paymentReference\":\"04/2016\",\"bookingDate\":\"2017-11-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-1080.00,\"counterPartyName\":\"Hans Felsenkamp\",\"counterPartyIban\":\"DE55201900030000675488\",\"paymentReference\":\"Miete\",\"bookingDate\":\"2017-11-10\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-200.00,\"counterPartyName\":\"Allianz AG\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Lebens Ref8876885\",\"bookingDate\":\"2017-11-12\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-155.23,\"counterPartyName\":\"hochzeitideal.de\",\"paymentReference\":\"REF 56981990\",\"bookingDate\":\"2017-11-18\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-50.00,\"counterPartyName\":\"hochzeitfoto.de\",\"paymentReference\":\"Rechnung 76543\",\"bookingDate\":\"2017-11-18\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-115.95,\"counterPartyName\":\"Netto\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-11-19\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-13.45,\"counterPartyName\":\"Kaisers Tengelmann\",\"paymentReference\":\"POS MIT PIN. Einkauf\",\"bookingDate\":\"2017-11-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MA005734466\",\"creditorId\":\"DE17ZZZ00046235285\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-88.22,\"counterPartyName\":\"PayPal Europe\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Ebay 666665\",\"bookingDate\":\"2017-11-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-39.99,\"counterPartyName\":\"O2\",\"paymentReference\":\"SEPA-BASISLASTSCHRIFT Galaxy S7 + All Flat 0172-5688779 VTRG 554154879\",\"bookingDate\":\"2017-11-20\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":2504.70,\"counterPartyName\":\"Gruner + Jahr\",\"paymentReference\":\"Gehalt\",\"bookingDate\":\"2017-11-23\",\"currencyCode\":\"EUR\",\"transactionCode\":\"153\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"SALA\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-400.00,\"counterPartyName\":\"Deutsche Bank\",\"paymentReference\":\"Barauszahlung, Innenstadt\",\"bookingDate\":\"2017-11-24\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"ABMX0355443\",\"creditorId\":\"DE0111100004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/XC:121600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-199.00,\"counterPartyName\":\"Zalando Online\",\"paymentReference\":\"RCHNR 4567890\",\"bookingDate\":\"2017-12-02\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"GIRO007535511\",\"creditorId\":\"DE19ZZZ00000899991\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"ZKLE 911/TN:171600 ABC\"},{\"originIban\":\"DE10000000000000002783\",\"amount\":-175.98,\"counterPartyName\":\"Karstadt AG\",\"paymentReference\":\"POS mit PIN\",\"bookingDate\":\"2017-12-06\",\"currencyCode\":\"EUR\",\"transactionCode\":\"123\",\"externalBankTransactionDomainCode\":\"D001\",\"externalBankTransactionFamilyCode\":\"CCRD\",\"externalBankTransactionSubFamilyCode\":\"CWDL\",\"mandateReference\":\"MX0355443\",\"creditorId\":\"DE0222200004544221\",\"e2eReference\":\"E2E - Reference\",\"paymentIdentification\":\"212+ZKLE 911/696682-X-ABC\"}]" + } + ] + }, + { + "name": "Age Certificate", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/ageCertificate?certificationMethod={{certificationMethod}}", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "ageCertificate" + ], + "query": [ + { + "key": "certificationMethod", + "value": "{{certificationMethod}}" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "'AgeCertificate' validates whether your Deutsche Bank customer is compliant with given age restrictions - based on the bank's records. Please note: This endpoint is currently only available in our APIs simulator stack and ready for you to test it. Don't hesitate to submit us your feedback and stay tuned on our future product development." + }, + "response": [] + }, + { + "name": "Retrieves personal information about the current partner.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/partners", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTkyODEsImlhdCI6MTUxMzExODY4MiwianRpIjoiZTM0ZDJkNmYtNmNjZS00ZDhlLTkyOWQtMDVkMWMxNTlmNjdmIn0.dLChnDbiVellMCmsYfW01wvLJpfE5QTlFGiUPvigsGhWQtuC-UEgJzYUX03jOR97CBgTho9l-Zv8_-tu00wJLOWXT_H5mW4E2msI5IfwAho8yrmczCOXnHx2pWt6FZid7GoyF-WxPQdnr6f5-b9OuGTauuaGQpN3byzJPNyQhJQPPSYsdE2TsJFfvuoyFbAaf15abtxmJF7NOGEdLO9dZpzJqEigCPAvS__5juTB6hcooVluaCShaAbYo-WSEAzmfN8NDFrazbCvugvayk5b27NDMvCKJO9efy7vMp7itABENwpXmlTqH48AojyhOOVDa8Y618oFx0kmV0fXEpidxg" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Retrieves personal information (e.g. first name, last name, date of birth) about the current partner." + }, + "response": [ + { + "id": "95d7421a-aa22-4e1a-83a5-1b44db0d49fe", + "name": "Retrieves personal information about the current partner.", + "originalRequest": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v1/partners", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTkyODEsImlhdCI6MTUxMzExODY4MiwianRpIjoiZTM0ZDJkNmYtNmNjZS00ZDhlLTkyOWQtMDVkMWMxNTlmNjdmIn0.dLChnDbiVellMCmsYfW01wvLJpfE5QTlFGiUPvigsGhWQtuC-UEgJzYUX03jOR97CBgTho9l-Zv8_-tu00wJLOWXT_H5mW4E2msI5IfwAho8yrmczCOXnHx2pWt6FZid7GoyF-WxPQdnr6f5-b9OuGTauuaGQpN3byzJPNyQhJQPPSYsdE2TsJFfvuoyFbAaf15abtxmJF7NOGEdLO9dZpzJqEigCPAvS__5juTB6hcooVluaCShaAbYo-WSEAzmfN8NDFrazbCvugvayk5b27NDMvCKJO9efy7vMp7itABENwpXmlTqH48AojyhOOVDa8Y618oFx0kmV0fXEpidxg", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "html", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "" + }, + { + "name": "content-length", + "key": "content-length", + "value": "596", + "description": "" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:48:11 GMT", + "description": "" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=100", + "description": "" + }, + { + "name": "message_id", + "key": "message_id", + "value": "83dfbab6-052f-410f-8ca1-973111ca4529", + "description": "" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "" + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "" + } + ], + "cookie": [], + "responseTime": "329", + "body": "{\"partnerType\":\"NATURAL_PERSON\",\"naturalPerson\":{\"firstName\":\"Kim\",\"lastName\":\"Schmid\",\"dateOfBirth\":\"1986-08-05\",\"gender\":\"FEMALE\",\"nationality\":\"DEU\",\"birthName\":\"Müller\",\"birthPlace\":\"München\",\"legitimation\":{\"documentType\":1,\"documentNumber\":\"ee1234\",\"documentIssueDate\":\"2005-05-05\",\"documentIssuingAuthority\":\"EEE Authority\",\"documentExpirationDate\":\"2555-05-05\"}},\"emailAddresses\":[{\"emailAddressType\":\"BUSINESS_ADDRESS\",\"emailAddress\":\"KimSchmid@test.com\"}],\"phoneNumbers\":[{\"communicationType\":\"MOBILE_PHONE\",\"internationalAreaCode\":\"+49\",\"areaCode\":\"170\",\"telephoneNumber\":\"123456\"}]}" + } + ] + }, + { + "name": "Checks the solvency for the current customer for a given account.", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/customerSolvency?iban=DE10000000000000002783", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "customerSolvency" + ], + "query": [ + { + "key": "iban", + "value": "DE10000000000000002783", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJkZXZlbG9wZXJwb3J0YWwiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMjIxODIsImlhdCI6MTUxMzExODU4MiwianRpIjoiYTcxMmYxYmMtMDgxNC00ZTAxLWFkZDAtYTcyM2RlM2ZmNWI4In0.WyqhWSL1XurGQLzLg5OAO9wIHEJzMADjgh4QRq_mkcsWuFM1997i61UPwxA4FlUG3_SrtV7L9guTsY9x1DQjszqa1Q7k8GKyBSDUiVHS4fRpnQ47JCjNFBCn7_cGz91SkQ-XjObzB_HtnHqryDkXCdpfvIQbdF-0tvDNJ5Q2-ik6YzxsRag8KXx60S2xb59oRxW6j0P-QMI7g0nH0FCspKedpKTwKn2Wc20v3_yTML83S_eCHzy7oInuXv-iX5EwVXs_F2TyE2qkYzjhSjVZXt3HsxTdlTJivgUApiylo5Yr2vm-aeMI9Is1o4_xQcqnmaKVzxUVqGyGuF9sNIpjGQ", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "CustomerSolvency does the background check of a customer’s creditworthiness based on past transactions and account data. A score of solvency is returned, providing you with more security that your customer is likely to pay his bill. Please note: This endpoint is currently only available in our APIs simulator stack and ready for you to test it. Don't hesitate to submit us your feedback and stay tuned on our future product development." + }, + "response": [ + { + "id": "94cd24d4-46b7-4006-927a-87af47c7bc6f", + "name": "Checks the solvency for the current customer for a given account.", + "originalRequest": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/customerSolvency?iban=DE10000000000000002783", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "customerSolvency" + ], + "query": [ + { + "key": "iban", + "value": "DE10000000000000002783", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "warning": "" + }, + { + "key": "Authorization", + "type": "text", + "name": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMTkyODEsImlhdCI6MTUxMzExODY4MiwianRpIjoiZTM0ZDJkNmYtNmNjZS00ZDhlLTkyOWQtMDVkMWMxNTlmNjdmIn0.dLChnDbiVellMCmsYfW01wvLJpfE5QTlFGiUPvigsGhWQtuC-UEgJzYUX03jOR97CBgTho9l-Zv8_-tu00wJLOWXT_H5mW4E2msI5IfwAho8yrmczCOXnHx2pWt6FZid7GoyF-WxPQdnr6f5-b9OuGTauuaGQpN3byzJPNyQhJQPPSYsdE2TsJFfvuoyFbAaf15abtxmJF7NOGEdLO9dZpzJqEigCPAvS__5juTB6hcooVluaCShaAbYo-WSEAzmfN8NDFrazbCvugvayk5b27NDMvCKJO9efy7vMp7itABENwpXmlTqH48AojyhOOVDa8Y618oFx0kmV0fXEpidxg", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "html", + "header": [ + { + "name": "cache-control", + "key": "cache-control", + "value": "private", + "description": "" + }, + { + "name": "connection", + "key": "connection", + "value": "Keep-Alive", + "description": "" + }, + { + "name": "content-length", + "key": "content-length", + "value": "15", + "description": "" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json", + "description": "" + }, + { + "name": "date", + "key": "date", + "value": "Tue, 12 Dec 2017 22:44:46 GMT", + "description": "" + }, + { + "name": "db-nickname", + "key": "db-nickname", + "value": "VTJGc2RHVmtYMS9qQk1MQ2NMV08rV092WmI1bnFidGtpSGVRTUoyTE91MD0=", + "description": "" + }, + { + "name": "expires", + "key": "expires", + "value": "Thu, 01 Jan 1970 01:00:00 GMT", + "description": "" + }, + { + "name": "keep-alive", + "key": "keep-alive", + "value": "timeout=5, max=99", + "description": "" + }, + { + "name": "message_id", + "key": "message_id", + "value": "1243a611-fa53-42b3-9973-765ae3a5a2fd", + "description": "" + }, + { + "name": "server", + "key": "server", + "value": "Apache-Coyote/1.1", + "description": "" + }, + { + "name": "strict-transport-security", + "key": "strict-transport-security", + "value": "max-age=31536000", + "description": "" + }, + { + "name": "x-content-type-options", + "key": "x-content-type-options", + "value": "nosniff", + "description": "" + }, + { + "name": "x-db-nar", + "key": "x-db-nar", + "value": "101235-1", + "description": "" + }, + { + "name": "x-frame-options", + "key": "x-frame-options", + "value": "DENY", + "description": "" + }, + { + "name": "x-xss-protection", + "key": "x-xss-protection", + "value": "1; mode=block", + "description": "" + } + ], + "cookie": [], + "responseTime": "3225", + "body": "{\"solvency\":21}" + } + ] + }, + { + "name": "Transaction Certificate", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v1/transactionCertificate?iban={{iban}}&amount={{amount}}¤cyCode={{currencyCode}}&certificationMethod={{certificationMethod}}", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v1", + "transactionCertificate" + ], + "query": [ + { + "key": "iban", + "value": "{{iban}}" + }, + { + "key": "amount", + "value": "{{amount}}" + }, + { + "key": "currencyCode", + "value": "{{currencyCode}}" + }, + { + "key": "certificationMethod", + "value": "{{certificationMethod}}" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Signing new contracts such as a lease contract often require the customer to provide a salary certificate from their bank. Getting hold of such a proof can be time consuming and annoying. TransactionCertificate automates this process and provides all the necessary data instantly. All parties save time on concluding contracts. Please note: This endpoint is currently only available in our APIs simulator stack and ready for you to test it. Don't hesitate to submit us your feedback and stay tuned on our future product development." + }, + "response": [] + } + ] + }, + { + "name": "v2", + "description": "", + "item": [ + { + "name": "Reads all addresses of the current user.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v2/addresses", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Reads all addresses of the current user. There might be several private and business addresses. AddressType can be BUSINESS_ADDRESS or PRIVATE_ADDRESS. Country format is ISO code." + }, + "response": [] + }, + { + "name": "Retrieves personal information about the current partner.", + "request": { + "url": "https://simulator-api.db.com:443/gw/dbapi/v2/partners", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Retrieves personal information (e.g. first name, last name, date of birth) about the current partner." + }, + "response": [] + }, + { + "name": "Reads all transactions of the current cash account.", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v2/transactions?iban={{iban}}¤cyCode={{currencyCode}}&bookingDateFrom={{bookingDateFrom}}&bookingDateTo={{bookingDateTo}}&sortBy=bookingDate[ASC]&limit=10&offset=0", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v2", + "transactions" + ], + "query": [ + { + "key": "iban", + "value": "{{iban}}" + }, + { + "key": "currencyCode", + "value": "{{currencyCode}}" + }, + { + "key": "bookingDateFrom", + "value": "{{bookingDateFrom}}" + }, + { + "key": "bookingDateTo", + "value": "{{bookingDateTo}}" + }, + { + "key": "sortBy", + "value": "bookingDate[ASC]" + }, + { + "key": "limit", + "value": "10" + }, + { + "key": "offset", + "value": "0" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Reads all transactions for a specific account of the current user. If given IBAN is not valid or does not represent an account of the current user, an empty result is returned. It is not apparent who issued a transaction, only whether the user gained or lost money by it (based on whether the amount is positive or negative respectively). The maximum number of transactions returned is 200." + }, + "response": [] + }, + { + "name": "Reads all cash accounts of the current user.", + "request": { + "url": { + "raw": "https://simulator-api.db.com:443/gw/dbapi/v2/cashAccounts?iban={{iban}}¤cyCode={{currencyCode}}&limit=10&offset=0", + "protocol": "https", + "host": [ + "simulator-api", + "db", + "com" + ], + "port": "443", + "path": [ + "gw", + "dbapi", + "v2", + "cashAccounts" + ], + "query": [ + { + "key": "iban", + "value": "{{iban}}" + }, + { + "key": "currencyCode", + "value": "{{currencyCode}}" + }, + { + "key": "limit", + "value": "10" + }, + { + "key": "offset", + "value": "0" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Reads all cash accounts of the current user. Only current accounts and accounts in the currency EUR are returned. If given IBAN is not valid or does not represent an account of the current user, an empty result is returned." + }, + "response": [] + } + ] + }, + { + "name": "3scale Reads all addresses of the current user.", + "request": { + "url": "https://db-api-gateway.app.test.openshift.es/gw/dbapi/v1/addresses", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "description": "" + }, + { + "key": "Authorization", + "value": "Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlM1MTIifQ.eyJhdWQiOiJmM2JjZDlkYi1lMGY4LTQxY2UtOGUxNS00ODVjYjBiMWJjYjgiLCJpc3MiOiJodHRwczpcL1wvc2ltdWxhdG9yLWFwaS5kYi5jb21cL2d3XC9vaWRjXC8iLCJleHAiOjE1MTMxMjI0MTEsImlhdCI6MTUxMzEyMTgxMSwianRpIjoiY2E3MzUzNDQtMmU0Ni00MGY1LTk2MjEtYjMyOGZkMTM4N2JlIn0.ODLmiiixM8BKZmNM869t08cpFVVavuWE9AsCNnGxwuHyoKcNDJaHDxYhdAnY1Q5AbzAcyIVUwmNHZ-QPhV8MUe3PD-UnEVWr0XMfw6jLuflTzg-Pur730djiVuM4hv5bZSSID98PPH2KKt2tkY437cZek0sSKeNRLeQ_SBREw49HKp84Y8F87JgUTRigWxYXpoXdAr-gjie9eNARp8CZXw_CLDeYv2cUH3tzXVTg5qhB0iJGqm0vRaNJennyYjv7eOWvN_o3ccojGSTtm06dFKl2kC1nDcq1GFKniVS1W0va3ZNWK--jAHpyG-MKitqbuh8u45KdJbAjsACpqd0-TQ", + "description": "" + }, + { + "key": "user-key", + "value": "dcd6b0554d1b38fff2b4bc43fad96048", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Reads all addresses of the current user. There might be several private and business addresses. AddressType can be BUSINESS_ADDRESS or PRIVATE_ADDRESS. Country format is ISO code." + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Nicolas.postman_collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Nicolas.postman_collection.json new file mode 100644 index 000000000..c4c2d648f --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Nicolas.postman_collection.json @@ -0,0 +1,359 @@ +{ + "info": { + "_postman_id": "dac49161-6832-4972-acaf-3b7b5cfcbef4", + "name": "Beer Catalog API", + "description": "version=1.1 - An API for querying beer catalog of Acme Inc.", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "beer", + "description": "Folder for beer", + "item": [ + { + "name": "Get beer having name", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var expectedName = globals[\"name\"];", + "var jsonData = JSON.parse(responseBody);", + "", + "var schema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": { \"type\": \"string\", \"enum\": [expectedName] },", + " \"country\": { \"type\": \"string\" },", + " \"type\": { \"type\": \"string\" },", + " \"rating\": { \"type\": \"number\" },", + " \"status\": { \"type\": \"string\" }", + " }", + "};", + "", + "tests[\"Valid name in response\"] = tv4.validate(jsonData, schema);" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://beer/:name", + "protocol": "http", + "host": [ + "beer" + ], + "path": [ + ":name" + ], + "variable": [ + { + "key": "name", + "value": "" + } + ] + }, + "description": "Get beer having name" + }, + "response": [ + { + "id": "627ae527-5e35-46b5-ab51-bf4207ddede5", + "name": "Weissbier", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://beer/:name", + "protocol": "http", + "host": [ + "beer" + ], + "path": [ + ":name" + ], + "variable": [ + { + "key": "name", + "value": "Weissbier" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"name\": \"Weissbier\",\n \"country\": \"Germany\",\n \"type\": \"Wheat\",\n \"rating\": 4.1,\n \"status\": \"out_of_stock\"\n}" + }, + { + "id": "15ca871a-091b-4b2b-bf8e-77aacea773d6", + "name": "Rodenbach", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://beer/:name", + "protocol": "http", + "host": [ + "beer" + ], + "path": [ + ":name" + ], + "variable": [ + { + "key": "name", + "value": "Rodenbach" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\"name\": \"Rodenbach\", \"country\": \"Belgium\", \"type\": \"Brown ale\", \"rating\": 4.2, \"status\": \"available\"}" + } + ] + }, + { + "name": "Get beers having status", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var expectedStatus = globals[\"status\"];", + "var jsonData = JSON.parse(responseBody);", + "", + "var schema = {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": { \"type\": \"string\" },", + " \"country\": { \"type\": \"string\" },", + " \"type\": { \"type\": \"string\" },", + " \"rating\": { \"type\": \"number\" },", + " \"status\": { \"type\": \"string\", \"enum\": [expectedStatus] }", + " }", + " }", + "};", + "", + "tests[\"Valid response\"] = tv4.validate(jsonData, schema);" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://beer/findByStatus/:status", + "protocol": "http", + "host": [ + "beer" + ], + "path": [ + "findByStatus", + ":status" + ], + "variable": [ + { + "key": "status", + "value": "" + } + ] + }, + "description": "Get beers having status" + }, + "response": [ + { + "id": "830f5c2b-09a1-473b-81f5-646c49a32ffa", + "name": "Get out_of_stock beers", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://beer/findByStatus/:status", + "protocol": "http", + "host": [ + "beer" + ], + "path": [ + "findByStatus", + ":status" + ], + "variable": [ + { + "key": "status", + "value": "out_of_stock" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "[{\"name\": \"Weissbier\", \"country\": \"Germany\", \"type\": \"Wheat\", \"rating\": 4.1, \"status\": \"out_of_stock\"}]" + }, + { + "id": "19bad278-5fc0-4940-ba5e-9810d82332dc", + "name": "Get available beers", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://beer/findByStatus/:status", + "protocol": "http", + "host": [ + "beer" + ], + "path": [ + "findByStatus", + ":status" + ], + "variable": [ + { + "key": "status", + "value": "available" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "[\n {\n \"name\": \"Rodenbach\",\n \"country\": \"Belgium\",\n \"type\": \"Brown ale\",\n \"rating\": 4.2,\n \"status\": \"available\"\n },\n {\n \"name\": \"Westmalle Triple\",\n \"country\": \"Belgium\",\n \"type\": \"Trappist\",\n \"rating\": 3.8,\n \"status\": \"available\"\n },\n {\n \"name\": \"Heineken\",\n \"country\": \"France\",\n \"type\": \"Universal\",\n \"rating\": 3.1,\n \"status\": \"available\"\n }\n]" + } + ] + }, + { + "name": "List beers within catalog", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "tests[\"Status code is OK\"] = (responseCode.code === 200 || responseCode.code === 404);" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://beer?page={{page}}", + "protocol": "http", + "host": [ + "beer" + ], + "query": [ + { + "key": "page", + "value": "{{page}}" + } + ] + }, + "description": "List beers within catalog" + }, + "response": [ + { + "id": "2f09845b-5b42-4406-9bdd-0846d9ecb722", + "name": "List page 0", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://beer?page=0", + "protocol": "http", + "host": [ + "beer" + ], + "query": [ + { + "key": "page", + "value": "0" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "[\n {\n \"name\": \"Rodenbach\",\n \"country\": \"Belgium\",\n \"type\": \"Brown ale\",\n \"rating\": 4.2,\n \"status\": \"available\"\n },\n {\n \"name\": \"Westmalle Triple\",\n \"country\": \"Belgium\",\n \"type\": \"Trappist\",\n \"rating\": 3.8,\n \"status\": \"available\"\n },\n {\n \"name\": \"Weissbier\",\n \"country\": \"Germany\",\n \"type\": \"Wheat\",\n \"rating\": 4.1,\n \"status\": \"out_of_stock\"\n }\n]" + } + ] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "0d53df2c-6ce6-49bf-b07e-32153a610b29", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "6cd7edcb-b7f1-4ac6-af38-b89c89af64d5", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/OpenBankAPI.postman_collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/OpenBankAPI.postman_collection.json new file mode 100644 index 000000000..41dd4a7f8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/OpenBankAPI.postman_collection.json @@ -0,0 +1,472 @@ +{ + "id": "a4caef00-030a-702e-1f27-4b1ff1341abc", + "name": "Random Open Bank Project API", + "description": "version=0.0.1 - An API based on the Open Bank project. (C) Unisystems 2017, (c) TESOBE Ltd. 2011 - 2016. Licensed under the AGPL and commercial licences.", + "order": [], + "folders": [ + { + "id": "b5bf75f5-3e73-f221-a078-226910ff483b", + "name": "banks", + "description": "Folder for banks", + "order": [ + "7867bc19-ed7f-0b85-d46a-50967e459f88", + "7460e640-ced5-ca36-a88f-d647576f49a8", + "fb619a9d-4439-972f-ada5-dea756a79e46", + "e7aa9c34-efa1-3af7-afe1-f29714c8c450", + "afc8ab76-8e9e-61a0-cfad-014bffdbe7ba", + "7fbe0e22-b54c-2bd0-c424-6f52ab53dfeb" + ], + "owner": "1278651", + "collectionId": "a4caef00-030a-702e-1f27-4b1ff1341abc" + } + ], + "timestamp": 1413302258635, + "owner": "1278651", + "public": false, + "requests": [ + { + "id": "7460e640-ced5-ca36-a88f-d647576f49a8", + "headers": "", + "headerData": [], + "url": "https:///banks/random/accounts/456/account", + "queryParams": [], + "preRequestScript": "", + "pathVariables": {}, + "pathVariableData": [], + "method": "GET", + "data": [], + "dataMode": "params", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1497024977867, + "name": "GetAccountById (Full - 456)", + "description": "

Information returned about an account specified by ACCOUNT_ID:

  • Number
  • Type
  • Balance
  • IBAN

PSD2 Context: PSD2 requires customers to have access to their account information via third party applications. This call provides balance and other account information via delegated authentication using OAuth.

OAuth authentication is required.

", + "collectionId": "a4caef00-030a-702e-1f27-4b1ff1341abc", + "responses": [ + { + "status": "", + "responseCode": { + "code": 404, + "name": "Not Found" + }, + "time": 0, + "headers": [], + "cookies": [], + "mime": "", + "text": "", + "language": "json", + "rawDataType": "", + "previewType": "parsed", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": false, + "id": "3dd559cb-edbf-0f8c-5687-7c6fa127ff1f", + "name": "456 response", + "isSample": true, + "scrollToResult": false, + "runTests": false, + "request": { + "url": "https:///banks/random/accounts/:ACCOUNT_ID/account", + "pathVariables": { + "ACCOUNT_ID": "456" + }, + "pathVariableData": [ + { + "description": "", + "key": "ACCOUNT_ID", + "value": "456" + } + ], + "queryParams": [], + "headerData": [], + "headers": "", + "data": [], + "method": "GET", + "dataMode": "params" + } + } + ] + }, + { + "id": "7867bc19-ed7f-0b85-d46a-50967e459f88", + "headers": "", + "headerData": [], + "url": "https:///banks/random/accounts/123/account", + "queryParams": [], + "preRequestScript": "", + "pathVariables": {}, + "pathVariableData": [], + "method": "GET", + "data": [], + "dataMode": "params", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1497024969239, + "name": "GetAccountById (Full - 123)", + "description": "

Information returned about an account specified by ACCOUNT_ID:

  • Number
  • Type
  • Balance
  • IBAN

PSD2 Context: PSD2 requires customers to have access to their account information via third party applications. This call provides balance and other account information via delegated authentication using OAuth.

OAuth authentication is required.

", + "collectionId": "a4caef00-030a-702e-1f27-4b1ff1341abc", + "responses": [ + { + "status": "", + "responseCode": { + "code": 200, + "name": "OK" + }, + "time": 0, + "headers": [], + "cookies": [], + "mime": "", + "text": "{\n\t\"Number\": 123,\n\t\"IBAN\": \"azertyuiop\",\n\t\"Type\": \"AccountType\",\n\t\"Balance\": 1000.00\n}", + "language": "json", + "rawDataType": "", + "previewType": "parsed", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": false, + "id": "d198b9c1-49fd-9590-c21e-2c6702c72669", + "name": "123 response", + "isSample": true, + "scrollToResult": false, + "runTests": false, + "request": { + "url": "https:///banks/random/accounts/:ACCOUNT_ID/account", + "pathVariables": { + "ACCOUNT_ID": "123" + }, + "pathVariableData": [ + { + "description": "", + "key": "ACCOUNT_ID", + "value": "123" + } + ], + "queryParams": [], + "headerData": [], + "headers": "", + "data": [], + "method": "GET", + "dataMode": "params" + } + } + ] + }, + { + "id": "7fbe0e22-b54c-2bd0-c424-6f52ab53dfeb", + "headers": "obp_from_date: 2017-01-01'T'12:00:00.000'Z'\nobp_to_date: 2017-06-01'T'12:00:00.000'Z'\n", + "headerData": [ + { + "key": "obp_from_date", + "value": "2017-01-01'T'12:00:00.000'Z'", + "description": "", + "enabled": true + }, + { + "key": "obp_to_date", + "value": "2017-06-01'T'12:00:00.000'Z'", + "description": "", + "enabled": true + } + ], + "url": "https:///banks/random/accounts/456/transactions", + "queryParams": [], + "preRequestScript": "", + "pathVariables": {}, + "pathVariableData": [], + "method": "GET", + "data": [], + "dataMode": "params", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1497024997617, + "name": "GetTransactionsForAccount (456)", + "description": "

Returns transactions list of the account specified by ACCOUNT_ID.

Authentication via OAuth is required.

Possible custom headers for pagination:

  • obp_from_date=DATE => default value: date of the oldest transaction registered (format below)
  • obp_to_date=DATE => default value: date of the newest transaction registered (format below)

Date format parameter: \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\" (2014-07-01T00:00:00.000Z) ==> time zone is UTC.

", + "collectionId": "a4caef00-030a-702e-1f27-4b1ff1341abc", + "responses": [ + { + "status": "", + "responseCode": { + "code": 404, + "name": "Not Found" + }, + "time": 0, + "headers": [], + "cookies": [], + "mime": "", + "text": "", + "language": "", + "rawDataType": "", + "previewType": "parsed", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": false, + "id": "06169598-b643-4072-5cbb-31e2f4326f6c", + "name": "456 response", + "isSample": true, + "scrollToResult": false, + "runTests": false, + "request": { + "url": "https:///banks/random/accounts/:ACCOUNT_ID/transactions", + "pathVariables": { + "ACCOUNT_ID": "456" + }, + "pathVariableData": [ + { + "description": "", + "key": "ACCOUNT_ID", + "value": "456" + } + ], + "queryParams": [], + "headerData": [ + { + "key": "obp_from_date", + "value": "2017-01-01'T'12:00:00.000'Z'", + "description": "", + "enabled": true, + "warning": "" + }, + { + "key": "obp_to_date", + "value": "2017-06-01'T'12:00:00.000'Z'", + "description": "", + "enabled": true, + "warning": "" + } + ], + "headers": "obp_from_date: 2017-01-01'T'12:00:00.000'Z'\nobp_to_date: 2017-06-01'T'12:00:00.000'Z'\n", + "data": [], + "method": "GET", + "dataMode": "params" + } + } + ] + }, + { + "id": "afc8ab76-8e9e-61a0-cfad-014bffdbe7ba", + "headers": "obp_from_date: 2017-01-01'T'12:00:00.000'Z'\nobp_to_date: 2017-06-01'T'12:00:00.000'Z'\n", + "headerData": [ + { + "key": "obp_from_date", + "value": "2017-01-01'T'12:00:00.000'Z'", + "description": "", + "enabled": true + }, + { + "key": "obp_to_date", + "value": "2017-06-01'T'12:00:00.000'Z'", + "description": "", + "enabled": true + } + ], + "url": "https:///banks/random/accounts/123/transactions", + "queryParams": [], + "preRequestScript": "", + "pathVariables": {}, + "pathVariableData": [], + "method": "GET", + "data": [], + "dataMode": "params", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1497024987734, + "name": "GetTransactionsForAccount (123)", + "description": "

Returns transactions list of the account specified by ACCOUNT_ID.

Authentication via OAuth is required.

Possible custom headers for pagination:

  • obp_from_date=DATE => default value: date of the oldest transaction registered (format below)
  • obp_to_date=DATE => default value: date of the newest transaction registered (format below)

Date format parameter: \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\" (2014-07-01T00:00:00.000Z) ==> time zone is UTC.

", + "collectionId": "a4caef00-030a-702e-1f27-4b1ff1341abc", + "responses": [ + { + "status": "", + "responseCode": { + "code": 200, + "name": "OK" + }, + "time": 0, + "headers": [], + "cookies": [], + "mime": "", + "text": "[\n\t{\"tx\": 0},\n\t{\"tx\": 1}\n]", + "language": "json", + "rawDataType": "", + "previewType": "parsed", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": false, + "id": "8f1cb68b-8dc5-7a16-b0fe-d70378c8cfb9", + "name": "123 response", + "isSample": true, + "scrollToResult": false, + "runTests": false, + "request": { + "url": "https:///banks/random/accounts/:ACCOUNT_ID/transactions", + "pathVariables": { + "ACCOUNT_ID": "123" + }, + "pathVariableData": [ + { + "description": "", + "key": "ACCOUNT_ID", + "value": "123" + } + ], + "queryParams": [], + "headerData": [ + { + "key": "obp_from_date", + "value": "2017-01-01'T'12:00:00.000'Z'", + "enabled": true, + "description": "" + }, + { + "key": "obp_to_date", + "value": "2017-06-01'T'12:00:00.000'Z'", + "enabled": true, + "description": "" + } + ], + "headers": "obp_from_date: 2017-01-01'T'12:00:00.000'Z'\nobp_to_date: 2017-06-01'T'12:00:00.000'Z'\n", + "data": [], + "method": "GET", + "dataMode": "params" + } + } + ] + }, + { + "id": "e7aa9c34-efa1-3af7-afe1-f29714c8c450", + "headers": "", + "headerData": [], + "url": "https:///banks/random/accounts/456/transaction-request-types/SEPA/transaction-requests", + "queryParams": [], + "preRequestScript": "", + "pathVariables": {}, + "pathVariableData": [], + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1497025541125, + "name": "CreateTransactionRequest (SEPA - 456)", + "description": "

Initiate a Payment via creating a Transaction Request.

In OBP, a transaction request may or may not result in a transaction. However, a transaction only has one possible state: completed.

Transactions are modeled on items in a bank statement that represent the movement of money.

Transaction Requests are requests to move money which may or may not succeeed and thus result in a Transaction.

Authentication is Mandatory

", + "collectionId": "a4caef00-030a-702e-1f27-4b1ff1341abc", + "responses": [ + { + "status": "", + "responseCode": { + "code": 400, + "name": "Bad Request" + }, + "time": 0, + "headers": [], + "cookies": [], + "mime": "", + "text": "", + "language": "text", + "rawDataType": "", + "previewType": "parsed", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": false, + "id": "a56ee1aa-192e-a39f-f9fa-ba95b248bc5f", + "name": "456 response", + "isSample": true, + "scrollToResult": false, + "runTests": false, + "request": { + "url": "https:///banks/random/accounts/123/transaction-request-types/SEPA/transaction-requests", + "pathVariables": {}, + "pathVariableData": [], + "queryParams": [], + "headerData": [], + "headers": "", + "data": "BANK_BODY", + "method": "POST", + "dataMode": "raw" + } + } + ], + "rawModeData": "BANK_BODY" + }, + { + "id": "fb619a9d-4439-972f-ada5-dea756a79e46", + "headers": "", + "headerData": [], + "url": "https:///banks/random/accounts/123/transaction-request-types/SEPA/transaction-requests", + "queryParams": [], + "preRequestScript": "", + "pathVariables": {}, + "pathVariableData": [], + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1497025653147, + "name": "CreateTransactionRequest (SEPA - 123)", + "description": "

Initiate a Payment via creating a Transaction Request.

In OBP, a transaction request may or may not result in a transaction. However, a transaction only has one possible state: completed.

Transactions are modeled on items in a bank statement that represent the movement of money.

Transaction Requests are requests to move money which may or may not succeeed and thus result in a Transaction.

Authentication is Mandatory

", + "collectionId": "a4caef00-030a-702e-1f27-4b1ff1341abc", + "responses": [ + { + "status": "", + "responseCode": { + "code": 201, + "name": "Created" + }, + "time": 0, + "headers": [], + "cookies": [], + "mime": "", + "text": "{\n\t\"tx\": 0\n}", + "language": "json", + "rawDataType": "", + "previewType": "parsed", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": false, + "name": "123 response", + "id": "fa1e706e-64b6-eac1-8114-fbdda8543733", + "isSample": true, + "scrollToResult": false, + "runTests": false, + "request": { + "url": "https:///banks/random/accounts/123/transaction-request-types/SEPA/transaction-requests", + "pathVariables": {}, + "pathVariableData": [], + "queryParams": [], + "headerData": [], + "headers": "", + "data": "BANK_BODY", + "method": "POST", + "dataMode": "raw" + } + } + ], + "rawModeData": "BANK_BODY" + } + ] +} +] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-sample-trailing-dollar.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-sample-trailing-dollar.json new file mode 100644 index 000000000..e7649211a --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-sample-trailing-dollar.json @@ -0,0 +1,485 @@ +{ + "variables": [], + "info": { + "name": "Petstore API", + "_postman_id": "c0bc3513-c83a-8b5d-bbff-1a4a0dc001d7", + "description": "version=12.0 - This is a PetStore API description", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "pet", + "description": "", + "item": [ + { + "name": "Find pet by ID", + "request": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/:petId/$access", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + ], + "variable": [ + { + "key": "petId", + "value": "" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "" + }, + "response": [ + { + "id": "05486047-439f-408d-97e7-a9925107a2a1", + "name": "get-2", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.staging.apicast.io:443/v2/pet/:petId/$access", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "staging", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId", + "" + ], + "query": [ + ], + "variable": [ + { + "key": "petId", + "value": "2" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "" + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "" + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "" + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "" + }, + { + "name": "Content-Length", + "key": "Content-Length", + "value": "137", + "description": "" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:18 GMT", + "description": "" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "" + } + ], + "cookie": [], + "responseTime": 233, + "body": "{\"id\":2,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"}" + }, + { + "id": "187fb73f-edbb-47d4-89ae-7e3e4a8cfcbd", + "name": "get-1", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/:petId/$access", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + ], + "variable": [ + { + "key": "petId", + "value": "1" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "", + "_postman_previewtype": "parsed", + "header": [ + { + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Headers" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Methods" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Origin" + }, + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "" + }, + { + "key": "Content-Length", + "value": "0", + "description": "", + "type": "text", + "name": "Content-Length" + }, + { + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "", + "type": "text", + "name": "Server" + }, + { + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:22 GMT", + "description": "", + "type": "text", + "name": "Date" + }, + { + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "", + "type": "text", + "name": "X-Powered-By" + }, + { + "key": "Connection", + "value": "keep-alive", + "description": "", + "type": "text", + "name": "Connection" + } + ], + "cookie": [], + "responseTime": 0, + "body": "" + } + ] + }, + { + "name": "Count pet by ID", + "request": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/:petId/$count", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + ], + "variable": [ + { + "key": "petId", + "value": "" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "" + }, + "response": [ + { + "id": "05486047-439f-408d-97e7-a9925107a2a1", + "name": "get-2", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.staging.apicast.io:443/v2/pet/:petId/$count", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "staging", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId", + "" + ], + "query": [ + ], + "variable": [ + { + "key": "petId", + "value": "2" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "" + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "" + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "" + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "" + }, + { + "name": "Content-Length", + "key": "Content-Length", + "value": "137", + "description": "" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:18 GMT", + "description": "" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "" + } + ], + "cookie": [], + "responseTime": 233, + "body": "{\"id\":2,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"}" + }, + { + "id": "187fb73f-edbb-47d4-89ae-7e3e4a8cfcbd", + "name": "get-1", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/:petId/$count", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + ], + "variable": [ + { + "key": "petId", + "value": "1" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "", + "_postman_previewtype": "parsed", + "header": [ + { + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Headers" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Methods" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Origin" + }, + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "" + }, + { + "key": "Content-Length", + "value": "0", + "description": "", + "type": "text", + "name": "Content-Length" + }, + { + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "", + "type": "text", + "name": "Server" + }, + { + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:22 GMT", + "description": "", + "type": "text", + "name": "Date" + }, + { + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "", + "type": "text", + "name": "X-Powered-By" + }, + { + "key": "Connection", + "value": "keep-alive", + "description": "", + "type": "text", + "name": "Connection" + } + ], + "cookie": [], + "responseTime": 0, + "body": "" + } + ] + } + ] + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-sample.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-sample.json new file mode 100644 index 000000000..8fcd26fac --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-sample.json @@ -0,0 +1,412 @@ +{ + "variables": [], + "info": { + "name": "Petstore API", + "_postman_id": "c0bc3513-c83a-8b5d-bbff-1a4a0dc001d7", + "description": "version=1.0 - This is a PetStore API description", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "pet", + "description": "", + "item": [ + { + "name": "Finds Pets by status", + "request": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=998bac0775b1d5f588e0a6ca7c11b852&status=available", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true, + "description": "" + }, + { + "key": "status", + "value": "available", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "" + }, + "response": [ + { + "id": "c7e562f0-3d87-4925-96d1-c38e0dc4c0d6", + "name": "available response", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=70f735676ec46351c6699c4bb767878a&status=available", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "user_key", + "value": "70f735676ec46351c6699c4bb767878a", + "equals": true, + "description": "" + }, + { + "key": "status", + "value": "available", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "" + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "" + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "" + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:31:45 GMT", + "description": "" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.15.1", + "description": "" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "" + }, + { + "name": "transfer-encoding", + "key": "transfer-encoding", + "value": "chunked", + "description": "" + } + ], + "cookie": [], + "responseTime": 1466, + "body": "[\n {\n \"id\": 190192062,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192063,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192285,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192654,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192671,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192727,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192736,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192768,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192878,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192907,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190193000,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": -98125093,\n \"category\": {\n \"id\": -517488397,\n \"name\": \"EJvNbK\"\n },\n \"name\": \"LuEfMZATrHz\",\n \"photoUrls\": [\n \"XCXOVVkaxa\",\n \"gNwYqHEmC\",\n \"nvCvphDeuqztysUBNed\",\n \"W\",\n \"vmrxRIViyXqumolLIeoB\",\n \"JRqHVxk\",\n \"tCUGbegVHoXajm\",\n \"UiHppQn\"\n ],\n \"tags\": [\n {\n \"id\": 727599428,\n \"name\": \"RemggEDzxPljbrlktdWf\"\n },\n {\n \"id\": 1987753751,\n \"name\": \"zWqdKAGHMmhPPlomljaNtuvm\"\n },\n {\n \"id\": 1251632392,\n \"name\": \"BAgtgtKOxZGdsS\"\n },\n {\n \"id\": -1813025208,\n \"name\": \"OkKxtfAkCMEICbbQDVPi\"\n },\n {\n \"id\": -730110346,\n \"name\": \"WshDF\"\n },\n {\n \"id\": 2100951153,\n \"name\": \"yxUFSknQEleIAQCoocl\"\n },\n {\n \"id\": -2135188117,\n \"name\": \"M\"\n },\n {\n \"id\": 1352243140,\n \"name\": \"koKHsjysHXW\"\n },\n {\n \"id\": 1696778814,\n \"name\": \"KaihiyarcZkIzkkquWPZ\"\n },\n {\n \"id\": 659492963,\n \"name\": \"xqIzulcBPzWMyUpQwQK\"\n },\n {\n \"id\": -2118372841,\n \"name\": \"naYFGuHmqDqOpfHH\"\n }\n ],\n \"status\": \"available\"\n }\n]" + } + ] + }, + { + "name": "Find pet by ID", + "request": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/:petId?user_key={{user_key}}", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "{{user_key}}", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "key": "petId", + "value": "" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "" + }, + "response": [ + { + "id": "05486047-439f-408d-97e7-a9925107a2a1", + "name": "get-2", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.staging.apicast.io:443/v2/pet/:petId/?user_key=70f735676ec46351c6699c4bb767878a", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "staging", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId", + "" + ], + "query": [ + { + "key": "user_key", + "value": "70f735676ec46351c6699c4bb767878a", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "key": "petId", + "value": "2" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "" + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "" + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "" + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "" + }, + { + "name": "Content-Length", + "key": "Content-Length", + "value": "137", + "description": "" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:18 GMT", + "description": "" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "" + } + ], + "cookie": [], + "responseTime": 233, + "body": "{\"id\":2,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"}" + }, + { + "id": "187fb73f-edbb-47d4-89ae-7e3e4a8cfcbd", + "name": "get-1", + "originalRequest": { + "url": { + "raw": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/:petId?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "https", + "host": [ + "petstore-api-2445581593402", + "apicast", + "io" + ], + "port": "443", + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "key": "petId", + "value": "1" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "", + "_postman_previewtype": "parsed", + "header": [ + { + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Headers" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Methods" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "", + "type": "text", + "name": "Access-Control-Allow-Origin" + }, + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "" + }, + { + "key": "Content-Length", + "value": "0", + "description": "", + "type": "text", + "name": "Content-Length" + }, + { + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "", + "type": "text", + "name": "Server" + }, + { + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:22 GMT", + "description": "", + "type": "text", + "name": "Date" + }, + { + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "", + "type": "text", + "name": "X-Powered-By" + }, + { + "key": "Connection", + "value": "keep-alive", + "description": "", + "type": "text", + "name": "Connection" + } + ], + "cookie": [], + "responseTime": 0, + "body": "" + } + ] + } + ] + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-v1.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-v1.json new file mode 100644 index 000000000..0e11eb9b0 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection-v1.json @@ -0,0 +1,304 @@ +{ + "id": "c0bc3513-c83a-8b5d-bbff-1a4a0dc001d7", + "name": "Petstore API", + "description": "version=1.0 - This is a PetStore API description", + "order": [ + "85dea94d-1f68-3931-b877-a078f5b8966d", + "d7af0c2d-7698-4cd3-9921-b568aa47c201", + "54bbed70-260b-6113-32e0-6ade44b411ec" + ], + "folders": [], + "requests": [ + { + "id": "54bbed70-260b-6113-32e0-6ade44b411ec", + "headers": "", + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/1?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "pathVariables": {}, + "preRequestScript": null, + "method": "GET", + "collectionId": "c0bc3513-c83a-8b5d-bbff-1a4a0dc001d7", + "data": null, + "dataMode": "params", + "name": "findById 1 (404)", + "description": "", + "descriptionFormat": "html", + "time": 1481038225054, + "version": 2, + "responses": [ + { + "status": "", + "responseCode": { + "code": 404, + "name": "Not Found" + }, + "time": 18, + "headers": [], + "cookies": [], + "mime": "", + "text": "", + "language": "", + "rawDataType": "", + "previewType": "parsed", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": true, + "name": "1 response", + "id": "0540b712-10bc-bab1-cdc8-397a77694686", + "request": { + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/1?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "pathVariables": {}, + "pathVariableData": [], + "queryParams": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true, + "description": "", + "enabled": true + } + ], + "headerData": [], + "headers": "", + "data": null, + "method": "GET", + "dataMode": "params" + } + } + ], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "isFromCollection": true, + "collectionRequestId": "5db387e3-7732-d87e-d05a-f3fdef95b23f" + }, + { + "id": "85dea94d-1f68-3931-b877-a078f5b8966d", + "headers": "", + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=998bac0775b1d5f588e0a6ca7c11b852&status=available", + "pathVariables": {}, + "preRequestScript": null, + "method": "GET", + "collectionId": "c0bc3513-c83a-8b5d-bbff-1a4a0dc001d7", + "data": null, + "dataMode": "params", + "name": "findByStatus available", + "description": "", + "descriptionFormat": "html", + "time": 1481032740622, + "version": 2, + "responses": [ + { + "status": "", + "responseCode": { + "code": 200, + "name": "OK", + "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action." + }, + "time": 1466, + "headers": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request." + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request." + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "Options that are desired for the connection" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:31:45 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.15.1", + "description": "A name for the server" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)" + }, + { + "name": "transfer-encoding", + "key": "transfer-encoding", + "value": "chunked", + "description": "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity." + } + ], + "cookies": [], + "mime": "", + "text": "[{\"id\":190192062,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192063,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192285,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192654,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192671,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192727,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192736,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192768,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192878,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192907,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190193000,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":-98125093,\"category\":{\"id\":-517488397,\"name\":\"EJvNbK\"},\"name\":\"LuEfMZATrHz\",\"photoUrls\":[\"XCXOVVkaxa\",\"gNwYqHEmC\",\"nvCvphDeuqztysUBNed\",\"W\",\"vmrxRIViyXqumolLIeoB\",\"JRqHVxk\",\"tCUGbegVHoXajm\",\"UiHppQn\"],\"tags\":[{\"id\":727599428,\"name\":\"RemggEDzxPljbrlktdWf\"},{\"id\":1987753751,\"name\":\"zWqdKAGHMmhPPlomljaNtuvm\"},{\"id\":1251632392,\"name\":\"BAgtgtKOxZGdsS\"},{\"id\":-1813025208,\"name\":\"OkKxtfAkCMEICbbQDVPi\"},{\"id\":-730110346,\"name\":\"WshDF\"},{\"id\":2100951153,\"name\":\"yxUFSknQEleIAQCoocl\"},{\"id\":-2135188117,\"name\":\"M\"},{\"id\":1352243140,\"name\":\"koKHsjysHXW\"},{\"id\":1696778814,\"name\":\"KaihiyarcZkIzkkquWPZ\"},{\"id\":659492963,\"name\":\"xqIzulcBPzWMyUpQwQK\"},{\"id\":-2118372841,\"name\":\"naYFGuHmqDqOpfHH\"}],\"status\":\"available\"},{\"id\":-68132066,\"category\":{\"id\":-91395365,\"name\":\"FmMkqZRSCOaAThTjTIcolI\"},\"name\":\"bpUDswwAfGEyuZYAyCqaSZS\",\"photoUrls\":[\"EDwpbUPQtkQTvN\",\"pISYlIfScAw\",\"okQ\",\"guIVyggGwnGe\",\"mgvbRLAtzqLwaxOCdOtK\",\"JIYGRxq\",\"cVcdQVCnELszLNIfNwAQUUL\",\"cyPU\",\"VCqurkja\"],\"tags\":[{\"id\":-1860308527,\"name\":\"Ypyvo\"},{\"id\":740966829,\"name\":\"EoUdEnBRKaYnrAwemm\"},{\"id\":2118690298,\"name\":\"GNN\"},{\"id\":-66381237,\"name\":\"LRM\"},{\"id\":839909562,\"name\":\"NbwCTvnFCtQkfh\"},{\"id\":216887794,\"name\":\"s\"},{\"id\":193060448,\"name\":\"eferBLgSJmhevsc\"},{\"id\":-266174447,\"name\":\"IKqwZHPvgCEhMlCuW\"},{\"id\":-887416107,\"name\":\"ZQH\"},{\"id\":-588775660,\"name\":\"LGHzdgipDktfQ\"}],\"status\":\"available\"},{\"id\":1989942593,\"category\":{\"id\":-1050210553,\"name\":\"tIIloCsOtWWFktrnRtuElv\"},\"name\":\"IPJNpqeajFWeuqgHoTfXEBX\",\"photoUrls\":[\"F\",\"nqVfXMsVjVRmVuOXWjWlT\",\"VEoPbexHbLDYTzyBzsQJd\",\"ZmWwLXozFHBsWgrjaffHfMb\",\"XGWKr\",\"erMZKquTGGmdamyrdGy\",\"OUqvpjWRpNXBoHqIIu\",\"cpMzSPnuXyvFi\",\"PFRlBWNVDeoBVLVlhB\"],\"tags\":[{\"id\":1886264408,\"name\":\"BjrHnahKzYCvFZKlBznn\"},{\"id\":-1789173402,\"name\":\"rBakdYK\"},{\"id\":442240572,\"name\":\"VpgFcmhiFPpfb\"},{\"id\":1715588737,\"name\":\"tD\"},{\"id\":-551476096,\"name\":\"lTneHCv\"},{\"id\":-914787304,\"name\":\"VsJlrbNUrpkMnyHgFxE\"},{\"id\":522671708,\"name\":\"X\"},{\"id\":681668409,\"name\":\"VEBMhHClSpeaa\"},{\"id\":-827194980,\"name\":\"iKigvtAcWvzhAnqQRDx\"}],\"status\":\"available\"},{\"id\":7941546543762313289,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":-571816035,\"category\":{\"id\":-383000061,\"name\":\"pDkzvcKQAPRhRLEAhKzHJd\"},\"name\":\"DRjzW\",\"photoUrls\":[\"OfDr\",\"ibBFe\",\"IJrgezWuPJOxahnebHK\",\"wXsRoCYOlGSaCIKPTaVsUNG\",\"uNnFXaSVK\",\"ZGZEzluvbTr\",\"cpzoVGjsdMI\",\"XIJKuAeMrjQYeeiw\",\"ernTpbNhCBBFwmnPxI\",\"YCuIagVzY\",\"lGp\"],\"tags\":[{\"id\":505181167,\"name\":\"WzaaytigrGbmAJneh\"},{\"id\":-207595508,\"name\":\"zQgfduqsOc\"},{\"id\":915506540,\"name\":\"RXSASezQnhej\"},{\"id\":-524908203,\"name\":\"QCBrZZgFrgpfAbMP\"}],\"status\":\"available\"},{\"id\":828402747,\"category\":{\"id\":160060140,\"name\":\"vMa\"},\"name\":\"wEbazmFfDopuCSNLJAbIPA\",\"photoUrls\":[\"kbHWHMBdDRYPlxbWdKTP\",\"QKlLP\",\"xWdxuLXmhEimjtGlBLr\",\"qMYnprRqwUF\",\"xGkLzSjCCeGevw\",\"AQHpALYdkKNCWYugtYV\",\"S\",\"MthDcf\",\"jKzjvMRIkdVPHpz\"],\"tags\":[{\"id\":2091383900,\"name\":\"qDkxCxCFSnqwITTteFq\"},{\"id\":-1169806954,\"name\":\"Dh\"},{\"id\":-828501234,\"name\":\"WgxTsIV\"},{\"id\":1974844324,\"name\":\"sGXOzQUWscDHdIx\"},{\"id\":-1604579899,\"name\":\"BJojDCvzcRJriGAx\"},{\"id\":1661915405,\"name\":\"BMbVnuoyKyLmcFodVK\"},{\"id\":1242922733,\"name\":\"HWHGaZaIcWOKMcdaVV\"},{\"id\":-1658771766,\"name\":\"YYLivDLXsmfMYAbsHWl\"}],\"status\":\"available\"},{\"id\":8739826599258044469,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044511,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cano\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044524,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cazzo di cane\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044690,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044748,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044837,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044876,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044976,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044979,\"category\":{\"id\":0,\"name\":\"string\"},\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258044982,\"category\":{\"id\":0,\"name\":\"string\"},\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045054,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045091,\"category\":{\"id\":2,\"name\":\"Cats\"},\"name\":\"Testing\",\"photoUrls\":[\"url1\",\"url2\"],\"tags\":[{\"id\":1,\"name\":\"tag2\"},{\"id\":2,\"name\":\"tag3\"}],\"status\":\"available\"},{\"id\":8739826599258045174,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045346,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045515,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045619,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045700,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045701,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045719,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045765,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258045974,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258046028,\"category\":{\"id\":56,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258046697,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258046743,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258046986,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258046996,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258047056,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258047198,\"category\":{\"id\":22222,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":4000,\"category\":{\"id\":22222,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258047225,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":123654,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"BerniDoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258047803,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258048226,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258048263,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258048300,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258048343,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258048441,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258048448,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258048494,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258048503,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049000,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"charlie_cactus\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049343,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049489,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049623,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049644,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049661,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049762,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049767,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049808,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258049862,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050079,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050105,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050110,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050211,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050214,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050217,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050280,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050319,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1233979806,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":875247351,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":729860794,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":496205625,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258050539,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050589,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050604,\"category\":{\"id\":1,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050605,\"category\":{\"id\":8,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050734,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050761,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050804,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258050954,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258051064,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1896501265,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1914188565,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":2042432507,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1895798423,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":820841306,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":126059826,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":290850928,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258051370,\"category\":{\"id\":0},\"name\":\"\",\"photoUrls\":[],\"tags\":[{\"id\":0}],\"status\":\"available\"},{\"id\":8739826599258051371,\"category\":{\"id\":0},\"name\":\"\",\"photoUrls\":[],\"tags\":[{\"id\":0}],\"status\":\"available\"},{\"id\":8739826599258051373,\"category\":{\"id\":0},\"name\":\"\",\"photoUrls\":[],\"tags\":[{\"id\":0}],\"status\":\"available\"},{\"id\":8739826599258051377,\"category\":{\"id\":0},\"name\":\"\",\"photoUrls\":[],\"tags\":[{\"id\":0}],\"status\":\"available\"},{\"id\":8739826599258051378,\"category\":{\"id\":0},\"name\":\"\",\"photoUrls\":[],\"tags\":[{\"id\":0}],\"status\":\"available\"},{\"id\":8739826599258051443,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258051556,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258051558,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258051584,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258051627,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052079,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052232,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052707,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052736,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052745,\"category\":{\"id\":1,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052775,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052826,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052833,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052958,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258052959,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053150,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053256,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053281,\"category\":{\"id\":0,\"name\":\"collie\"},\"name\":\"fido\",\"photoUrls\":[\"http://www.autocarpro.in/IMG/434/8434/tvs-wego.jpg\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258053298,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053300,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053364,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053369,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053370,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053379,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053400,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053567,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053572,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053637,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053646,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053663,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053765,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053930,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258053939,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054101,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054178,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1731649842,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258054204,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1054388406,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1065270692,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1738523854,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1134664371,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":546409016,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":841309002,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":587399387,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1329600786,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1890445681,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":211691395,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258054253,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054290,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054307,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054333,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054385,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054466,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054578,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054585,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054592,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054593,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054643,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054683,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054685,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054693,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054694,\"category\":{\"id\":0,\"name\":\"DOG\"},\"name\":\"doggie\",\"photoUrls\":[\"http://xxx\"],\"tags\":[{\"id\":0,\"name\":\"dog\"}],\"status\":\"available\"},{\"id\":8739826599258054715,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054737,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054746,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054749,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1978985922,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258054873,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054880,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054881,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054920,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054929,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054930,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054932,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054936,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054940,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054946,\"category\":{\"id\":0,\"name\":\"111\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054950,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258054992,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055005,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055067,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055130,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055155,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055156,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055161,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055175,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":818912,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":818913,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":818914,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":818915,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":818916,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":818918,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":130514,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":130515,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":130516,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":130517,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":130518,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":130520,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":8739826599258055314,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055331,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055345,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055354,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055355,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":123987123,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"314234\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055387,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055388,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055396,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055398,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055399,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055406,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055413,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055420,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055427,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055436,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055442,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1234578,\"category\":{\"id\":4013,\"name\":\"dogs\"},\"name\":\"tommy\",\"photoUrls\":[\"http://example.com/photo.jpg\"],\"tags\":[{\"id\":4,\"name\":\"dogName123\"}],\"status\":\"available\"},{\"id\":8739826599258055478,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055483,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055488,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055509,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1241425344,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258055540,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055613,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055622,\"category\":{\"id\":0,\"name\":\"Dog\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"Fluffy\"}],\"status\":\"available\"},{\"id\":8739826599258055623,\"category\":{\"id\":0,\"name\":\"Dog\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"Fluffy\"}],\"status\":\"available\"},{\"id\":8739826599258055644,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055680,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055731,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055792,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055797,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055802,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055806,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055824,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055849,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055878,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055903,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258055932,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056063,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056083,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056096,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056165,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056259,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056272,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056309,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056310,\"category\":{\"id\":1,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056319,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056333,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056373,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1616,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"google\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056391,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056403,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056464,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056494,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056495,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056499,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056513,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056550,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056551,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056583,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056593,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"MyPetAMoi\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056602,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056607,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"MyPetAAlex\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056635,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056638,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056649,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056667,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056680,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056697,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056792,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056794,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056798,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056808,\"category\":{\"id\":0,\"name\":\"Moataz Test\"},\"name\":\"Zizo\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":388932,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"My Test in Pets\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258056897,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057011,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057012,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057067,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057086,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057096,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057105,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057107,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057119,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057123,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057129,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057150,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057158,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057204,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057205,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057215,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057219,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057232,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057233,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057243,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057244,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057245,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057246,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057252,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057256,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057309,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057358,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057420,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057454,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057471,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057480,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057542,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057562,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057572,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":546879787,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258057609,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057630,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057652,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[{\"id\":1,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057656,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057661,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057666,\"name\":\"doggie\",\"photoUrls\":[\"foto1\",\"foto2\",\"foto3\"],\"tags\":[{\"id\":1,\"name\":\"etiqueta1\"}],\"status\":\"available\"},{\"id\":8739826599258057667,\"name\":\"doggie\",\"photoUrls\":[\"foto1\",\"foto2\",\"foto3\"],\"tags\":[{\"id\":1,\"name\":\"etiqueta1\"}],\"status\":\"available\"},{\"id\":-183995778,\"category\":{\"id\":1068182148,\"name\":\"apmQbifQGIdlUs\"},\"name\":\"aKkAGJ\",\"photoUrls\":[\"lZi\",\"WYcBHbldKQOqr\",\"UbIFwSZ\",\"u\"],\"tags\":[{\"id\":-415060162,\"name\":\"eWRuNONeulAg\"},{\"id\":-2090275102,\"name\":\"cC\"},{\"id\":240557353,\"name\":\"GeIGwoax\"},{\"id\":-308070012,\"name\":\"WngEHYshOPbTQaHAyueY\"},{\"id\":1621342826,\"name\":\"umeDq\"},{\"id\":974775150,\"name\":\"bOYiNBpehPYzH\"}],\"status\":\"available\"},{\"id\":-2061635395,\"category\":{\"id\":49536909,\"name\":\"QFZxjoGCBBgGfsjEowD\"},\"name\":\"xyhWVodbJLPOaTtlSOUp\",\"photoUrls\":[\"YMIkiGuiLtgKBB\",\"VqMBwKtUPB\",\"ym\",\"lRmIfPWiDOONcsqfBwsrTYdo\",\"CjlkbZrCpAvPsFEvfb\",\"CvQZiD\",\"KfOVvKWHaWucSCaXadWocn\",\"hfcdkgFDdimU\",\"cvgt\",\"wIFvq\"],\"tags\":[{\"id\":1912910331,\"name\":\"cZ\"},{\"id\":1407376528,\"name\":\"qXaTIUayG\"},{\"id\":762963409,\"name\":\"ggKYxYZFUoa\"},{\"id\":-181975149,\"name\":\"ioHTaLWKOndRGJjaOVoRFJnS\"},{\"id\":493710917,\"name\":\"vaNSAEDPJfrK\"}],\"status\":\"available\"},{\"id\":-2147483648,\"category\":{\"id\":2147483646,\"name\":\"c\"},\"name\":\"a\",\"photoUrls\":[\"dIxgGwrHWUEjPYpQFtIdzvwJ\",\"t\",\"S\",\"TTirZKxnjTEgxdikVhvGoVxV\",\"TecvsDBrcqsnZfVBCFXyziPY\",\"i\",\"YtkTEDOuTZZCalCMHFcumgYW\",\"stAeaDEojNmbwFdzLEKPGYEz\",\"x\"],\"tags\":[{\"id\":-2147483648,\"name\":\"SCGQEAUUAyYepOcQIFZLqKmC\"},{\"id\":-2147483648,\"name\":\"I\"},{\"id\":2147483646,\"name\":\"eISLtvdiUDssBWOhmLTMXfpZ\"}],\"status\":\"available\"},{\"id\":98989,\"category\":{\"id\":98989,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057913,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258057950,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058024,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058092,\"category\":{\"id\":0,\"name\":\"monstar\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058191,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058203,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058216,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058219,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1500,\"category\":{\"id\":0,\"name\":\" \"},\"name\":\"butch\",\"photoUrls\":[\" \"],\"tags\":[{\"id\":0,\"name\":\" \"}],\"status\":\"available\"},{\"id\":1501,\"category\":{\"id\":0,\"name\":\" \"},\"name\":\"stormy\",\"photoUrls\":[\" \"],\"tags\":[{\"id\":0,\"name\":\" \"}],\"status\":\"available\"},{\"id\":8739826599258058243,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"valentin\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058248,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058253,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058339,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058372,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058388,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058482,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058491,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058495,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058512,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8968674,\"category\":{\"id\":0,\"name\":\"Monster\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058522,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058686,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Hana\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":5454545,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058745,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058750,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058755,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058784,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058916,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058917,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258058919,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192061,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059005,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059014,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059019,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059028,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059041,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059053,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059074,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059148,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059177,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059182,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059218,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059245,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059266,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059275,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":357,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"pepe\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059416,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059474,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":9456,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggifrts\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059527,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059600,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059645,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059698,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059707,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059737,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258059930,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060004,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060008,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060103,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060152,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060189,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060210,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060228,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060265,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060314,\"category\":{\"id\":0,\"name\":\"dogs\"},\"name\":\"supper doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060315,\"category\":{\"id\":0,\"name\":\"dogs\"},\"name\":\"supper doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060344,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060345,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060366,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060367,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060405,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":999990,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"billzhang\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060452,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060540,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060565,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060570,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060611,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060612,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":16165,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":77771239,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiebeair\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060618,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":40401111,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Barkley\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060682,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060691,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060703,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060705,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":7431699383703892594,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060716,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060772,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060782,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060794,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060796,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060812,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":786158,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"peanutwasbetter\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060833,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060838,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060863,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060865,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060889,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060967,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258060988,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061025,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061045,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061063,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061067,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061108,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061126,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1414974222,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258061131,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":20161101,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Hardlopertje\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061140,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061144,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061186,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061190,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061196,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061208,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061209,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061215,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061224,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061230,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061243,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061256,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061274,\"category\":{\"id\":0,\"name\":\"First\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"TAG1\"}],\"status\":\"available\"},{\"id\":8739826599258061278,\"category\":{\"id\":0,\"name\":\"First\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"TAG1\"}],\"status\":\"available\"},{\"id\":8739826599258061287,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061304,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061305,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061306,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061307,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061316,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":77,\"category\":{\"id\":4,\"name\":\"lions\"},\"name\":\"Kira\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061321,\"category\":{\"id\":4,\"name\":\"Lions\"},\"name\":\"MyPetDog\",\"photoUrls\":[\"htttp://some.url.com\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258061345,\"category\":{\"id\":2,\"name\":\"Bauernhoftier\"},\"name\":\"Linda\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061359,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061372,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061377,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061389,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061390,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061420,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061460,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061534,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061539,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061545,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061566,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061595,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061658,\"category\":{\"id\":0,\"name\":\"Bird\"},\"name\":\"Birdie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"bird\"}],\"status\":\"available\"},{\"id\":8739826599258061679,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061683,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061700,\"category\":{\"id\":0,\"name\":\"bird\"},\"name\":\"birdie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"PiouPiou\"}],\"status\":\"available\"},{\"id\":8739826599258061727,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061731,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061745,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061747,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061815,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061817,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061850,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061873,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061874,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061936,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061941,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258061957,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062013,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062133,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062145,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062211,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062328,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062333,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062335,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062363,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062365,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062366,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062367,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062368,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062369,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062387,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062443,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062480,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062484,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062524,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062526,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062529,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062548,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062575,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062588,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062655,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062666,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062669,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062729,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062730,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062760,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"superdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062765,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"superdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062767,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"superdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062768,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"superdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062769,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"superdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062770,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"superdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062777,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062795,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062832,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062841,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062870,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062911,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062920,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062922,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258062975,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063002,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063008,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063049,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063051,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063060,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063089,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063094,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":218649003,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1140749743,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258063151,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063161,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063181,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063183,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggieXX\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063184,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggieXX\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063237,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063258,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063265,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063286,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063290,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"http://monurl.com\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063295,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"http://monurl.com\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063300,\"name\":\"toto\",\"photoUrls\":[\"http://monurl.com\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258063302,\"name\":\"toto\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":975050,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":975051,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":975052,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":975053,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":975054,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":975056,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":8739826599258063426,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063442,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063504,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063548,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063577,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063643,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063716,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063826,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063838,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063872,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063929,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258063965,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064003,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064008,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064014,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064018,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064021,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":572656770,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258064025,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1875850496,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1089172452,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1530209852,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258064070,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":782565173,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":169417756,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":135076109,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1681759985,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258064114,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064116,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064120,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064141,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064151,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064243,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1319,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"krüüüten\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":839679681,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1193534937,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1731325711,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":172019689,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":963041419,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":668908074,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":458258991,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258064345,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1461367554,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1774989390,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":428863110,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":294417922,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1573221456,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258064379,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064380,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1537706562,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":614723764,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1924148178,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":958105495,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258064406,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":800646059,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":823911523,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":180040753,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":518479124,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":760032101,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":49703183,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1379346965,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":5000,\"category\":{\"id\":150,\"name\":\"dog\"},\"name\":\"maggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":50,\"name\":\"Smart\"}],\"status\":\"available\"},{\"id\":5001,\"category\":{\"id\":150,\"name\":\"dog\"},\"name\":\"marcie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":50,\"name\":\"Smart\"}],\"status\":\"available\"},{\"id\":8739826599258064458,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064461,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064464,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064503,\"category\":{\"id\":0},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258064541,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064577,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064580,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064587,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064645,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064646,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064751,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064767,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064843,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064847,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064848,\"category\":{\"id\":1,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258064956,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065060,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065065,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065083,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065120,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065129,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065167,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065256,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065260,\"category\":{\"id\":0,\"name\":\"rongxing's dog\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065269,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Rong's doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065315,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065376,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065453,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065457,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065459,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065509,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065617,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065630,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065631,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065637,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065737,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065787,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065797,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065805,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065886,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065888,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"culo\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258065897,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"culo\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066142,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066236,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066241,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"husky\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066243,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"husky\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066296,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066369,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066371,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066397,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066422,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066426,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066427,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066468,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066478,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066495,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":99999,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"JJ Lin\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066528,\"category\":{\"id\":33699,\"name\":\"string\"},\"name\":\"jj_lin\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066582,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066583,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066663,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066758,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066762,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066764,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066767,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"piggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066792,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066830,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066872,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066913,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066926,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258066961,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067039,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067130,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067133,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067163,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067192,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"sigbin\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067220,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067253,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067339,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":28073424237421,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Mr. Hunde-Wollenmonster\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067376,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067382,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067391,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067400,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067401,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067406,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067456,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067491,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067497,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067531,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067540,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":23792735,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Mr. DogCatHamsterMouse\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067601,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067613,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067618,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067620,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067632,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067633,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067639,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067643,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067652,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067655,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067658,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067668,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067670,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067673,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067679,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067708,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067716,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067726,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067747,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067748,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067777,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067814,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067872,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067881,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067882,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067898,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067911,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067917,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258067933,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068010,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068029,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068038,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068077,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068080,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068110,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068118,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068131,\"category\":{\"id\":0,\"name\":\"malcome\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068133,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1110,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"dddoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068155,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":124,\"category\":{\"id\":0,\"name\":\"mastif\"},\"name\":\"pooch\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068238,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068251,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068280,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068296,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068361,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"DEE DOGGIE\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068367,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"DEE DOGGIE\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068387,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068388,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068389,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068391,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"ssdfds\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068407,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068408,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068436,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068445,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068485,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068486,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068488,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068495,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068500,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"wuffie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068502,\"category\":{\"id\":10,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068526,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068535,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068585,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068614,\"category\":{\"id\":0,\"name\":\"sadfasdf\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"asdf\"}],\"status\":\"available\"},{\"id\":8739826599258068624,\"category\":{\"id\":0,\"name\":\"new category\"},\"name\":\"FOO\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068654,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068679,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068681,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068682,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068686,\"category\":{\"id\":0,\"name\":\"1\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068691,\"category\":{\"id\":0,\"name\":\"1\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068696,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068717,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068718,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068727,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068728,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068749,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068750,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068768,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068797,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068802,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258068804,\"name\":\"PierreTestDog\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258068825,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068879,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068895,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":916258993,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":2007483992,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":759106830,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1840549552,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258068949,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068963,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258068967,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069005,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069019,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069043,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069065,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069098,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069103,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069108,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069114,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069119,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069128,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069157,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069169,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069176,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069191,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069213,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069254,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069258,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069259,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069334,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069339,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069357,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069401,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069474,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":729805904,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1841547532,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":870750105,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1227870827,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258069492,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069500,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069502,\"category\":{\"id\":0,\"name\":\"Hello\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069503,\"category\":{\"id\":0,\"name\":\"Hello2\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069508,\"category\":{\"id\":0,\"name\":\"Tester\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069578,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":204143264,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1598944276,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1423475130,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1472266289,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":301704332,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":745946262,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1503432204,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":346896641,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258069724,\"category\":{\"id\":1,\"name\":\"foos\"},\"name\":\"Foo\",\"photoUrls\":[\"photo1\"],\"tags\":[{\"id\":13,\"name\":\"cute\"}],\"status\":\"available\"},{\"id\":8739826599258069810,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"kitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069837,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069841,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069843,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069851,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"http://test.com/2016-11-30T18:47:53+00:00default.png\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069864,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069909,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069911,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258069919,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":234372172,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1009685017,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258070110,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070119,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070132,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070154,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070408,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070442,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070498,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":780,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070503,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070528,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070529,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070568,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070616,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070689,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070699,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070737,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070764,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070821,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070854,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258070897,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071053,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071164,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071212,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071283,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071311,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071623,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071627,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071648,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071657,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071686,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071819,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071820,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071821,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071822,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071888,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071898,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071902,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071903,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"super pet 3\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071912,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"snoop dog3\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258071953,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072042,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072044,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072045,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072170,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072179,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072196,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072197,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072202,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258072203,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258072204,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258072209,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258072214,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072333,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072471,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072563,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072604,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072618,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"kitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"Minni\"}],\"status\":\"available\"},{\"id\":77777,\"category\":{\"id\":77777,\"name\":\"hyoon構欺運型Ääăą\"},\"name\":\"hyoon構欺運型Ääăą\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"hyoon構欺運型Ääăą\"}],\"status\":\"available\"},{\"id\":8739826599258072659,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072677,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072801,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072810,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072900,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258072997,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073043,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073057,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073113,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073118,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073147,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073188,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073189,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073214,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073216,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073248,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073261,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073371,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073385,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073401,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073403,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073407,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073424,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073429,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073442,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073456,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073461,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073472,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073506,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073519,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073562,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073610,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073615,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073628,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073645,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073662,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073677,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073700,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"felix\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073701,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073715,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":101,\"category\":{\"id\":111,\"name\":\"German\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":20,\"name\":\"Labodor\"}],\"status\":\"available\"},{\"id\":8739826599258073748,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"felix\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073753,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073758,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073763,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073788,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"felix\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073792,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":24892,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Kungfoopanda\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073797,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":248921,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Kungfoopanda1\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073802,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073815,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073820,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073829,\"category\":{\"id\":0,\"name\":\"tom\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073838,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073851,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073852,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073857,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073866,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073913,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073923,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073926,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073931,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073936,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073947,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073951,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073955,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073956,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073961,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073966,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073967,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073976,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073977,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073978,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073983,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073984,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073985,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073994,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073995,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073996,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073997,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073998,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258073999,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":502762354,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258074012,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074013,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074014,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074015,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074016,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074017,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1008947357,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258074034,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074035,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074036,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074037,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074038,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074040,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074044,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074045,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074050,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"fred\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074052,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074054,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074055,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074087,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074089,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074090,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074109,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074110,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074111,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074125,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074126,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074127,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074152,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074161,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074162,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074163,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074176,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074177,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074178,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074179,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074180,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074181,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074188,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074204,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HelloKitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074205,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074206,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Snoopy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074223,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074228,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074238,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074239,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074243,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074244,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074266,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074269,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074276,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074293,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2010044471,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258074322,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1894403371,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258074404,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074406,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074446,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074447,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074468,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074469,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074498,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074511,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074552,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258074704,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074713,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"iain\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074718,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie111\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074727,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074732,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":131,\"category\":{\"id\":0,\"name\":\"\"},\"name\":\"Kosekosen\",\"photoUrls\":[\"\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074798,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074823,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074824,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074829,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074830,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074835,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":399083785,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":190191173,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"test\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190191133,\"name\":\"test\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1459849232,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1485802231,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1027387015,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258074870,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074873,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074890,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074907,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074916,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074931,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074955,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074960,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074965,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258074966,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075023,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075028,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075037,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie a\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075074,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075075,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075076,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075101,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075102,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075103,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075122,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie a\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075145,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie a\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075150,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Ndoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075151,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Ndoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075153,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Ndoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075157,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie a\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075158,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Ndoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075163,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Ndoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075184,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258075189,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258075252,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075281,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075290,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075405,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075411,\"category\":{\"id\":0,\"name\":\"Baily\"},\"name\":\"doggie\",\"photoUrls\":[\"url1\"],\"tags\":[{\"id\":0,\"name\":\"One\"}],\"status\":\"available\"},{\"id\":8739826599258075432,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075445,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075455,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075467,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075514,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cats\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075515,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cats\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075524,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cats\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075537,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075539,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075564,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075669,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075729,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075793,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075794,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075823,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"kitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":10000000,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075848,\"category\":{\"id\":0,\"name\":\"aves\"},\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"verde\"}],\"status\":\"available\"},{\"id\":8739826599258075909,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075934,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258075959,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076055,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076078,\"category\":{\"id\":0,\"name\":\"\"},\"name\":\"\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"\"}],\"status\":\"available\"},{\"id\":8739826599258076079,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076112,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":16851750,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Proud King Max\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076185,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076187,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":131609,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"ibumed\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076388,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076389,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076390,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076399,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076401,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076469,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076475,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076648,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076653,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076771,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076772,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"felix\"}],\"status\":\"available\"},{\"id\":8739826599258076794,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2274,\"category\":{\"id\":101,\"name\":\"Animal\"},\"name\":\"doggie\",\"photoUrls\":[\"/img/1.png\",\"/img/2.png\"],\"tags\":[{\"id\":2001,\"name\":\"Pet\"}],\"status\":\"available\"},{\"id\":8739826599258076831,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076832,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076894,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076940,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076952,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076953,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076975,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258076984,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077025,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077159,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077187,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077196,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077209,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258077214,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077216,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077231,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077232,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077235,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077240,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077249,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077254,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077255,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077260,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077261,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077270,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077283,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077312,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077337,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077338,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077351,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077352,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077353,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077354,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077356,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077357,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077361,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077362,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077364,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077366,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077369,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077423,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077424,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077425,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077430,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077431,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077436,\"category\":{\"id\":8739826599258077431,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077463,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077466,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077471,\"category\":{\"id\":8739826599258077466,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077488,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077489,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077504,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077505,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077519,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bugs\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077520,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bunny\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077546,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077792,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258077981,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078032,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078065,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078119,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078180,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078191,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078198,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078361,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078486,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078519,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078524,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":77812,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"hello_buddy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078621,\"name\":\"PierresDog\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"foobar\"}],\"status\":\"available\"},{\"id\":8739826599258078645,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1430083797,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258078764,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078801,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258078841,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":319956970,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258078991,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079044,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079145,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079178,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079187,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":9633,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiepet\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"pet\"}],\"status\":\"available\"},{\"id\":8739826599258079213,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079215,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079243,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079248,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079257,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079270,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079355,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079360,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079409,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079414,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079451,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079452,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079461,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079471,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079472,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079477,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079498,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079523,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079524,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079525,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079526,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079539,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079544,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079549,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079570,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"test\"}],\"status\":\"available\"},{\"id\":8739826599258079579,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":96331,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiepet\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"pet\"}],\"status\":\"available\"},{\"id\":8739826599258079592,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079597,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079598,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079599,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079600,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079605,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079606,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":963311,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiepet\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"pet\"}],\"status\":\"available\"},{\"id\":9633111,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiepet\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"pet\"}],\"status\":\"available\"},{\"id\":8739826599258079615,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079632,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"test\"}],\"status\":\"available\"},{\"id\":8739826599258079637,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079719,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"dog\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079720,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079725,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"dog\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079743,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079748,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079761,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079766,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079767,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079780,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"test\"}],\"status\":\"available\"},{\"id\":8739826599258079809,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079813,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079814,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079824,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079829,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"test\"}],\"status\":\"available\"},{\"id\":8739826599258079829,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"test\"}],\"status\":\"available\"},{\"id\":8739826599258079838,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079875,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079896,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079905,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":49128,\"category\":{\"id\":0,\"name\":\"hansdodel\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079987,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079988,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258079994,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080035,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080064,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080069,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080070,\"category\":{\"id\":0,\"name\":\"lorem category\"},\"name\":\"lorem ipsum\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080079,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080096,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080113,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080118,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080131,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Pppo\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080184,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080189,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080190,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080227,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080232,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080250,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080255,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080284,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080382,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080395,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080432,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080438,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1993,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Akshay\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080464,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080469,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080474,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080484,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080516,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080571,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080584,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":65432,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Buddy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080678,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080761,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080763,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080769,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080770,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080779,\"category\":{\"id\":0,\"name\":\"fauna salvaje\"},\"name\":\"dora\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"grande\"}],\"status\":\"available\"},{\"id\":8739826599258080800,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080829,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":110484,\"category\":{\"id\":2,\"name\":\"xiaoxue\"},\"name\":\"xiaoxue\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258080989,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081020,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081061,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"frog\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081066,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081070,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"frog\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0}],\"status\":\"available\"},{\"id\":8739826599258081107,\"category\":{\"id\":1,\"name\":\"qw\"},\"name\":\"doggie\",\"photoUrls\":[\"\"],\"tags\":[{\"id\":1,\"name\":\"tag\"}],\"status\":\"available\"},{\"id\":8739826599258081196,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081262,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081267,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081312,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081317,\"category\":{\"id\":0,\"name\":\"pup\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":12345678,\"category\":{\"id\":1666,\"name\":\"dogs\"},\"name\":\"chapa\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081338,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081340,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081416,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081432,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1129190671,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258081526,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081555,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":29590331,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":642638259,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":742204593,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":214829769,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258081588,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081621,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081626,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081667,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081680,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081681,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081682,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258081687,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":340,\"category\":{\"id\":0,\"name\":\"sd222\"},\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258081696,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081717,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081718,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081719,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081752,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081762,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081787,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081792,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258081958,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":17971828,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"YoungManFranz\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082056,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082097,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":12312321,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie13\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082240,\"category\":{\"id\":1,\"name\":\"string\"},\"name\":\"doggie1\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082245,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082286,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082307,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082472,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082502,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082503,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082512,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082522,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082533,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082535,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082588,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082617,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082629,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082744,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082817,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258082958,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083000,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083001,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"kgkgk\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083006,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"kgkgk\",\"photoUrls\":[\"strijjjng\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083035,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083036,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083073,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083285,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083297,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":29635484,\"category\":{\"id\":0,\"name\":\"leito\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083552,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083605,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083855,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258083973,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258084287,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258084462,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258084693,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258085235,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258085364,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258085422,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258085660,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258085673,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258085806,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":229,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":804,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":95,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":825,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":715,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":603,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":164,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":843,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"2016-12-338000000\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":155,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"201612338100000\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":585,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"20161203100000\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":411,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"2016123100000\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":83,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258086522,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":942,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258086543,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":826,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":109,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":563,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":223,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":498,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258086599,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258086678,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":449,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":799,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":722,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":414,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":745,\"category\":{\"id\":0,\"name\":\"Carlitos\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":245,\"category\":{\"id\":0,\"name\":\"\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":951,\"category\":{\"id\":0,\"name\":\"Julieta\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":953,\"category\":{\"id\":0,\"name\":\"Julieta\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":179,\"category\":{\"id\":0,\"name\":\"Julieta\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":133,\"category\":{\"id\":0,\"name\":\"Julieta\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":448,\"category\":{\"id\":0,\"name\":\"Julieta\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258086787,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258086835,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258086966,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258087079,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258087592,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258087595,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258087701,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258087761,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258087851,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258087997,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258088062,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258088517,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"trogdor\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258088522,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":89898,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"speedy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1779449101,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":632734783,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1346186738,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1702781133,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":116116693,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1183891365,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":306944854,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":482601291,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258089587,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258089716,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258089891,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258089919,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258089921,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090016,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090590,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090615,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090660,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090769,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090771,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090773,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090779,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258090958,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258091140,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258091149,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258091262,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":707498418,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":532534103,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1070345344,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":274110806,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1550854413,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1071696075,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1744112312,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":842451578,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":896502677,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":961398956,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":370351991,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":717721122,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258091328,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":30,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258091404,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258091406,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258091407,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258091435,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1640703541,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1118549291,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":286929443,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":2049086350,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":252743803,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":927624271,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1462288018,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1043546813,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":468100706,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1035575409,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":12398,\"category\":{\"id\":123,\"name\":\"test\"},\"name\":\"funky\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"strong\"}],\"status\":\"available\"},{\"id\":1976956443,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":73516949,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":132643147,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258092039,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258092103,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258092207,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258092263,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258092388,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258092446,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":10005,\"category\":{\"id\":10005,\"name\":\"test php category\"},\"name\":\"PHP Unit Test\",\"photoUrls\":[],\"tags\":[{\"id\":10005,\"name\":\"test php tag\"}],\"status\":\"available\"},{\"id\":8739826599258092644,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258092754,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":112,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Anglo\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":113,\"category\":{\"id\":2,\"name\":\"Kleintier\"},\"name\":\"Theodor\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258092931,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258093206,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258093275,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094000,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1786514825,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":942854430,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":314791192,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":4650933,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":2050974054,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":221286337,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1183075726,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":456423486,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258094128,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":876932344,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":924470074,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1298069252,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258094267,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094317,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094474,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094632,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094637,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094642,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094656,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094664,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094666,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094708,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2333,\"category\":{\"id\":44,\"name\":\"category name lalala\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094847,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094972,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258094999,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1337,\"name\":\"foo\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258095189,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":692552891,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258095311,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1268236716,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1228644108,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":169525345,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1372936539,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":42,\"category\":{\"id\":42,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\",\"string2\"],\"tags\":[{\"id\":42,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258095489,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258095497,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258095647,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258095752,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258095801,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258095915,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096061,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096062,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096063,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096067,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096081,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096082,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096189,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096335,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096459,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096460,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096606,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096676,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096846,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096851,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096855,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258096992,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097013,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097083,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097092,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097105,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097118,\"name\":\"doggie\",\"photoUrls\":[\"123\"],\"tags\":[{\"id\":0,\"name\":\"123\"}],\"status\":\"available\"},{\"id\":8739826599258097120,\"name\":\"doggie\",\"photoUrls\":[\"123\"],\"tags\":[{\"id\":0,\"name\":\"123\"}],\"status\":\"available\"},{\"id\":8739826599258097133,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097135,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097144,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097124,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":8739826599258097124,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097196,\"name\":\"kelb\",\"photoUrls\":[\"https://www.google.com/url?sa=i&rct=j&q=&esrc=s&source=images&cd=&cad=rja&uact=8&ved=0ahUKEwj1ypqkstzQAhUCRiYKHWSxAmYQjBwIBA&url=http%3A%2F%2Fr.ddmcdn.com%2Fs_f%2Fo_1%2Fcx_633%2Fcy_0%2Fcw_1725%2Fch_1725%2Fw_720%2FAPL%2Fuploads%2F2014%2F11%2Ftoo-cute-doggone-it-video-playlist.jpg&psig=AFQjCNHi8SFCgNtwf3hPVIEABfxemnBUsw&ust=1481004616527191\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258097244,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097266,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097295,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097362,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097366,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097368,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097391,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097402,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097407,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097417,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097497,\"category\":{\"id\":0,\"name\":\"labra\"},\"name\":\"pj\",\"photoUrls\":[],\"tags\":[{\"id\":0,\"name\":\"pk\"}],\"status\":\"available\"},{\"id\":8739826599258097526,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097530,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097535,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097556,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097655,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097672,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097674,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string2\"}],\"status\":\"available\"},{\"id\":8739826599258097693,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097700,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097762,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097811,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097812,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097821,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097830,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097868,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097932,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097938,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097950,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097963,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097978,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097987,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258097999,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098008,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098018,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":789789,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098107,\"category\":{\"id\":0,\"name\":\"\"},\"name\":\"\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258098112,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098113,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098115,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098000,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cattie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098120,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098125,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098134,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"bob\"}],\"status\":\"available\"},{\"id\":8739826599258098138,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098139,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098141,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098149,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098162,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098163,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098168,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098169,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190191367,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"srihari\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098186,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098191,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098192,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"hardik\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098218,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098235,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098337,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098353,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098410,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098415,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098501,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098502,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098507,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098509,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098516,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098518,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098523,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098553,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098610,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098639,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098640,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098651,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098655,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098667,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098693,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098710,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098723,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098724,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":9080,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"構欺運型ÄäăąĎďĐđßनमस्ते안녕\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098733,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098734,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098739,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098752,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098758,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098767,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098780,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098804,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098813,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098846,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098872,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098885,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098899,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098912,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098933,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258098934,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258098935,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258098942,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098956,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098969,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098975,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258098979,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258098980,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258098981,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099011,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099016,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":76546,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"carita\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099072,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099081,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099087,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099096,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099106,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099128,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099172,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099180,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099182,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099185,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099188,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1891539816,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099217,\"category\":{\"id\":1,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099226,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099228,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":965964787,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099248,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099253,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099254,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099261,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099265,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099286,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099291,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099298,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190191925,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099332,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099409,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099413,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099416,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099421,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099429,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099438,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099439,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099451,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099453,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099458,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099467,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099469,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099481,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099490,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099492,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099493,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099501,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099505,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099520,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099521,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099530,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099534,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099536,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":121314,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099557,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099565,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099566,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099575,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099584,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099597,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099606,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099619,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099621,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099625,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099642,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099652,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099657,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099670,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099671,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099672,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099673,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099678,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099682,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1317018460,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":799435387,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099685,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":872573662,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099702,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":46735988,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099714,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099715,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099716,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099717,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099722,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099728,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099732,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099741,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":527354657,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099746,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2220,\"category\":{\"id\":121,\"name\":\"abc\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099751,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099752,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099756,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099767,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1806094554,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099783,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099784,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099785,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099790,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099791,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099792,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":2007064294,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1201729079,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":815739327,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1283553154,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1623613224,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1769271792,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099830,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1944245317,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1576099751,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1174494665,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1136399726,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1557386627,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099850,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2014880408,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1899674584,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1846008339,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1074686172,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":12580734,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099883,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1031290343,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1297630904,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258099938,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099963,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099964,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099965,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258099968,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100023,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100032,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100073,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"rabbit\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100142,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"dog1\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100181,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100185,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100186,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100187,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100188,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100189,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100190,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100191,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100001,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100200,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100222,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100269,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100291,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Lisa\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100316,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100317,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100322,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100327,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"daisy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100343,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100438,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100440,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100444,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100481,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100483,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100547,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100601,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100657,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":377,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"mr smyth\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100659,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100885,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100887,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100908,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100967,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258100991,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101028,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101049,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101062,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101115,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101169,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101195,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101207,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101288,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":10045,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"buddy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101298,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"hello\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":156789,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"buddys\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101311,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101395,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1664596557,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1268697297,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1119918021,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":715510363,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":277467690,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":583099844,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1199298534,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1858861949,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258101408,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"felix\"}],\"status\":\"available\"},{\"id\":8739826599258101430,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doogie Howser\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101448,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101452,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"felix\"}],\"status\":\"available\"},{\"id\":8739826599258101489,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"felix\"}],\"status\":\"available\"},{\"id\":8739826599258101538,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101630,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101657,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101698,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101715,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101724,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101758,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101798,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101836,\"category\":{\"id\":0,\"name\":\"true\"},\"name\":\"doggie\",\"photoUrls\":[\"string\",\"otro dato\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101909,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101926,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101967,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101993,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101996,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101998,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258101999,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102032,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102041,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102051,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102057,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102069,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102094,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102095,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102104,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102130,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102131,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102160,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102194,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102195,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102197,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102200,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102202,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102207,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102248,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102249,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102254,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102255,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102256,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102257,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bingo\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102266,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102372,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102373,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Jerry\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102374,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Bingo\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102407,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102416,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102417,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102418,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":74760936,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102456,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1521800274,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102497,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102510,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102511,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102516,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102518,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102522,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102523,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102524,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102526,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102530,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102539,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102540,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102541,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102542,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102555,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102557,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Tom\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102566,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102574,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102575,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"catie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102579,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1530527731,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":949358517,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":256320103,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1101347526,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":601010473,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102589,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102590,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102591,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1942012720,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1634869968,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1739480119,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102604,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102613,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102614,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2089269165,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102637,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1853335934,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1008438226,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102662,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"super pet 3\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102663,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"garfield\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102664,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1231361044,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1462289455,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":569476938,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1744052574,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102705,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":809840995,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":897073557,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":278609299,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1116736623,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102749,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1872903061,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1085361482,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":163848011,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1525349145,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258102810,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102823,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102888,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102909,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102934,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102935,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102940,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102941,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102943,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102947,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cateee\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102952,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102954,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258102958,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"teeeeeee\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":345,\"category\":{\"id\":10,\"name\":\"string\"},\"name\":\"YYY\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103154,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103191,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103211,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103258,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103262,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103273,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103277,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103282,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103287,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103300,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103305,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103331,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103359,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103364,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103389,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103390,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103436,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie2344\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103452,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103482,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103495,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103504,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103513,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103593,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggieeeeeeeeee\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103633,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103693,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103718,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103732,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103761,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103766,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103786,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103844,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103849,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103865,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103887,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103916,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103922,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103926,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103931,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103932,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103937,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103938,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103942,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"jhgjhjjhghj\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103944,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103955,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258103976,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258103977,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258103978,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104037,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104082,\"category\":{\"id\":0,\"name\":\"Mammal\"},\"name\":\"Elephant\",\"photoUrls\":[\"https://www.google.images.con/elephant\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104000,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104091,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104096,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104097,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104099,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104103,\"category\":{\"id\":0,\"name\":\"Mammal\"},\"name\":\"Elephant\",\"photoUrls\":[\"https://www.google.images.con/elephant\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":486132161561,\"category\":{\"id\":0,\"name\":\"Mammal\"},\"name\":\"Elephant\",\"photoUrls\":[\"https://www.google.images.con/elephant\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104104,\"category\":{\"id\":0,\"name\":\"Mammal\"},\"name\":\"Elephant\",\"photoUrls\":[\"https://www.google.images.con/elephant\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104117,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Kitty\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104126,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104132,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104137,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104142,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104151,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104160,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104173,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104186,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104219,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1777681792,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104240,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":508458693,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1726085370,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104262,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104271,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104272,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104281,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104290,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104295,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1574781447,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104312,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":657247432,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104317,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104319,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104326,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1809957747,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":56603043,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104376,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104380,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104381,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104383,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2345,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"David Harkins\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104417,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104418,\"category\":{\"id\":1,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104431,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104436,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104510,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104511,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104520,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104525,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":211428204,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104554,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1542036710,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104563,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Lisa\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104612,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104625,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104629,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1598484861,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104656,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104658,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104662,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104663,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104668,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104669,\"category\":{\"id\":0,\"name\":\"DOG\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104674,\"category\":{\"id\":0,\"name\":\"FUCK\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104675,\"category\":{\"id\":0,\"name\":\"CAT\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104680,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104685,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104686,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104696,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104714,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2040099029,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":786683223,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":564,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"buddy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104730,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104751,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104752,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104762,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":96331111,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiepet\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"pet\"}],\"status\":\"available\"},{\"id\":8739826599258104808,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258104849,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":9633156,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiepet\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"pet\"}],\"status\":\"available\"},{\"id\":9633511,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiepet\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"pet\"}],\"status\":\"available\"},{\"id\":8739826599258104854,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1456480288,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104915,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":942982952,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104960,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1491482544,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258104985,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1375370473,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1386346982,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258105014,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105015,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105020,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105025,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105027,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105035,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"Sunil Kumar Sahu\"}],\"status\":\"available\"},{\"id\":796812977,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1937953206,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258105045,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105050,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105069,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105086,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105091,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105104,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105113,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105122,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105147,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105160,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"dogao\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105161,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105166,\"name\":\"dogao\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258105167,\"name\":\"dogao\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258105168,\"name\":\"gh\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258105170,\"name\":\"gh\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258105226,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105227,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105236,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105253,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105282,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105287,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105305,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105314,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105327,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105328,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105329,\"category\":{\"id\":0,\"name\":\"Vasya\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105330,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105341,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105344,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105365,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105378,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105383,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105440,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105449,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105469,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105483,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105496,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105497,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105499,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105515,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":122122,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"TROY\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105532,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105549,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105555,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105560,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105568,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105578,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105591,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105592,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105593,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105598,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105599,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105600,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105601,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":12346,\"category\":{\"id\":1,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":2,\"name\":\"zfsy\"}],\"status\":\"available\"},{\"id\":8739826599258105606,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105615,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105628,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":123456,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"buddy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1130715880,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258105679,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":143551979,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258105716,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105741,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105746,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105751,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105752,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105753,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105755,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105759,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"catname\"}],\"status\":\"available\"},{\"id\":8739826599258105760,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258105761,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106000,\"name\":\"trolek\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106009,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1176908669,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106022,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106023,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106028,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":6619481,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"HanSoloCaptainKirk\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":246581112,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106049,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":96221120,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106071,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106140,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106141,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106142,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106143,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106172,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106173,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106174,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106179,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106180,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106181,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106194,\"category\":{\"id\":0,\"name\":\"fatkdk\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":113113,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggieisgood\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106199,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106201,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106203,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106210,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106211,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106212,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106225,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106226,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106243,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106260,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106261,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106262,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106271,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106272,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106273,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106278,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106279,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106280,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106288,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106289,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106291,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106308,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106309,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106310,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106315,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106361,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106362,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106363,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106372,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106373,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106374,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106379,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106380,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106381,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106402,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106411,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106432,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106433,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106434,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106435,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106440,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106441,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106442,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106451,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106452,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106453,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106466,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106467,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106468,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106477,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106482,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106483,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106484,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106489,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106502,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106503,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106504,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106509,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106510,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106511,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106520,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106521,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106522,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106555,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106556,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106557,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106566,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106567,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106568,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106573,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106574,\"name\":\"doggie\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106576,\"name\":\"doggies\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258106645,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Ndoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":234530,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"n jn\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":456,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"hund it\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106686,\"category\":{\"id\":12345,\"name\":\"string\"},\"name\":\"roger federer\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106695,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2223,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"robnho\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106803,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106805,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106810,\"category\":{\"id\":2340,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106823,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Susie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106828,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106829,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106850,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106879,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106897,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106906,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106911,\"category\":{\"id\":0},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106912,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106921,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258106990,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107003,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107133,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107134,\"category\":{\"id\":2340,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107135,\"category\":{\"id\":2340,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107136,\"category\":{\"id\":2340,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":45,\"category\":{\"id\":1,\"name\":\"mangool\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107145,\"category\":{\"id\":2340,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":43243,\"category\":{\"id\":0,\"name\":\"rerety\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107263,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107268,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107277,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107342,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107359,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107437,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107453,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1988,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"froggie doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1989,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"mr doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107507,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107596,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107638,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107639,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107680,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107693,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107698,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107699,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107700,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107701,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107706,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107719,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik\",\"photoUrls\":[\"fsdfasdfasdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107728,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik\",\"photoUrls\":[\"fsdfasdfasdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107729,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik\",\"photoUrls\":[\"fsdfasdfasdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107730,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik\",\"photoUrls\":[\"fsdfasdfasdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107731,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik\",\"photoUrls\":[\"fsdfasdfasdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107732,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik\",\"photoUrls\":[\"fsdfasdfasdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107733,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107738,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107763,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik1\",\"photoUrls\":[\"fsdfasdf1sdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107766,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik2\",\"photoUrls\":[\"fsdfasdf1sdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107769,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik2\",\"photoUrls\":[\"fsdfasdf1sdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107770,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik2\",\"photoUrls\":[\"fsdfasdf1sdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107771,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik2\",\"photoUrls\":[\"fsdfasdf1sdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107772,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"bobik2\",\"photoUrls\":[\"fsdfasdf1sdf\"],\"tags\":[{\"id\":0,\"name\":\"new pet\"}],\"status\":\"available\"},{\"id\":8739826599258107773,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107794,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107799,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107802,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"stoogie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107805,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"NutsnBolts\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"NutsnBolts\"}],\"status\":\"available\"},{\"id\":8739826599258107814,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258107992,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108041,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108050,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108051,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108052,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108085,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108154,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108328,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108341,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108503,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108508,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1234567,\"name\":\"doggie12345\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1212321,\"category\":{\"id\":0,\"name\":\"<script src=http://evilwebsite.inject.js>\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108627,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108648,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"froggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108661,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1491949973,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258108678,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":839222003,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":880,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":990,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1274598481,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":306290014,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":609209263,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":2025233784,\"name\":\"fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":122,\"category\":{\"id\":10,\"name\":\"string\"},\"name\":\"doggissse\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108940,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"nis\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1212,\"category\":{\"id\":10,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108953,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108966,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258108971,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109032,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109045,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109114,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109132,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109141,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109290,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109293,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie2\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109308,\"category\":{\"id\":0},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109313,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109350,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109351,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109356,\"category\":{\"id\":0,\"name\":\"Amit\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109378,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":11111,\"category\":{\"id\":0,\"name\":\"aaaaa\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109415,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109436,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109491,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109496,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109713,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109718,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109719,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109776,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109777,\"category\":{\"id\":666,\"name\":\"string\"},\"name\":\"Sgt Peppers\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109778,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109804,\"category\":{\"id\":666,\"name\":\"string\"},\"name\":\"Sgt Peppers\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":666,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Sgt Peppers\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109813,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":60949,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":60950,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":60951,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":60952,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":60953,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":60956,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":3,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"cow\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"},{\"id\":4,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"horse\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"},{\"id\":8739826599258109919,\"category\":{\"id\":0,\"name\":\"ann\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":111,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109985,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258109990,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110008,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110033,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110034,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110035,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110036,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110037,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110050,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":104,\"category\":{\"id\":0,\"name\":\"victor\"},\"name\":\"2016-12-33800:00:00\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110074,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":67,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"buddy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110092,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110117,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110122,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110123,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110126,\"category\":{\"id\":0,\"name\":\"dog\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"dog\"}],\"status\":\"available\"},{\"id\":8739826599258110162,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110167,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110184,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110232,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110254,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110319,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110324,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110357,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110391,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110400,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":222,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"222 ux doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1481101179,\"category\":{\"id\":75822,\"name\":\"super-happy\"},\"name\":\"monkey\",\"photoUrls\":[\"http://foo.bar.com/3\",\"http://foo.bar.com/4\"],\"tags\":[{\"id\":87363,\"name\":\"test tag 1\"},{\"id\":82671,\"name\":\"test tag 2\"}],\"status\":\"available\"},{\"id\":8739826599258110464,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110469,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110470,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110478,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110492,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1286987908787,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"swagg\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":11,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":12345,\"name\":\"cat\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258110549,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110565,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"tibco-doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110575,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Monica_TIBCOdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110581,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Monica_TIBCOdoggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1234,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"buddy\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110626,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"goldfish\",\"photoUrls\":[\"http://cf.ltkcdn.net/small-pets/images/std/136437-372x323-goldfish.jpg\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1238064799,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258110632,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"goldfish\",\"photoUrls\":[\"http://cf.ltkcdn.net/small-pets/images/std/136437-372x323-goldfish.jpg\"],\"tags\":[{\"id\":100,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110648,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110650,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110651,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":467266,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":467267,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":467268,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":467269,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":467270,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258110662,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"apple\"}],\"status\":\"available\"},{\"id\":495236,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258110665,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":495237,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":495238,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":467272,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":495239,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258110666,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":495240,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258110671,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggieáű\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":616783,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":616784,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":616785,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":495242,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":616786,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":616787,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":616789,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":8739826599258110676,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggieáű\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110689,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":999111,\"category\":{\"id\":0,\"name\":\"quitepat\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":909090,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie909090\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"ASCIITAG\"}],\"status\":\"available\"},{\"id\":8739826599258110714,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110715,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110719,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110725,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":696050,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":696051,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":696052,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":696053,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":696054,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":696056,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":61910,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":827102,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":61911,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":61912,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":61913,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":61914,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":61916,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":8739826599258110794,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":827103,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":827104,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"programmer\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":827105,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"furt\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":827106,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"gorilla\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[],\"status\":\"available\"},{\"id\":827108,\"category\":{\"id\":0,\"name\":\"really-happy\"},\"name\":\"monster\",\"photoUrls\":[\"http://foo.bar.com/1\",\"http://foo.bar.com/2\"],\"tags\":[{\"id\":0,\"name\":\"friendly\"}],\"status\":\"available\"},{\"id\":7533858,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Admiral Barkington\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110815,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110840,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110864,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110868,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110892,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110897,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110902,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110903,\"category\":{\"id\":0,\"name\":\"A\"},\"name\":\"doggie1\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110912,\"category\":{\"id\":0,\"name\":\"A\"},\"name\":\"doggie1\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110917,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110930,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":22,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2047264383,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":5,\"category\":{\"id\":2,\"name\":\"Bird\"},\"name\":\"sparrow\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"},{\"id\":228503015,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":909091,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"NONASCII構欺運型ÄäăąĎďĐđßनमस्ते안녕\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258110986,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":604681680,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1111,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":9090,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"構欺運型ÄäăąĎďĐđßनमस्ते안녕\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1037772929,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258111007,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":287972431,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258111016,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111041,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111046,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111051,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111052,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111057,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111058,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111063,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111064,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":9633512,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggiepet\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"pet\"}],\"status\":\"available\"},{\"id\":998765,\"category\":{\"id\":999,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":999,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111073,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111078,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111099,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111112,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111117,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111130,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111132,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111133,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111138,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111139,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111140,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111141,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111146,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111147,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111148,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111149,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111154,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111158,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111160,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111161,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111162,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111170,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111172,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111178,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111179,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111184,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111185,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111190,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111191,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111192,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111196,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111198,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111203,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111204,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111209,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111210,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111211,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111212,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111213,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111218,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111219,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111220,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111225,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1638181578,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258111231,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111238,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111240,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111241,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111286,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111295,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111296,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111337,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111359,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111360,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111369,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111460,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111469,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111486,\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1981896117,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258111551,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1696372009,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":826623093,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":116839726,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":320538623,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1037736343,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1653068000,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":1880719146,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":603821547,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258111701,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1945348680,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258111750,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111755,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1890682369,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258111764,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111773,\"category\":{\"id\":0,\"name\":\"truc\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"truc\"}],\"status\":\"available\"},{\"id\":8739826599258111778,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111779,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":981,\"category\":{\"id\":1,\"name\":\"ret\"},\"name\":\"doggie\",\"photoUrls\":[\"ret\"],\"tags\":[{\"id\":1,\"name\":\"ret\"}],\"status\":\"available\"},{\"id\":7648651,\"category\":{\"id\":1,\"name\":\"ret\"},\"name\":\"doggie\",\"photoUrls\":[\"ret\"],\"tags\":[{\"id\":1,\"name\":\"ret\"}],\"status\":\"available\"},{\"id\":8739826599258111796,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111802,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111806,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1833726630,\"name\":\"Fido\",\"photoUrls\":[],\"tags\":[],\"status\":\"available\"},{\"id\":8739826599258111815,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111816,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111817,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111838,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111847,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111876,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":12,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111901,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111907,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111915,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":2,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"},{\"id\":8739826599258111932,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":8739826599258111940,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"cow\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":1,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"Aditya\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"}]", + "language": "json", + "rawDataType": "text", + "previewType": "text", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": false, + "state": { + "size": "normal" + }, + "id": "4d9987b5-a42e-8e9d-3b18-c98bc4c5c48d", + "name": "available response", + "request": { + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=70f735676ec46351c6699c4bb767878a&status=available", + "headers": [], + "data": null, + "method": "GET", + "dataMode": "params" + }, + "owner": "1278651" + } + ], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {} + }, + { + "id": "d7af0c2d-7698-4cd3-9921-b568aa47c201", + "headers": "", + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/2?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "pathVariables": {}, + "preRequestScript": null, + "method": "GET", + "collectionId": "c0bc3513-c83a-8b5d-bbff-1a4a0dc001d7", + "data": null, + "dataMode": "params", + "name": "findById 2", + "description": "", + "descriptionFormat": "html", + "time": 1481033690181, + "version": 2, + "responses": [ + { + "status": "", + "responseCode": { + "code": 200, + "name": "OK", + "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action." + }, + "time": 233, + "headers": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request." + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request." + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "Options that are desired for the connection" + }, + { + "name": "Content-Length", + "key": "Content-Length", + "value": "137", + "description": "The length of the response body in octets (8-bit bytes)" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:18 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "A name for the server" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)" + } + ], + "cookies": [], + "mime": "", + "text": "{\"id\":2,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"}", + "language": "json", + "rawDataType": "text", + "previewType": "text", + "searchResultScrolledTo": -1, + "forceNoPretty": false, + "write": true, + "empty": false, + "failed": false, + "state": { + "size": "normal" + }, + "id": "ae805d6f-f63a-c98a-8b3a-30beddafcb00", + "name": "2 response", + "request": { + "url": "https://petstore-api-2445581593402.staging.apicast.io:443/v2/pet/2?user_key=70f735676ec46351c6699c4bb767878a", + "headers": [], + "data": null, + "method": "GET", + "dataMode": "params" + }, + "owner": "1278651" + } + ], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "isFromCollection": true, + "collectionRequestId": "b87e8886-8bce-bee2-519b-c110e2520b73" + } + ], + "timestamp": 1496988410914, + "syncedFilename": "", + "public": false, + "owner": "1278651", + "isDeleted": false +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection.json new file mode 100644 index 000000000..d911d65fa --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/PetstoreAPI-collection.json @@ -0,0 +1,225 @@ +{ + "variables": [], + "info": { + "name": "Petstore API", + "_postman_id": "b70174e2-0525-8fcd-da9b-578f9dd9cfb4", + "description": "version=1.0 - This is a PetStore API description", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "findByStatus available", + "request": { + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=998bac0775b1d5f588e0a6ca7c11b852&status=available", + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "" + }, + "response": [ + { + "id": "20be7555-3661-4ee4-9e94-0384867a12d1", + "name": "available response", + "originalRequest": { + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=70f735676ec46351c6699c4bb767878a&status=available", + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request." + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request." + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "Options that are desired for the connection" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:31:45 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.15.1", + "description": "A name for the server" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)" + }, + { + "name": "transfer-encoding", + "key": "transfer-encoding", + "value": "chunked", + "description": "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity." + } + ], + "cookie": [], + "responseTime": 1466, + "body": "[{\"id\":190192062,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192063,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192285,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192654,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192671,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192727,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192736,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192768,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192878,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190192907,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":190193000,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"},{\"id\":-98125093,\"category\":{\"id\":-517488397,\"name\":\"EJvNbK\"},\"name\":\"LuEfMZATrHz\",\"photoUrls\":[\"XCXOVVkaxa\",\"gNwYqHEmC\",\"nvCvphDeuqztysUBNed\",\"W\",\"vmrxRIViyXqumolLIeoB\",\"JRqHVxk\",\"tCUGbegVHoXajm\",\"UiHppQn\"],\"tags\":[{\"id\":727599428,\"name\":\"RemggEDzxPljbrlktdWf\"},{\"id\":1987753751,\"name\":\"zWqdKAGHMmhPPlomljaNtuvm\"},{\"id\":1251632392,\"name\":\"BAgtgtKOxZGdsS\"},{\"id\":-1813025208,\"name\":\"OkKxtfAkCMEICbbQDVPi\"},{\"id\":-730110346,\"name\":\"WshDF\"},{\"id\":2100951153,\"name\":\"yxUFSknQEleIAQCoocl\"},{\"id\":-2135188117,\"name\":\"M\"},{\"id\":1352243140,\"name\":\"koKHsjysHXW\"},{\"id\":1696778814,\"name\":\"KaihiyarcZkIzkkquWPZ\"},{\"id\":659492963,\"name\":\"xqIzulcBPzWMyUpQwQK\"},{\"id\":-2118372841,\"name\":\"naYFGuHmqDqOpfHH\"}],\"status\":\"available\"},{\"id\":8739826599258110549,\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"doggie\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"}]" + } + ] + }, + { + "name": "findById 2", + "request": { + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/2?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "" + }, + "response": [ + { + "id": "010df50f-46a4-4bc3-9ef1-42b4faf84179", + "name": "2 response", + "originalRequest": { + "url": "https://petstore-api-2445581593402.staging.apicast.io:443/v2/pet/2?user_key=70f735676ec46351c6699c4bb767878a", + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text", + "header": [ + { + "name": "Access-Control-Allow-Headers", + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, api_key, Authorization", + "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request." + }, + { + "name": "Access-Control-Allow-Methods", + "key": "Access-Control-Allow-Methods", + "value": "GET, POST, DELETE, PUT", + "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request." + }, + { + "name": "Access-Control-Allow-Origin", + "key": "Access-Control-Allow-Origin", + "value": "*", + "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." + }, + { + "name": "Connection", + "key": "Connection", + "value": "keep-alive", + "description": "Options that are desired for the connection" + }, + { + "name": "Content-Length", + "key": "Content-Length", + "value": "137", + "description": "The length of the response body in octets (8-bit bytes)" + }, + { + "name": "Content-Type", + "key": "Content-Type", + "value": "application/json", + "description": "The mime type of this content" + }, + { + "name": "Date", + "key": "Date", + "value": "Wed, 07 Dec 2016 15:27:18 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "Server", + "key": "Server", + "value": "openresty/1.9.7.4", + "description": "A name for the server" + }, + { + "name": "X-Powered-By", + "key": "X-Powered-By", + "value": "3scale API Management - http://www.3scale.net", + "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)" + } + ], + "cookie": [], + "responseTime": 233, + "body": "{\"id\":2,\"category\":{\"id\":1,\"name\":\"Animal\"},\"name\":\"cat\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":1,\"name\":\"domestic\"}],\"status\":\"available\"}" + } + ] + }, + { + "name": "findById 1 (404)", + "request": { + "url": "https://petstore-api-2445581593402.apicast.io:443/v2/pet/1?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "" + }, + "response": [ + { + "id": "010df50f-46a4-4bc3-9ef1-42b4faf84180", + "name": "1 response", + "originalRequest": { + "url": "https://petstore-api-2445581593402.staging.apicast.io:443/v2/pet/1?user_key=70f735676ec46351c6699c4bb767878a", + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "_postman_previewtype": "text" + } + ] + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_collection-2.1.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_collection-2.1.json new file mode 100644 index 000000000..b48525593 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_collection-2.1.json @@ -0,0 +1,273 @@ +{ + "info": { + "name": "Swagger Petstore", + "_postman_id": "6173c1b4-0edb-4bd1-871b-7797258716d3", + "description": "This is a PetStore API description", + "version": "1.1", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "pet", + "description": "Folder for pet", + "item": [ + { + "name": "Finds Pets by status", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/findByStatus?status={{status}}&user_key={{user_key}}", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "status", + "value": "{{status}}", + "equals": true + }, + { + "key": "user_key", + "value": "{{user_key}}", + "equals": true + } + ] + }, + "description": "Multiple status values can be provided with comma separated strings" + }, + "response": [ + { + "id": "5764f6e1-3fec-40b8-9a0d-8e4f4fc66e5b", + "name": "findbystatus-available", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/findByStatus?status=available&user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "status", + "value": "available", + "equals": true + }, + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text", + "name": "Content-Type" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 190192062,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192063,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192285,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192654,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192671,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192727,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192736,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192768,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192878,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192907,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190193000,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": -98125093,\n \"category\": {\n \"id\": -517488397,\n \"name\": \"EJvNbK\"\n },\n \"name\": \"LuEfMZATrHz\",\n \"photoUrls\": [\n \"XCXOVVkaxa\",\n \"gNwYqHEmC\",\n \"nvCvphDeuqztysUBNed\",\n \"W\",\n \"vmrxRIViyXqumolLIeoB\",\n \"JRqHVxk\",\n \"tCUGbegVHoXajm\",\n \"UiHppQn\"\n ],\n \"tags\": [\n {\n \"id\": 727599428,\n \"name\": \"RemggEDzxPljbrlktdWf\"\n },\n {\n \"id\": 1987753751,\n \"name\": \"zWqdKAGHMmhPPlomljaNtuvm\"\n },\n {\n \"id\": 1251632392,\n \"name\": \"BAgtgtKOxZGdsS\"\n },\n {\n \"id\": -1813025208,\n \"name\": \"OkKxtfAkCMEICbbQDVPi\"\n },\n {\n \"id\": -730110346,\n \"name\": \"WshDF\"\n },\n {\n \"id\": 2100951153,\n \"name\": \"yxUFSknQEleIAQCoocl\"\n },\n {\n \"id\": -2135188117,\n \"name\": \"M\"\n },\n {\n \"id\": 1352243140,\n \"name\": \"koKHsjysHXW\"\n },\n {\n \"id\": 1696778814,\n \"name\": \"KaihiyarcZkIzkkquWPZ\"\n },\n {\n \"id\": 659492963,\n \"name\": \"xqIzulcBPzWMyUpQwQK\"\n },\n {\n \"id\": -2118372841,\n \"name\": \"naYFGuHmqDqOpfHH\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": -68132066,\n \"category\": {\n \"id\": -91395365,\n \"name\": \"FmMkqZRSCOaAThTjTIcolI\"\n },\n \"name\": \"bpUDswwAfGEyuZYAyCqaSZS\",\n \"photoUrls\": [\n \"EDwpbUPQtkQTvN\",\n \"pISYlIfScAw\",\n \"okQ\",\n \"guIVyggGwnGe\",\n \"mgvbRLAtzqLwaxOCdOtK\",\n \"JIYGRxq\",\n \"cVcdQVCnELszLNIfNwAQUUL\",\n \"cyPU\",\n \"VCqurkja\"\n ],\n \"tags\": [\n {\n \"id\": -1860308527,\n \"name\": \"Ypyvo\"\n },\n {\n \"id\": 740966829,\n \"name\": \"EoUdEnBRKaYnrAwemm\"\n },\n {\n \"id\": 2118690298,\n \"name\": \"GNN\"\n },\n {\n \"id\": -66381237,\n \"name\": \"LRM\"\n },\n {\n \"id\": 839909562,\n \"name\": \"NbwCTvnFCtQkfh\"\n },\n {\n \"id\": 216887794,\n \"name\": \"s\"\n },\n {\n \"id\": 193060448,\n \"name\": \"eferBLgSJmhevsc\"\n },\n {\n \"id\": -266174447,\n \"name\": \"IKqwZHPvgCEhMlCuW\"\n },\n {\n \"id\": -887416107,\n \"name\": \"ZQH\"\n },\n {\n \"id\": -588775660,\n \"name\": \"LGHzdgipDktfQ\"\n }\n ],\n \"status\": \"available\"\n }\n]" + } + ] + }, + { + "name": "Find pet by ID", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key={{user_key}}", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "{{user_key}}", + "equals": true + } + ], + "variable": [ + { + "key": "petId", + "value": "{{petId}}" + } + ] + }, + "description": "Returns a single pet" + }, + "response": [ + { + "id": "40f56c46-2d3c-48d1-a19f-6c62b382710c", + "name": "findbyid-2", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true + } + ], + "variable": [ + { + "key": "petId", + "value": "2" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text", + "name": "Content-Type" + } + ], + "cookie": [], + "body": "{\n \"id\": 2,\n \"category\": {\n \"id\": 1,\n \"name\": \"Animal\"\n },\n \"name\": \"cat\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 1,\n \"name\": \"domestic\"\n }\n ],\n \"status\": \"available\"\n}" + }, + { + "id": "f63759af-1ea4-41bf-91b6-c3bdb1c584d1", + "name": "findbyid-1", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true + } + ], + "variable": [ + { + "key": "petId", + "value": "1" + } + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "Text", + "header": [], + "cookie": [], + "body": "" + } + ] + } + ] + } + ] +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_collection.json new file mode 100644 index 000000000..58332dc7d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_collection.json @@ -0,0 +1,281 @@ +{ + "variables": [], + "info": { + "name": "Swagger Petstore", + "_postman_id": "f92d904c-de70-75ca-a9ae-a1326802cb24", + "description": "version=1.0 - This is a PetStore API description", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "pet", + "description": "Folder for pet", + "item": [ + { + "name": "Finds Pets by status", + "request": { + "url": { + "raw": "http://petstore.swagger.io/v2/pet/findByStatus?status={{status}}&user_key={{user_key}}", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "status", + "value": "{{status}}", + "equals": true, + "description": "" + }, + { + "key": "user_key", + "value": "{{user_key}}", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json", + "description": "" + } + ], + "body": {}, + "description": "Multiple status values can be provided with comma separated strings" + }, + "response": [ + { + "id": "ae210466-a845-4af2-a98b-bc17bf17ed06", + "name": "findbystatus-available", + "originalRequest": { + "url": { + "raw": "http://petstore.swagger.io/v2/pet/findByStatus?status=available&user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "status", + "value": "available", + "equals": true, + "description": "" + }, + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json", + "description": "" + } + ], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text", + "name": "Content-Type" + } + ], + "cookie": [], + "responseTime": 0, + "body": "[\n {\n \"id\": 190192062,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192063,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192285,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192654,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192671,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192727,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192736,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192768,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192878,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192907,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190193000,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": -98125093,\n \"category\": {\n \"id\": -517488397,\n \"name\": \"EJvNbK\"\n },\n \"name\": \"LuEfMZATrHz\",\n \"photoUrls\": [\n \"XCXOVVkaxa\",\n \"gNwYqHEmC\",\n \"nvCvphDeuqztysUBNed\",\n \"W\",\n \"vmrxRIViyXqumolLIeoB\",\n \"JRqHVxk\",\n \"tCUGbegVHoXajm\",\n \"UiHppQn\"\n ],\n \"tags\": [\n {\n \"id\": 727599428,\n \"name\": \"RemggEDzxPljbrlktdWf\"\n },\n {\n \"id\": 1987753751,\n \"name\": \"zWqdKAGHMmhPPlomljaNtuvm\"\n },\n {\n \"id\": 1251632392,\n \"name\": \"BAgtgtKOxZGdsS\"\n },\n {\n \"id\": -1813025208,\n \"name\": \"OkKxtfAkCMEICbbQDVPi\"\n },\n {\n \"id\": -730110346,\n \"name\": \"WshDF\"\n },\n {\n \"id\": 2100951153,\n \"name\": \"yxUFSknQEleIAQCoocl\"\n },\n {\n \"id\": -2135188117,\n \"name\": \"M\"\n },\n {\n \"id\": 1352243140,\n \"name\": \"koKHsjysHXW\"\n },\n {\n \"id\": 1696778814,\n \"name\": \"KaihiyarcZkIzkkquWPZ\"\n },\n {\n \"id\": 659492963,\n \"name\": \"xqIzulcBPzWMyUpQwQK\"\n },\n {\n \"id\": -2118372841,\n \"name\": \"naYFGuHmqDqOpfHH\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": -68132066,\n \"category\": {\n \"id\": -91395365,\n \"name\": \"FmMkqZRSCOaAThTjTIcolI\"\n },\n \"name\": \"bpUDswwAfGEyuZYAyCqaSZS\",\n \"photoUrls\": [\n \"EDwpbUPQtkQTvN\",\n \"pISYlIfScAw\",\n \"okQ\",\n \"guIVyggGwnGe\",\n \"mgvbRLAtzqLwaxOCdOtK\",\n \"JIYGRxq\",\n \"cVcdQVCnELszLNIfNwAQUUL\",\n \"cyPU\",\n \"VCqurkja\"\n ],\n \"tags\": [\n {\n \"id\": -1860308527,\n \"name\": \"Ypyvo\"\n },\n {\n \"id\": 740966829,\n \"name\": \"EoUdEnBRKaYnrAwemm\"\n },\n {\n \"id\": 2118690298,\n \"name\": \"GNN\"\n },\n {\n \"id\": -66381237,\n \"name\": \"LRM\"\n },\n {\n \"id\": 839909562,\n \"name\": \"NbwCTvnFCtQkfh\"\n },\n {\n \"id\": 216887794,\n \"name\": \"s\"\n },\n {\n \"id\": 193060448,\n \"name\": \"eferBLgSJmhevsc\"\n },\n {\n \"id\": -266174447,\n \"name\": \"IKqwZHPvgCEhMlCuW\"\n },\n {\n \"id\": -887416107,\n \"name\": \"ZQH\"\n },\n {\n \"id\": -588775660,\n \"name\": \"LGHzdgipDktfQ\"\n }\n ],\n \"status\": \"available\"\n }\n]" + } + ] + }, + { + "name": "Find pet by ID", + "request": { + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key={{user_key}}", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "{{user_key}}", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "description": "", + "key": "petId", + "value": "{{petId}}" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json", + "description": "" + } + ], + "body": {}, + "description": "Returns a single pet" + }, + "response": [ + { + "id": "4bbf4440-c41b-4a3f-a4e7-c518b7529ac3", + "name": "findbyid-1", + "originalRequest": { + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "description": "", + "key": "petId", + "value": "1" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json", + "description": "" + } + ], + "body": {} + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "" + }, + { + "id": "af7071e5-14d5-4a0e-9cfe-6324c757572a", + "name": "findbyid-2", + "originalRequest": { + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true, + "description": "" + } + ], + "variable": [ + { + "description": "", + "key": "petId", + "value": "2" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json", + "description": "" + } + ], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text", + "name": "Content-Type" + } + ], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": 2,\n \"category\": {\n \"id\": 1,\n \"name\": \"Animal\"\n },\n \"name\": \"cat\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 1,\n \"name\": \"domestic\"\n }\n ],\n \"status\": \"available\"\n}" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_workspace_collection-2.1.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_workspace_collection-2.1.json new file mode 100644 index 000000000..2f286b967 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Swagger Petstore.postman_workspace_collection-2.1.json @@ -0,0 +1,275 @@ +{ + "collection": { + "info": { + "name": "Swagger Petstore", + "_postman_id": "6173c1b4-0edb-4bd1-871b-7797258716d3", + "description": "This is a PetStore API description", + "version": "1.1", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "pet", + "description": "Folder for pet", + "item": [ + { + "name": "Finds Pets by status", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/findByStatus?status={{status}}&user_key={{user_key}}", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "status", + "value": "{{status}}", + "equals": true + }, + { + "key": "user_key", + "value": "{{user_key}}", + "equals": true + } + ] + }, + "description": "Multiple status values can be provided with comma separated strings" + }, + "response": [ + { + "id": "5764f6e1-3fec-40b8-9a0d-8e4f4fc66e5b", + "name": "findbystatus-available", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/findByStatus?status=available&user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + "findByStatus" + ], + "query": [ + { + "key": "status", + "value": "available", + "equals": true + }, + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text", + "name": "Content-Type" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 190192062,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192063,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192285,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192654,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192671,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192727,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192736,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192768,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192878,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190192907,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": 190193000,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": -98125093,\n \"category\": {\n \"id\": -517488397,\n \"name\": \"EJvNbK\"\n },\n \"name\": \"LuEfMZATrHz\",\n \"photoUrls\": [\n \"XCXOVVkaxa\",\n \"gNwYqHEmC\",\n \"nvCvphDeuqztysUBNed\",\n \"W\",\n \"vmrxRIViyXqumolLIeoB\",\n \"JRqHVxk\",\n \"tCUGbegVHoXajm\",\n \"UiHppQn\"\n ],\n \"tags\": [\n {\n \"id\": 727599428,\n \"name\": \"RemggEDzxPljbrlktdWf\"\n },\n {\n \"id\": 1987753751,\n \"name\": \"zWqdKAGHMmhPPlomljaNtuvm\"\n },\n {\n \"id\": 1251632392,\n \"name\": \"BAgtgtKOxZGdsS\"\n },\n {\n \"id\": -1813025208,\n \"name\": \"OkKxtfAkCMEICbbQDVPi\"\n },\n {\n \"id\": -730110346,\n \"name\": \"WshDF\"\n },\n {\n \"id\": 2100951153,\n \"name\": \"yxUFSknQEleIAQCoocl\"\n },\n {\n \"id\": -2135188117,\n \"name\": \"M\"\n },\n {\n \"id\": 1352243140,\n \"name\": \"koKHsjysHXW\"\n },\n {\n \"id\": 1696778814,\n \"name\": \"KaihiyarcZkIzkkquWPZ\"\n },\n {\n \"id\": 659492963,\n \"name\": \"xqIzulcBPzWMyUpQwQK\"\n },\n {\n \"id\": -2118372841,\n \"name\": \"naYFGuHmqDqOpfHH\"\n }\n ],\n \"status\": \"available\"\n },\n {\n \"id\": -68132066,\n \"category\": {\n \"id\": -91395365,\n \"name\": \"FmMkqZRSCOaAThTjTIcolI\"\n },\n \"name\": \"bpUDswwAfGEyuZYAyCqaSZS\",\n \"photoUrls\": [\n \"EDwpbUPQtkQTvN\",\n \"pISYlIfScAw\",\n \"okQ\",\n \"guIVyggGwnGe\",\n \"mgvbRLAtzqLwaxOCdOtK\",\n \"JIYGRxq\",\n \"cVcdQVCnELszLNIfNwAQUUL\",\n \"cyPU\",\n \"VCqurkja\"\n ],\n \"tags\": [\n {\n \"id\": -1860308527,\n \"name\": \"Ypyvo\"\n },\n {\n \"id\": 740966829,\n \"name\": \"EoUdEnBRKaYnrAwemm\"\n },\n {\n \"id\": 2118690298,\n \"name\": \"GNN\"\n },\n {\n \"id\": -66381237,\n \"name\": \"LRM\"\n },\n {\n \"id\": 839909562,\n \"name\": \"NbwCTvnFCtQkfh\"\n },\n {\n \"id\": 216887794,\n \"name\": \"s\"\n },\n {\n \"id\": 193060448,\n \"name\": \"eferBLgSJmhevsc\"\n },\n {\n \"id\": -266174447,\n \"name\": \"IKqwZHPvgCEhMlCuW\"\n },\n {\n \"id\": -887416107,\n \"name\": \"ZQH\"\n },\n {\n \"id\": -588775660,\n \"name\": \"LGHzdgipDktfQ\"\n }\n ],\n \"status\": \"available\"\n }\n]" + } + ] + }, + { + "name": "Find pet by ID", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key={{user_key}}", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "{{user_key}}", + "equals": true + } + ], + "variable": [ + { + "key": "petId", + "value": "{{petId}}" + } + ] + }, + "description": "Returns a single pet" + }, + "response": [ + { + "id": "40f56c46-2d3c-48d1-a19f-6c62b382710c", + "name": "findbyid-2", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true + } + ], + "variable": [ + { + "key": "petId", + "value": "2" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text", + "name": "Content-Type" + } + ], + "cookie": [], + "body": "{\n \"id\": 2,\n \"category\": {\n \"id\": 1,\n \"name\": \"Animal\"\n },\n \"name\": \"cat\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 1,\n \"name\": \"domestic\"\n }\n ],\n \"status\": \"available\"\n}" + }, + { + "id": "f63759af-1ea4-41bf-91b6-c3bdb1c584d1", + "name": "findbyid-1", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml, application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://petstore.swagger.io/v2/pet/:petId?user_key=998bac0775b1d5f588e0a6ca7c11b852", + "protocol": "http", + "host": [ + "petstore", + "swagger", + "io" + ], + "path": [ + "v2", + "pet", + ":petId" + ], + "query": [ + { + "key": "user_key", + "value": "998bac0775b1d5f588e0a6ca7c11b852", + "equals": true + } + ], + "variable": [ + { + "key": "petId", + "value": "1" + } + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "Text", + "header": [], + "cookie": [], + "body": "" + } + ] + } + ] + } + ] + } +} diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API bad version.postman_collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API bad version.postman_collection.json new file mode 100644 index 000000000..fbc4ae980 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API bad version.postman_collection.json @@ -0,0 +1,397 @@ +{ + "variables": [], + "info": { + "name": "Test API", + "_postman_id": "48430400-3a9b-28dc-4f16-c1c404eee237", + "description": "version=0.0.1-Description", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "order", + "description": "Folder for order", + "item": [ + { + "name": "http:///order?status={{status}}&page={{page}}", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "", + "console.log(\"status: \" + environment.status);", + "console.log(\"page: \" + environment.page);", + "", + "var schema = {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": { \"type\": \"string\" },", + " \"status\": { \"type\": \"string\", \"enum\": [\"pending_approval\"] }", + " }", + " }", + "};", + "", + "tests[\"Valid response\"] = tv4.validate(jsonData, schema);", + "console.log(\"Validation failed: \", tv4.error);", + "" + ] + } + }, + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "postman.setEnvironmentVariable(\"status\", \"pending_approval\");", + "postman.setEnvironmentVariable(\"page\", \"0\");" + ] + } + } + ], + "request": { + "url": { + "raw": "http://localhost:8080/rest/Test%20API/0.0.1/order?status=pending_approval&page=0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "rest", + "Test%20API", + "0.0.1", + "order" + ], + "query": [ + { + "key": "status", + "value": "pending_approval", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Retrieve a list of orders" + }, + "response": [ + { + "id": "3f0597ff-975b-492e-927b-bcec40b491f0", + "name": "list-pending_approval", + "originalRequest": { + "url": { + "raw": "http:///order?status=pending_approval&page=0", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order" + ], + "query": [ + { + "key": "status", + "value": "pending_approval", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[\n {\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50,\n \"items\": [\n {\n \"price\": 40,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20\n }\n },\n {\n \"price\": 10,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n },\n {\n \"id\": \"7891011\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20,\n \"items\": [\n {\n \"price\": 20,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n }\n]" + }, + { + "id": "80668c70-e9cc-436e-bae1-056f018261cb", + "name": "list-approved", + "originalRequest": { + "url": { + "raw": "http:///order?status=approved&page=0", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order" + ], + "query": [ + { + "key": "status", + "value": "approved", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[\n\t{\n\t \"id\": \"121314\",\n\t \"status\": \"approved\",\n\t \"price\": 4,\n\t \"quantity\": 2,\n\t \"item\": {\n\t \"id\": \"2\",\n\t \"label\": \"Bar\",\n\t \"price\": 2\n\t }\n\t}\n]" + } + ] + }, + { + "name": "http:///order", + "request": { + "url": "http:///order", + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "The order to create" + }, + "description": "Create (place) a new order" + }, + "response": [ + { + "id": "709884b2-7fc9-400c-b632-3f99689ae788", + "name": "create-123456", + "originalRequest": { + "url": "http:///order", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"devise\": \"EUR\",\n\t\"amount\": 50.0,\n\t\"items\": [\n\t\t{\n\t\t\t\"price\": 40.0,\n\t\t\t\"quantity\": 2,\n\t\t\t\"item\": {\n\t\t\t\t\"id\": \"1\",\n\t\t\t\t\"label\": \"Foo\",\n\t\t\t\t\"price\": 20.0\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"price\": 10.0,\n\t\t\t\"quantity\": 5,\n\t\t\t\"item\": {\n\t\t\t\t\"id\": \"2\",\n\t\t\t\t\"label\": \"Bar\",\n\t\t\t\t\"price\": 2.0\n\t\t\t}\n\t\t}\n\t]\n}" + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50.0,\n \"items\": [\n {\n \"price\": 40.0,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20.0\n }\n },\n {\n \"price\": 10.0,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + }, + { + "id": "f36d1b1c-17a5-45e6-a575-db148ac26c17", + "name": "create-7891011", + "originalRequest": { + "url": "http:///order", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"devise\": \"EUR\",\n \"amount\": 20.0,\n \"items\": [\n {\n \"price\": 20.0,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": 7891011,\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20.0,\n \"items\": [\n {\n \"price\": 20.0,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + } + ] + }, + { + "name": "http:///order/:id", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "var expectedId = request.url.slice(request.url.lastIndexOf('/order/') + 7);", + "tests[\"Check id value\"] = jsonData.id === expectedId;" + ] + } + }, + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "", + "" + ] + } + } + ], + "request": { + "url": { + "raw": "http://localhost:8080/rest/Test%20API/0.0.1/order/:id", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "rest", + "Test%20API", + "0.0.1", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "123456" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Retrieve an existing order using its id" + }, + "response": [ + { + "id": "b06c6433-2409-4332-8b43-c2980fc609cd", + "name": "get-123456", + "originalRequest": { + "url": { + "raw": "http:///order/:id", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "123456" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50,\n \"items\": [\n {\n \"price\": 40,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20\n }\n },\n {\n \"price\": 10,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n}" + }, + { + "id": "297e7f99-8727-497c-a3ba-c735ff65b62a", + "name": "get-7891011", + "originalRequest": { + "url": { + "raw": "http:///order/:id", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "7891011" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": 7891011,\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20,\n \"items\": [\n {\n \"price\": 20,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n}" + } + ] + }, + { + "name": "http:///order/:id", + "request": { + "url": "http:///order/:id", + "method": "DELETE", + "header": [], + "body": {}, + "description": "Delete an existing order using its id" + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API no version.postman_collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API no version.postman_collection.json new file mode 100644 index 000000000..d955be449 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API no version.postman_collection.json @@ -0,0 +1,397 @@ +{ + "variables": [], + "info": { + "name": "Test API", + "_postman_id": "48430400-3a9b-28dc-4f16-c1c404eee237", + "description": "Description for Test API using Apicurio Studio", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "order", + "description": "Folder for order", + "item": [ + { + "name": "http:///order?status={{status}}&page={{page}}", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "", + "console.log(\"status: \" + environment.status);", + "console.log(\"page: \" + environment.page);", + "", + "var schema = {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": { \"type\": \"string\" },", + " \"status\": { \"type\": \"string\", \"enum\": [\"pending_approval\"] }", + " }", + " }", + "};", + "", + "tests[\"Valid response\"] = tv4.validate(jsonData, schema);", + "console.log(\"Validation failed: \", tv4.error);", + "" + ] + } + }, + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "postman.setEnvironmentVariable(\"status\", \"pending_approval\");", + "postman.setEnvironmentVariable(\"page\", \"0\");" + ] + } + } + ], + "request": { + "url": { + "raw": "http://localhost:8080/rest/Test%20API/0.0.1/order?status=pending_approval&page=0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "rest", + "Test%20API", + "0.0.1", + "order" + ], + "query": [ + { + "key": "status", + "value": "pending_approval", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Retrieve a list of orders" + }, + "response": [ + { + "id": "3f0597ff-975b-492e-927b-bcec40b491f0", + "name": "list-pending_approval", + "originalRequest": { + "url": { + "raw": "http:///order?status=pending_approval&page=0", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order" + ], + "query": [ + { + "key": "status", + "value": "pending_approval", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[\n {\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50,\n \"items\": [\n {\n \"price\": 40,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20\n }\n },\n {\n \"price\": 10,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n },\n {\n \"id\": \"7891011\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20,\n \"items\": [\n {\n \"price\": 20,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n }\n]" + }, + { + "id": "80668c70-e9cc-436e-bae1-056f018261cb", + "name": "list-approved", + "originalRequest": { + "url": { + "raw": "http:///order?status=approved&page=0", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order" + ], + "query": [ + { + "key": "status", + "value": "approved", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[\n\t{\n\t \"id\": \"121314\",\n\t \"status\": \"approved\",\n\t \"price\": 4,\n\t \"quantity\": 2,\n\t \"item\": {\n\t \"id\": \"2\",\n\t \"label\": \"Bar\",\n\t \"price\": 2\n\t }\n\t}\n]" + } + ] + }, + { + "name": "http:///order", + "request": { + "url": "http:///order", + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "The order to create" + }, + "description": "Create (place) a new order" + }, + "response": [ + { + "id": "709884b2-7fc9-400c-b632-3f99689ae788", + "name": "create-123456", + "originalRequest": { + "url": "http:///order", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"devise\": \"EUR\",\n\t\"amount\": 50.0,\n\t\"items\": [\n\t\t{\n\t\t\t\"price\": 40.0,\n\t\t\t\"quantity\": 2,\n\t\t\t\"item\": {\n\t\t\t\t\"id\": \"1\",\n\t\t\t\t\"label\": \"Foo\",\n\t\t\t\t\"price\": 20.0\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"price\": 10.0,\n\t\t\t\"quantity\": 5,\n\t\t\t\"item\": {\n\t\t\t\t\"id\": \"2\",\n\t\t\t\t\"label\": \"Bar\",\n\t\t\t\t\"price\": 2.0\n\t\t\t}\n\t\t}\n\t]\n}" + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50.0,\n \"items\": [\n {\n \"price\": 40.0,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20.0\n }\n },\n {\n \"price\": 10.0,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + }, + { + "id": "f36d1b1c-17a5-45e6-a575-db148ac26c17", + "name": "create-7891011", + "originalRequest": { + "url": "http:///order", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"devise\": \"EUR\",\n \"amount\": 20.0,\n \"items\": [\n {\n \"price\": 20.0,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": 7891011,\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20.0,\n \"items\": [\n {\n \"price\": 20.0,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + } + ] + }, + { + "name": "http:///order/:id", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "var expectedId = request.url.slice(request.url.lastIndexOf('/order/') + 7);", + "tests[\"Check id value\"] = jsonData.id === expectedId;" + ] + } + }, + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "", + "" + ] + } + } + ], + "request": { + "url": { + "raw": "http://localhost:8080/rest/Test%20API/0.0.1/order/:id", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "rest", + "Test%20API", + "0.0.1", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "123456" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Retrieve an existing order using its id" + }, + "response": [ + { + "id": "b06c6433-2409-4332-8b43-c2980fc609cd", + "name": "get-123456", + "originalRequest": { + "url": { + "raw": "http:///order/:id", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "123456" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50,\n \"items\": [\n {\n \"price\": 40,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20\n }\n },\n {\n \"price\": 10,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n}" + }, + { + "id": "297e7f99-8727-497c-a3ba-c735ff65b62a", + "name": "get-7891011", + "originalRequest": { + "url": { + "raw": "http:///order/:id", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "7891011" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": 7891011,\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20,\n \"items\": [\n {\n \"price\": 20,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n}" + } + ] + }, + { + "name": "http:///order/:id", + "request": { + "url": "http:///order/:id", + "method": "DELETE", + "header": [], + "body": {}, + "description": "Delete an existing order using its id" + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API.postman_collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API.postman_collection.json new file mode 100644 index 000000000..fcde93514 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/Test API.postman_collection.json @@ -0,0 +1,397 @@ +{ + "variables": [], + "info": { + "name": "Test API", + "_postman_id": "48430400-3a9b-28dc-4f16-c1c404eee237", + "description": "version=0.0.1 - Description for Test API using Apicurio Studio", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "order", + "description": "Folder for order", + "item": [ + { + "name": "http:///order?status={{status}}&page={{page}}", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "", + "console.log(\"status: \" + environment.status);", + "console.log(\"page: \" + environment.page);", + "", + "var schema = {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"id\": { \"type\": \"string\" },", + " \"status\": { \"type\": \"string\", \"enum\": [\"pending_approval\"] }", + " }", + " }", + "};", + "", + "tests[\"Valid response\"] = tv4.validate(jsonData, schema);", + "console.log(\"Validation failed: \", tv4.error);", + "" + ] + } + }, + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "postman.setEnvironmentVariable(\"status\", \"pending_approval\");", + "postman.setEnvironmentVariable(\"page\", \"0\");" + ] + } + } + ], + "request": { + "url": { + "raw": "http://localhost:8080/rest/Test%20API/0.0.1/order?status=pending_approval&page=0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "rest", + "Test%20API", + "0.0.1", + "order" + ], + "query": [ + { + "key": "status", + "value": "pending_approval", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Retrieve a list of orders" + }, + "response": [ + { + "id": "3f0597ff-975b-492e-927b-bcec40b491f0", + "name": "list-pending_approval", + "originalRequest": { + "url": { + "raw": "http:///order?status=pending_approval&page=0", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order" + ], + "query": [ + { + "key": "status", + "value": "pending_approval", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[\n {\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50,\n \"items\": [\n {\n \"price\": 40,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20\n }\n },\n {\n \"price\": 10,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n },\n {\n \"id\": \"7891011\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20,\n \"items\": [\n {\n \"price\": 20,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n }\n]" + }, + { + "id": "80668c70-e9cc-436e-bae1-056f018261cb", + "name": "list-approved", + "originalRequest": { + "url": { + "raw": "http:///order?status=approved&page=0", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order" + ], + "query": [ + { + "key": "status", + "value": "approved", + "equals": true, + "description": "" + }, + { + "key": "page", + "value": "0", + "equals": true, + "description": "" + } + ], + "variable": [] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "[\n\t{\n\t \"id\": \"121314\",\n\t \"status\": \"approved\",\n\t \"price\": 4,\n\t \"quantity\": 2,\n\t \"item\": {\n\t \"id\": \"2\",\n\t \"label\": \"Bar\",\n\t \"price\": 2\n\t }\n\t}\n]" + } + ] + }, + { + "name": "http:///order", + "request": { + "url": "http:///order", + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "The order to create" + }, + "description": "Create (place) a new order" + }, + "response": [ + { + "id": "709884b2-7fc9-400c-b632-3f99689ae788", + "name": "create-123456", + "originalRequest": { + "url": "http:///order", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"devise\": \"EUR\",\n\t\"amount\": 50.0,\n\t\"items\": [\n\t\t{\n\t\t\t\"price\": 40.0,\n\t\t\t\"quantity\": 2,\n\t\t\t\"item\": {\n\t\t\t\t\"id\": \"1\",\n\t\t\t\t\"label\": \"Foo\",\n\t\t\t\t\"price\": 20.0\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"price\": 10.0,\n\t\t\t\"quantity\": 5,\n\t\t\t\"item\": {\n\t\t\t\t\"id\": \"2\",\n\t\t\t\t\"label\": \"Bar\",\n\t\t\t\t\"price\": 2.0\n\t\t\t}\n\t\t}\n\t]\n}" + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50.0,\n \"items\": [\n {\n \"price\": 40.0,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20.0\n }\n },\n {\n \"price\": 10.0,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + }, + { + "id": "f36d1b1c-17a5-45e6-a575-db148ac26c17", + "name": "create-7891011", + "originalRequest": { + "url": "http:///order", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"devise\": \"EUR\",\n \"amount\": 20.0,\n \"items\": [\n {\n \"price\": 20.0,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": 7891011,\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20.0,\n \"items\": [\n {\n \"price\": 20.0,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2.0\n }\n }\n ]\n}" + } + ] + }, + { + "name": "http:///order/:id", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "var expectedId = request.url.slice(request.url.lastIndexOf('/order/') + 7);", + "tests[\"Check id value\"] = jsonData.id === expectedId;" + ] + } + }, + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "", + "" + ] + } + } + ], + "request": { + "url": { + "raw": "http://localhost:8080/rest/Test%20API/0.0.1/order/:id", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "rest", + "Test%20API", + "0.0.1", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "123456" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "description": "Retrieve an existing order using its id" + }, + "response": [ + { + "id": "b06c6433-2409-4332-8b43-c2980fc609cd", + "name": "get-123456", + "originalRequest": { + "url": { + "raw": "http:///order/:id", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "123456" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": \"123456\",\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 50,\n \"items\": [\n {\n \"price\": 40,\n \"quantity\": 2,\n \"item\": {\n \"id\": \"1\",\n \"label\": \"Foo\",\n \"price\": 20\n }\n },\n {\n \"price\": 10,\n \"quantity\": 5,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n}" + }, + { + "id": "297e7f99-8727-497c-a3ba-c735ff65b62a", + "name": "get-7891011", + "originalRequest": { + "url": { + "raw": "http:///order/:id", + "host": [ + "http:" + ], + "port": "", + "path": [ + "", + "", + "order", + ":id" + ], + "query": [], + "variable": [ + { + "description": "", + "key": "id", + "value": "7891011" + } + ] + }, + "method": "GET", + "header": [], + "body": {} + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "_postman_previewtype": "parsed", + "header": [], + "cookie": [], + "responseTime": 0, + "body": "{\n \"id\": 7891011,\n \"status\": \"pending_approval\",\n \"devise\": \"EUR\",\n \"amount\": 20,\n \"items\": [\n {\n \"price\": 20,\n \"quantity\": 10,\n \"item\": {\n \"id\": \"2\",\n \"label\": \"Bar\",\n \"price\": 2\n }\n }\n ]\n}" + } + ] + }, + { + "name": "http:///order/:id", + "request": { + "url": "http:///order/:id", + "method": "DELETE", + "header": [], + "body": {}, + "description": "Delete an existing order using its id" + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-desc-collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-desc-collection.json new file mode 100644 index 000000000..aacd8f718 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-desc-collection.json @@ -0,0 +1,11 @@ +{ + "variables": [], + "info": { + "name": "Test API", + "_postman_id": "48430400-3a9b-28dc-4f16-c1c404eee237", + "description": "version=1.0.0 - Description for Test API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-digits-collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-digits-collection.json new file mode 100644 index 000000000..666f6a0d3 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-digits-collection.json @@ -0,0 +1,16 @@ +{ + "variables": [], + "info": { + "name": "Test API", + "_postman_id": "48430400-3a9b-28dc-4f16-c1c404eee237", + "description": "Description for Test API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "version": { + "major": 1, + "minor": 0, + "patch": 0 + } + }, + "item": [ + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-identifier-collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-identifier-collection.json new file mode 100644 index 000000000..2633a0af9 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-identifier-collection.json @@ -0,0 +1,17 @@ +{ + "variables": [], + "info": { + "name": "Test API", + "_postman_id": "48430400-3a9b-28dc-4f16-c1c404eee237", + "description": "Description for Test API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "version": { + "major": 2, + "minor": 1, + "patch": 0, + "identifier": "1.0.0" + } + }, + "item": [ + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-raw-collection.json b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-raw-collection.json new file mode 100644 index 000000000..20d00b566 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/postman/structured-version-raw-collection.json @@ -0,0 +1,12 @@ +{ + "variables": [], + "info": { + "name": "Test API", + "_postman_id": "48430400-3a9b-28dc-4f16-c1c404eee237", + "description": "Description for Test API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "version": "1.0.0" + }, + "item": [ + ] +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/GetDrivers-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/GetDrivers-soapui-project.xml new file mode 100644 index 000000000..be6307e4d --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/GetDrivers-soapui-project.xml @@ -0,0 +1,203 @@ + + + + + + + + file:/Users/lbroudou/Development/github/microcks/GetDrivers.wsdl + + Hours of Service (HOS) web service for Driverdata + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Returns the driver IDs of the drivers in thecompany. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hours of Service (HOS) web service for Driverdata. + + + + + + + +]]> + http://schemas.xmlsoap.org/wsdl/ + + + + http://MacBook-Pro:8088/mockDriverSoap + https://localhost:8080/ws/GetDrivers + + + + + + + + + + file:/Users/lbroudou/Development/github/microcks/GetDrivers.wsdl + + Hours of Service (HOS) web service for Driverdata + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Returns the driver IDs of the drivers in thecompany. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hours of Service (HOS) web service for Driverdata. + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/SEQUENTIALDriverSoapGetDrivers<xml-fragment/>UTF-8http://MacBook-Pro:8088/mockDriverSoap + + + + +]]>No Authorizationfalseversion1.0Response 1SCRIPTreturn "Sample Response" + + + + + + + + + 123 + 456 + 789 + + + + +]]> \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloAPI-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloAPI-soapui-project.xml new file mode 100644 index 000000000..9fddba9fa --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloAPI-soapui-project.xml @@ -0,0 +1,28 @@ + +This is a sample Hello APIhttp://api.example.comhttp://lbroudoux-OSX.local:8089/Get a new greeting message +Get a new greeting messagenameQUERYName of people to greetapplication/xml200application/json200application/xml405application/json405http://api.example.comTestSuite generated for REST Service [Hello API]SEQUENTIALTestCase generated for REST Resource [/hello] located at [/hello]<xml-fragment/>http://lbroudoux-OSX.local:8089/http://lbroudoux-OSX.local/v1/hello200greetingHello David !falsefalsefalse200No Authorization<xml-fragment/>UTF-8http://lbroudoux-OSX.local:8089/http://lbroudoux-OSX.local/v1/hello200greetingHello Gavin !falsefalsefalse200No Authorizationversion0.8Unknwon ResponseSCRIPT// Script dispatcher is used to select a response based on the incoming request. +// Here are few examples showing how to match based on path, query param, header and body + +// Match based on query parameter +def queryString = mockRequest.getRequest().getQueryString() +log.info "QueryString: " + queryString + +if( queryString.contains("David") ) +{ + // return the name of the response you want to dispatch + return "David Response" +} +else if( queryString.contains("Gavin") ) +{ + // return the name of the response you want to dispatch + return "Gavin Response" +} +else return "Unknown Response"{ + 'name':'David', + 'greeting':'Hello David !' +} +{ + 'name':'Gavin', + 'greeting':'Hello Gavin !' +} + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-random-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-random-soapui-project.xml new file mode 100644 index 000000000..2535bebda --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-random-soapui-project.xml @@ -0,0 +1,105 @@ + +file:/Users/lbroudou/Development/github/microcks/samples/HelloService.wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://lbroudoux-OSX.local:8088/mockHelloServicehttp://localhost:8080/services/HelloService<xml-fragment/>UTF-8http://localhost:8080/soap/HelloService Mock/0.9/ + + + + Karla + + +]]>No AuthorizationSEQUENTIALHelloServicesayHello<xml-fragment/>UTF-8http://:8088/mockHelloService + + + + whathever + + +]]>200declare namespace ser='http://www.example.com/hello'; +//ser:sayHelloResponse/sayHello<sayHello>Hello Andrew !</sayHello>false500No Authorizationfalseversion0.9Response 1RANDOM + + + + Hello Andrew ! + + +]]> + + + + Hello Karla ! + + +]]> + + + + soapenv:Sender + Unknown name + + + 999 + + + + +]]>Andrewdeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameAndrewAndrew ResponseKarladeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameKarlaKarla Response \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-soapui-project.xml new file mode 100644 index 000000000..8af579ce8 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-soapui-project.xml @@ -0,0 +1,130 @@ + +file:/Users/lbroudou/Development/github/microcks/samples/HelloService.wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://lbroudoux-OSX.local:8088/mockHelloServicehttp://localhost:8080/services/HelloService<xml-fragment/>UTF-8http://localhost:8080/soap/HelloService Mock/0.9/ + + + + Karla + + +]]>No AuthorizationSEQUENTIALHelloServicesayHello<xml-fragment/>UTF-8http://:8088/mockHelloService + + + + Andrew + + +]]>200declare namespace ser='http://www.example.com/hello'; +//ser:sayHelloResponse/sayHello<sayHello>Hello Andrew !</sayHello>false1500No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://localhost:8080/services/HelloService + + + + Karla + + +]]>No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://lbroudoux-OSX.local:8088/mockHelloService + + + + World + + +]]>500No Authorizationfalseversion0.9Response 1SCRIPTimport com.eviware.soapui.support.XmlHolder +def holder = new XmlHolder( mockRequest.requestContent ) +def name = holder["//name"] + +if (name == "Andrew"){ + return "Andrew Response" +} else if (name == "Karla"){ + return "Karla Response" +} else { + return "World Response" +} + + + + + Hello Andrew ! + + +]]> + + + + Hello Karla ! + + +]]> + + + + soapenv:Sender + Unknown name + + + 999 + + + + +]]>Andrewdeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameAndrewAndrew ResponseKarladeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameKarlaKarla Response \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-to-set-proxy-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-to-set-proxy-soapui-project.xml new file mode 100644 index 000000000..d943c62fd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-to-set-proxy-soapui-project.xml @@ -0,0 +1,120 @@ + +file:/Users/lbroudou/Development/github/microcks/samples/HelloService.wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://lbroudoux-OSX.local:8088/mockHelloServicehttp://localhost:8080/services/HelloService<xml-fragment/>UTF-8http://localhost:8080/soap/HelloService Mock/0.9/ + + + + Karla + + +]]>No AuthorizationSEQUENTIALHelloServicesayHello<xml-fragment/>UTF-8http://:8088/mockHelloService + + + + Andrew + + +]]>200declare namespace ser='http://www.example.com/hello'; + //ser:sayHelloResponse/sayHello<sayHello>Hello Andrew !</sayHello>false500No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://localhost:8080/services/HelloService + + + + Karla + + +]]>No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://lbroudoux-OSX.local:8088/mockHelloService + + + + World + + +]]>500No Authorizationfalseversion0.9Response 1QUERY_MATCH + + + + Hello Andrew ! + + +]]> + + + + Hello Karla ! + + +]]> + + + + soapenv:Sender + Unknown name + + + 999 + + + + +]]>Andrewdeclare namespace ser='http://www.example.com/hello'; + //ser:sayHello/nameAndrewAndrew ResponseKarladeclare namespace ser='http://www.example.com/hello'; + //ser:sayHello/nameKarlaKarla ResponseWorlddeclare namespace ser='http://www.example.com/hello'; + //ser:sayHello/nameWorldWorld Response diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-to-test-proxy-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-to-test-proxy-soapui-project.xml new file mode 100644 index 000000000..9d1799c3c --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/HelloService-to-test-proxy-soapui-project.xml @@ -0,0 +1,120 @@ + +file:/Users/lbroudou/Development/github/microcks/samples/HelloService.wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://lbroudoux-OSX.local:8088/mockHelloServicehttp://localhost:8080/services/HelloService<xml-fragment/>UTF-8http://localhost:8080/soap/HelloService Real/0.9/ + + + + Garry + + +]]>No AuthorizationSEQUENTIALHelloServicesayHello<xml-fragment/>UTF-8http://:8088/mockHelloService + + + + Andrew + + +]]>200declare namespace ser='http://www.example.com/hello'; +//ser:sayHelloResponse/sayHello<sayHello>Hello Andrew !</sayHello>false500No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://localhost:8080/services/HelloService + + + + Garry + + +]]>No AuthorizationHelloServicesayHello<xml-fragment/>UTF-8http://lbroudoux-OSX.local:8088/mockHelloService + + + + World + + +]]>500No Authorizationfalseversion0.9Response 1QUERY_MATCH + + + + Hello Real Andrew ! + + +]]> + + + + Hello Real Garry ! + + +]]> + + + + soapenv:Sender + Unknown name + + + 999 + + + + +]]>Andrewdeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameAndrewAndrew ResponseGarrydeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameGarryGarry ResponseWorlddeclare namespace ser='http://www.example.com/hello'; +//ser:sayHello/nameWorldWorld Response diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-Product-GetProductElements-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-Product-GetProductElements-soapui-project.xml new file mode 100644 index 000000000..90d309335 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-Product-GetProductElements-soapui-project.xml @@ -0,0 +1,138 @@ + +http://localhost:57002/soa-infra/services/Product-10/Product_GetProductElements/Product_GetProductElements?wsdl + + http://localhost:57002/soa-infra/services/Product-10/Product_GetProductElements!1.0/SAM/Product_GetProductElements_v1.0.wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://localhost:57002/soa-infra/services/Product-10/Product_GetProductElements/Product_GetProductElements?XSD=xsd/Product_Anomalie_v1.0.xsd + + + + + + + + +]]>http://www.w3.org/2001/XMLSchemahttp://localhost:57002/soa-infra/services/Product-10/Product_GetProductElements/Product_GetProductElementshttp://snl09705:8088/mockexecute_pttBinding_GetProductElementsUTF-8http://localhost:57002/soa-infra/services/Product-10/Product_GetProductElements/Product_GetProductElements + + + + ? + ? + + ? + + ? + ? + + ? + + ? + + +]]>Global HTTP SettingsfalseSEQUENCEResponse 1 + + + + + ? + + ? + + + + ? + + ? + + ? + + + + + ? + + ? + + ? + + + +]]>version1.0.0 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-no-version-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-no-version-soapui-project.xml new file mode 100644 index 000000000..ddd707ffd --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-no-version-soapui-project.xml @@ -0,0 +1,137 @@ + +http://localhost:8181/RefTest/services/HelloService?wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://localhost:8080/smock/smock/HelloServiceSoapBinding MockService/1.2/sayHello/?delay=500http://localhost:8088/mockHelloServiceSoapBindinghttp://localhost:8181/RefTest/services/HelloServicehttp://snl09705:8088/mockHelloServiceSoapBinding<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="x-authtoken" value="55f03f2bbdc24a68af786a20b4bcccd4"/> + <con:entry key="x-userid" value="s026210"/> + <con:entry key="x-requestid" value="123456789"/> +</xml-fragment>UTF-8http://localhost:8181/RefTest/services/HelloService + + + + + Laurent + + +]]>Global HTTP Settings<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="Authorization" value="Basic YWRtaW46YWRtaW4="/> + <con:entry key="x-userid" value="s026210"/> + <con:entry key="x-requestid" value="123456789"/> +</xml-fragment>UTF-8http://localhost:8080/soap/HelloServiceSoapBinding/1.2/ + + + + + Laurent + + +]]>BasicBasicGlobal HTTP Settings<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="Authorization" value="Basic YWRtaW46YWRtaW4="/> + <con:entry key="x-userid" value="s026210"/> + <con:entry key="x-requestid" value="123456789"/> +</xml-fragment>UTF-8http://localhost:8080/soap/HelloServiceSoapBinding/1.2/ + + + + + Laurent + + +]]>BasicBasicGlobal HTTP SettingsSEQUENTIALHelloServiceSoapBindingsayHello<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="Authorization" value="Basic YWRtaW46YWRtaW4="/> + <con:entry key="x-userid" value="s026210"/> + <con:entry key="x-requestid" value="123456789"/> +</xml-fragment>UTF-8http://localhost:8088/mockHelloServiceSoapBinding + + + + + Anne + + +]]>Hello Anne !falsefalseGlobal HTTP SettingsHelloServiceSoapBindingsayHello<xml-fragment/>UTF-8http://localhost:8181/RefTest/services/HelloService + + + + + Laurent + + +]]>Global HTTP SettingsfalseQUERY_MATCHResponse 1 + + + + + Hello Laurent ! + + +]]> + + + + + Hello Anne ! + + +]]>Laurentdeclare namespace ser='http://lbroudoux.github.com/test/service'; +//ser:sayHello/name + +LaurentLaurent ResponseAnnedeclare namespace ser='http://lbroudoux.github.com/test/service'; +//ser:sayHello/nameAnneAnne Responserevision1 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-script-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-script-soapui-project.xml new file mode 100644 index 000000000..0856eda1b --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-script-soapui-project.xml @@ -0,0 +1,113 @@ + +http://localhost:8181/RefTest/services/HelloService?wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://localhost:8181/RefTest/services/HelloServicehttp://snl09705:8088/mockHelloServiceSoapBindingUTF-8http://localhost:8181/RefTest/services/HelloService + + + + + ? + + +]]>SEQUENTIALHelloServiceSoapBindingsayHello<xml-fragment/>UTF-8http://snl09705:8088/mockHelloServiceSoapBinding + + + + + Anne + + +]]>Global HTTP SettingsHelloServiceSoapBindingsayHello<xml-fragment/>UTF-8http://localhost:8181/RefTest/services/HelloService + + + + + Laurent + + +]]>Global HTTP Settingsfalseimport com.eviware.soapui.support.XmlHolder + +def holder = new XmlHolder( mockRequest.requestContent ) + +def name = holder["//name"] + +if (name == "Anne"){ + return "Anne Response" +} else if (name == "Laurent"){ + return "Laurent Response" +} else { + return "Laurent Response" +}SCRIPTResponse 1 + + + + + Hello Laurent Broudoux ! + + +]]> + + + + + Hello Anne Broudoux ! + + +]]>version1.0 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-soapui-project.xml new file mode 100644 index 000000000..0176e0909 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/RefTest-soapui-project.xml @@ -0,0 +1,137 @@ + +http://localhost:8181/RefTest/services/HelloService?wsdl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://schemas.xmlsoap.org/wsdl/http://localhost:8080/smock/smock/HelloServiceSoapBinding MockService/1.2/sayHello/?delay=500http://localhost:8088/mockHelloServiceSoapBindinghttp://localhost:8181/RefTest/services/HelloServicehttp://snl09705:8088/mockHelloServiceSoapBinding<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="x-authtoken" value="55f03f2bbdc24a68af786a20b4bcccd4"/> + <con:entry key="x-userid" value="s026210"/> + <con:entry key="x-requestid" value="123456789"/> +</xml-fragment>UTF-8http://localhost:8181/RefTest/services/HelloService + + + + + Laurent + + +]]>Global HTTP Settings<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="Authorization" value="Basic YWRtaW46YWRtaW4="/> + <con:entry key="x-userid" value="s026210"/> + <con:entry key="x-requestid" value="123456789"/> +</xml-fragment>UTF-8http://localhost:8080/soap/HelloServiceSoapBinding/1.2/ + + + + + Laurent + + +]]>BasicBasicGlobal HTTP Settings<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="Authorization" value="Basic YWRtaW46YWRtaW4="/> + <con:entry key="x-userid" value="s026210"/> + <con:entry key="x-requestid" value="123456789"/> +</xml-fragment>UTF-8http://localhost:8080/soap/HelloServiceSoapBinding/1.2/ + + + + + Laurent + + +]]>BasicBasicGlobal HTTP SettingsSEQUENTIALHelloServiceSoapBindingsayHello<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="Authorization" value="Basic YWRtaW46YWRtaW4="/> + <con:entry key="x-userid" value="s026210"/> + <con:entry key="x-requestid" value="123456789"/> +</xml-fragment>UTF-8http://localhost:8088/mockHelloServiceSoapBinding + + + + + Anne + + +]]>Hello Anne !falsefalseGlobal HTTP SettingsHelloServiceSoapBindingsayHello<xml-fragment/>UTF-8http://localhost:8181/RefTest/services/HelloService + + + + + Laurent + + +]]>Global HTTP SettingsfalseQUERY_MATCHResponse 1 + + + + + Hello Laurent ! + + +]]> + + + + + Hello Anne ! + + +]]>Laurentdeclare namespace ser='http://lbroudoux.github.com/test/service'; +//ser:sayHello/name + +LaurentLaurent ResponseAnnedeclare namespace ser='http://lbroudoux.github.com/test/service'; +//ser:sayHello/nameAnneAnne Responseversion1.2revision1 \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/Test-REST-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/Test-REST-soapui-project.xml new file mode 100644 index 000000000..c434b3b45 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/Test-REST-soapui-project.xml @@ -0,0 +1,179 @@ + +http://localhost:8080/whabedhttp://localhost:8088/mockwhabedhttp://snl09705:8080/componentcomponentTEMPLATEcomponentversionversionTEMPLATEversiontext/html;charset=UTF-8404htmlapplication/json;charset=UTF-8200json:Responsetext/html;charset=UTF-8200html404data404dataapplication/json200json:Responsetext/html; charset=iso-8859-1200html<xml-fragment/>http://localhost:8080/whabedhttp://localhost/whabed/deployment/testREST/1.2.jsonNo Authorization + + +http://localhost:8080/whabedNo Authorization + + +environmentenvironmentTEMPLATEenvironmentqualifierqualifierTEMPLATEqualifierstartDate2013-09-25QUERY2013-09-25endDate2013-09-27QUERY2013-09-27application/json200404datahttp://localhost:8080/whabedNo Authorization + + +environmentqualifierstartDateendDatetext/html;charset=UTF-8404 500htmlapplication/json;charset=UTF-8200Responseapplication/json<entry key="Content-Type" value="application/json" xmlns="http://eviware.com/soapui/config"/>http://localhost:8080/whabed{ +"component" : {"name":"testREST", "version":"1.2"}, +"environment" : {"name":"QUALIF2", "qualifier":"p9"}, +"agent" : {"name":"soapui", "version":"5.0.0"} +}http://localhost/whabed/deploymentNo AuthorizationSEQUENTIAL<xml-fragment/>http://localhost:8088/mockwhabedhttp://localhost/whabed/deployment/testREST/1.2.json200(?s).*deployments.*falsetrueimport com.eviware.soapui.support.XmlHolder + +assert messageExchange.responseHeaders["Content-Type"].contains("application/json;charset=UTF-8") + +log.info messageExchange.responseContentAsXml + +def holder = new XmlHolder(messageExchange.responseContentAsXml) +holder.namespaces["tns"] = "http://localhost/whabed/deployment/testREST/1.2.json" + +assert holder["count(//tns:deployments/tns:e)"] == "7"No Authorization + + +http://localhost:8080/whabed200No Authorization + + +<xml-fragment/>UTF-8http://localhost:8088/mockwhabedhttp://localhost/mockwhabed/deployment/byEnvironment/QUALIF2/cdsm.jsonNo Authorization + + + + +<xml-fragment/>UTF-8http://localhost:8088/mockwhabedhttp://localhost/mockwhabed/deployment/byEnvironment/QUALIF2/cdsm.jsonNo Authorization + + + + +version0.0.1testREST ResponseSEQUENCE/* +// Examples showing how to match based on path, query param and header +// Match based on path +def requestPath = mockRequest.getPath() +log.info "Path: "+ requestPath + +if( requestPath.contains("json") ) +{ + // return the name of the response you want to dispatch + return "JSON Response" +} + + +// Match based on query parameter +def queryString = mockRequest.getRequest().getQueryString() +log.info "QueryString: " + queryString + +if( queryString.contains("stockholm") ) +{ + // return the name of the response you want to dispatch + return "Response Stockholm" +} +else if( queryString.contains("london") ) +{ + // return the name of the response you want to dispatch + return "Response London" +} + + +// Match based on header +def acceptEncodingHeadeList = mockRequest.getRequestHeaders().get("Accept-Encoding") +log.info "AcceptEncodig Header List: " + acceptEncodingHeadeList + +if( acceptEncodingHeadeList.contains("gzip,deflate") ) +{ + // return the name of the response you want to dispatch + return "GZiped Response" +} + +*/ +{}SEQUENCE/* +// Examples showing how to match based on path, query param and header +// Match based on path +def requestPath = mockRequest.getPath() +log.info "Path: "+ requestPath + +if( requestPath.contains("json") ) +{ + // return the name of the response you want to dispatch + return "JSON Response" +} + + +// Match based on query parameter +def queryString = mockRequest.getRequest().getQueryString() +log.info "QueryString: " + queryString + +if( queryString.contains("stockholm") ) +{ + // return the name of the response you want to dispatch + return "Response Stockholm" +} +else if( queryString.contains("london") ) +{ + // return the name of the response you want to dispatch + return "Response London" +} + + +// Match based on header +def acceptEncodingHeadeList = mockRequest.getRequestHeaders().get("Accept-Encoding") +log.info "AcceptEncodig Header List: " + acceptEncodingHeadeList + +if( acceptEncodingHeadeList.contains("gzip,deflate") ) +{ + // return the name of the response you want to dispatch + return "GZiped Response" +} + +*/ +{"deployments":[{"startDate":1411635297497,"agent":{"name":"soapui","version":"5.0.0","deployments":[]},"component":{"name":"testREST","version":"1.2","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"p9","deployments":[]}},{"startDate":1411636274845,"agent":{"name":"soapui","version":"5.0.0","deployments":[]},"component":{"name":"testREST","version":"1.2","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"p9","deployments":[]}},{"startDate":1411636383627,"agent":{"name":"soapui","version":"5.0.0","deployments":[]},"component":{"name":"testREST","version":"1.2","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"p9","deployments":[]}},{"startDate":1411636400887,"agent":{"name":"soapui","version":"5.0.0","deployments":[]},"component":{"name":"testREST","version":"1.2","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"p9","deployments":[]}},{"startDate":1411636406559,"agent":{"name":"soapui","version":"5.0.0","deployments":[]},"component":{"name":"testREST","version":"1.2","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"p9","deployments":[]}},{"startDate":1411636412826,"agent":{"name":"soapui","version":"5.0.0","deployments":[]},"component":{"name":"testREST","version":"1.2","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"p9","deployments":[]}},{"startDate":1411636753587,"agent":{"name":"soapui","version":"5.0.0","deployments":[]},"component":{"name":"testREST","version":"1.2","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"p9","deployments":[]}}]}SEQUENCEdef requestPath = mockRequest.getPath() +log.info "Path: "+ requestPath + +/* +// Examples showing how to match based on path, query param and header +// Match based on path +def requestPath = mockRequest.getPath() +log.info "Path: "+ requestPath + +if( requestPath.contains("json") ) +{ + // return the name of the response you want to dispatch + return "JSON Response" +} + + +// Match based on query parameter +def queryString = mockRequest.getRequest().getQueryString() +log.info "QueryString: " + queryString + +if( queryString.contains("stockholm") ) +{ + // return the name of the response you want to dispatch + return "Response Stockholm" +} +else if( queryString.contains("london") ) +{ + // return the name of the response you want to dispatch + return "Response London" +} + + +// Match based on header +def acceptEncodingHeadeList = mockRequest.getRequestHeaders().get("Accept-Encoding") +log.info "AcceptEncodig Header List: " + acceptEncodingHeadeList + +if( acceptEncodingHeadeList.contains("gzip,deflate") ) +{ + // return the name of the response you want to dispatch + return "GZiped Response" +} + +*/ +{"deployments":[]}deploymentsForQUALIF2cdsm2 ResponseSCRIPT + +// Match based on query parameter +def queryString = mockRequest.getRequest().getQueryString() +log.info "QueryString: " + queryString + +if ( queryString.contains("startDate=2013-09") ){ + // return the name of the response you want to dispatch + log.info "Returning deploymentsForQUALIF2cdsm Response" + return "deploymentsForQUALIF2cdsm Response" +} +else if ( queryString.contains("startDate=2013-10") ){ + // return the name of the response you want to dispatch + log.info "Returning deploymentsForQUALIF2cdsm2 Response" + return "deploymentsForQUALIF2cdsm2 Response" +} +{"deployments":[{"startDate":1411635297497,"agent":{"name":"soapui","version":"5.0.0","deployments":[]},"component":{"name":"testREST","version":"1.2","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"cdsm","deployments":[]}}}]}{"deployments":[{"startDate":1411635297497,"agent":{"name":"soapui","version":"5.1.0","deployments":[]},"component":{"name":"testREST","version":"1.3","deployments":[]},"environment":{"name":"QUALIF2","qualifier":"cdsm","deployments":[]}}}]} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/VIES-soapui-project.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/VIES-soapui-project.xml new file mode 100644 index 000000000..4ef32a825 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/util/soapui/VIES-soapui-project.xml @@ -0,0 +1,391 @@ + + + + + + + + file:\C:\Users\PRIVATE\Documents\mock_soap\soapui\VITALE\VIES\VIES_ServiceVitale_v2.0.wsdl + + + Services du SI Vitale + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Services du SI Vitale + + + + +]]> + http://schemas.xmlsoap.org/wsdl/ + + + file:\C:\Users\PRIVATE\Documents\mock_soap\soapui\VITALE\VIES\xsd\Suivi_Parcours_Carte_2.0.xsd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + http://www.w3.org/2001/XMLSchema + + + + http://HOST:PORT/MetierVIESws/Vitale-v2 + + + + + + + + SEQUENTIAL + + + + + + VitaleServiceBinding-v2 + recupererSuiviParcoursCarte + + + <xml-fragment/> + + + UTF-8 + http://HOST:PORT/MetierVIESws/Vitale-v2 + \r + \r + \r + \r + 1561044109273\r + 1\r + 1\r + \r + \r +]]> + + No Authorization + + + + + + + + + + + + VitaleServiceBinding-v2 + recupererSuiviParcoursCarte + + + <xml-fragment/> + + + UTF-8 + http://HOST:PORT/MetierVIESws/Vitale-v2 + \r + \r + \r + \r + 1561044109273\r + 2\r + 2\r + \r + \r +]]> + + + + + + + + + No Authorization + + + + + + + + + + + + VitaleServiceBinding-v2 + recupererSuiviParcoursCarte + + + <xml-fragment/> + + + UTF-8 + http://HOST:PORT/MetierVIESws/Vitale-v2 + \r + \r + \r + \r + 1561044109274\r + 3\r + 2\r + \r + \r +]]> + + + + + + + + + No Authorization + + + + + + + + + + + + + + + false + + + + version + 060000 + + + + + Bad Response + SCRIPT + import com.eviware.soapui.support.XmlHolder + def holder = new XmlHolder(mockRequest.requestContent) + def nir = holder["//nir"] + def caisse = holder["//caisse"] + def regime = holder["//regime"] + + if (nir == "1561044109273") { + if (caisse == "1" && regime == "1") { + // requestContext.numEtape = 3 + return "Toto Response" + } + if (caisse == "2" && regime == "2") { + // requestContext.numEtape = 5 + return "Titi Response" + } + } + // requestContext.numEtape = 1 + return "Bad Response" + + + + + + + + + 000 + Fin normale + ${numEtape} + 9 + carteEnPersonnalisation + 2007-03-19+01:00 + + +]]> + + + + + + + + + 000 + Fin normale + ${numEtape} + 3 + envoiFormulaire + 2007-03-19+01:00 + + 1 + Réexpédiée suite à demande technicien + 1 + 3583 + + + +]]> + + + + + + + + + 000 + Fin normale + ${numEtape} + 17 + carteNonValide + 2007-03-19+01:00 + + 2009-12-30+01:00 + 467100017 + + Invalide + + + + +]]> + + + + + + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/web/pastry-for-test-openapi.yaml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/web/pastry-for-test-openapi.yaml new file mode 100644 index 000000000..33a7f4392 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/io/github/microcks/web/pastry-for-test-openapi.yaml @@ -0,0 +1,201 @@ +--- +openapi: 3.0.2 +info: + title: pastry-for-test + version: 2.0.0 + description: API definition of API Pastry sample app + contact: + name: Laurent Broudoux + url: http://github.com/lbroudoux + email: laurent.broudoux@gmail.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /pastry: + summary: Global operations on pastries + get: + tags: + - pastry + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pastry' + examples: + pastries_json: + value: + - name: Baba Rhum + description: Delicieux Baba au Rhum pas calorique du tout + size: L + price: 3.2 + status: available + - name: Divorces + description: Delicieux Divorces pas calorique du tout + size: M + price: 2.8 + status: available + - name: Tartelette Fraise + description: Delicieuse Tartelette aux Fraises fraiches + size: S + price: 2 + status: available + description: Get list of pastries + operationId: GetPastries + summary: Get list of pastries + /pastry/{name}: + summary: Specific operation on pastry + get: + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + Millefeuille: + value: Millefeuille + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.5 + status: available + Millefeuille: + value: + name: Millefeuille + description: Delicieux Millefeuille pas calorique du tout + size: L + price: 4.4 + status: available + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: |- + + Eclair Cafe + Delicieux Eclair au Cafe pas calorique du tout + M + 2.5 + available + + description: Pastry with specified name + operationId: GetPastryByName + summary: Get Pastry by name + description: Get Pastry by name + patch: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + price: 2.6 + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: "\n\t2.6\n" + required: true + parameters: + - examples: + Eclair Cafe: + value: Eclair Cafe + Eclair Cafe Xml: + value: Eclair Cafe + name: name + description: pastry name + schema: + type: string + in: path + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe: + value: + name: Eclair Cafe + description: Delicieux Eclair au Cafe pas calorique du tout + size: M + price: 2.6 + status: available + text/xml: + schema: + $ref: '#/components/schemas/Pastry' + examples: + Eclair Cafe Xml: + value: |- + + Eclair Cafe + Delicieux Eclair au Cafe pas calorique du tout + M + 2.6 + available + + description: Changed pastry + operationId: PatchPastry + summary: Patch existing pastry + parameters: + - name: name + description: pastry name + schema: + type: string + in: path + required: true +components: + schemas: + Pastry: + title: Root Type for Pastry + description: The root of the Pastry type's schema. + type: object + properties: + name: + description: Name of this pastry + type: string + description: + description: A short description of this pastry + type: string + size: + description: Size of pastry (S, M, L) + type: string + price: + format: double + description: Price (in USD) of this pastry + type: number + status: + description: Status in stock (available, out_of_stock) + type: string + example: + name: My Pastry + description: A short description os my pastry + size: M + price: 4.5 + status: available +tags: +- name: pastry + description: Pastry resource diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/logback-test.xml b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/logback-test.xml new file mode 100644 index 000000000..3533d4e87 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/webapp/karma.conf.js b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/webapp/karma.conf.js new file mode 100644 index 000000000..3592be4f2 --- /dev/null +++ b/jdk_21_maven/cs/rest-gui/microcks/webapp/src/test/webapp/karma.conf.js @@ -0,0 +1,76 @@ +// Karma configuration +// http://karma-runner.github.io/0.10/config/configuration-file.html + +module.exports = function(config) { + config.set({ + // base path, that will be used to resolve files and exclude + basePath: '../../../', + + // testing framework to use (jasmine/mocha/qunit/...) + frameworks: ['jasmine'], + + // list of files / patterns to load in the browser + files: [ + 'bower_components/jquery/dist/jquery.js', + 'bower_components/angular/angular.js', + 'bower_components/angular-mocks/angular-mocks.js', + 'bower_components/angular-resource/angular-resource.js', + 'bower_components/angular-cookies/angular-cookies.js', + 'bower_components/angular-sanitize/angular-sanitize.js', + 'bower_components/angular-route/angular-route.js', + 'bower_components/angular-bootstrap/ui-bootstrap-tpls.js', + 'bower_components/lodash/dist/lodash.compat.js', + 'src/main/webapp/scripts/directives.js', + 'src/main/webapp/scripts/services/*.js', + 'src/main/webapp/scripts/controllers/*.js', + 'src/main/webapp/directives/*.html', + 'src/main/webapp/styles/*.css', + 'src/main/webapp/views/**/*.html', + 'src/main/webapp/scripts/app.js' + ], + + preprocessors: { + '**/*.jade': 'ng-jade2js', + '**/*.html': 'html2js', + '**/*.coffee': 'coffee', + }, + + ngHtml2JsPreprocessor: { + stripPrefix: 'src/main/webapp/' + }, + + ngJade2JsPreprocessor: { + stripPrefix: 'src/main/webapp/' + }, + + // list of files / patterns to exclude + exclude: [], + + // web server port + port: 8080, + + // level of logging + // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG + logLevel: config.LOG_DEBUG, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: ['PhantomJS'], + + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false + }); +};